Skip to content

Commit 4247ffc

Browse files
committed
test: Move batcher fork safety test to batcher tests
1 parent fb587da commit 4247ffc

2 files changed

Lines changed: 68 additions & 65 deletions

File tree

tests/tracing/test_span_batcher.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import os
2+
import sys
13
import time
24
from unittest import mock
35

6+
import pytest
7+
48
import sentry_sdk
59
from sentry_sdk._span_batcher import SpanBatcher
610

@@ -425,3 +429,67 @@ def test_transport_format(sentry_init, capture_envelopes):
425429
assert "value" in value
426430
assert "type" in value
427431
assert value["type"] in ("string", "boolean", "integer", "double", "array")
432+
433+
434+
@pytest.mark.skipif(
435+
sys.platform == "win32"
436+
or not hasattr(os, "fork")
437+
or not hasattr(os, "register_at_fork"),
438+
reason="requires POSIX fork and os.register_at_fork (Python 3.7+)",
439+
)
440+
def test_span_batcher_lock_reset_in_child_after_fork(sentry_init):
441+
"""Regression test for the SpanBatcher fork-deadlock fix.
442+
443+
If os.fork() runs while another thread holds SpanBatcher._lock, the
444+
child inherits the lock locked. The holding thread does not exist in
445+
the child, so the lock can never be released and _ensure_thread
446+
deadlocks forever. The after-fork hook must replace the lock with a
447+
fresh one in the child and reset
448+
_flusher / _flusher_pid / _span_buffer / _running_size / _active /
449+
_flush_event.
450+
"""
451+
sentry_init(
452+
traces_sample_rate=1.0,
453+
_experiments={"trace_lifecycle": "stream"},
454+
)
455+
batcher = sentry_sdk.get_client().span_batcher
456+
assert batcher is not None
457+
458+
original_lock = batcher._lock
459+
original_lock.acquire()
460+
461+
batcher._span_buffer["test-trace-id"].append(object())
462+
batcher._running_size["test-trace-id"] = 42
463+
batcher._active.flag = True
464+
batcher._flush_event.set()
465+
batcher._running = False
466+
467+
pid = os.fork()
468+
if pid == 0:
469+
replaced = batcher._lock is not original_lock
470+
unheld = batcher._lock.acquire(blocking=False)
471+
472+
flusher_reset = batcher._flusher is None and batcher._flusher_pid is None
473+
span_buffer_reset = len(batcher._span_buffer) == 0
474+
running_size_reset = len(batcher._running_size) == 0
475+
476+
active_reset = not getattr(batcher._active, "flag", False)
477+
event_reset = not batcher._flush_event.is_set()
478+
running_reset = batcher._running is True
479+
480+
os._exit(
481+
0
482+
if replaced
483+
and unheld
484+
and flusher_reset
485+
and span_buffer_reset
486+
and running_size_reset
487+
and active_reset
488+
and event_reset
489+
and running_reset
490+
else 1
491+
)
492+
493+
original_lock.release()
494+
_, status = os.waitpid(pid, 0)
495+
assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0

tests/tracing/test_span_streaming.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
import os
32
import re
43
import sys
54
import time
@@ -1542,67 +1541,3 @@ def test_default_attributes(sentry_init, capture_envelopes):
15421541
"sentry.release": {"value": "1.0.0", "type": "string"},
15431542
"sentry.origin": {"value": "manual", "type": "string"},
15441543
}
1545-
1546-
1547-
@pytest.mark.skipif(
1548-
sys.platform == "win32"
1549-
or not hasattr(os, "fork")
1550-
or not hasattr(os, "register_at_fork"),
1551-
reason="requires POSIX fork and os.register_at_fork (Python 3.7+)",
1552-
)
1553-
def test_span_batcher_lock_reset_in_child_after_fork(sentry_init):
1554-
"""Regression test for the SpanBatcher fork-deadlock fix.
1555-
1556-
If os.fork() runs while another thread holds SpanBatcher._lock, the
1557-
child inherits the lock locked. The holding thread does not exist in
1558-
the child, so the lock can never be released and _ensure_thread
1559-
deadlocks forever. The after-fork hook must replace the lock with a
1560-
fresh one in the child and reset
1561-
_flusher / _flusher_pid / _span_buffer / _running_size / _active /
1562-
_flush_event.
1563-
"""
1564-
sentry_init(
1565-
traces_sample_rate=1.0,
1566-
_experiments={"trace_lifecycle": "stream"},
1567-
)
1568-
batcher = sentry_sdk.get_client().span_batcher
1569-
assert batcher is not None
1570-
1571-
original_lock = batcher._lock
1572-
original_lock.acquire()
1573-
1574-
batcher._span_buffer["test-trace-id"].append(object())
1575-
batcher._running_size["test-trace-id"] = 42
1576-
batcher._active.flag = True
1577-
batcher._flush_event.set()
1578-
batcher._running = False
1579-
1580-
pid = os.fork()
1581-
if pid == 0:
1582-
replaced = batcher._lock is not original_lock
1583-
unheld = batcher._lock.acquire(blocking=False)
1584-
1585-
flusher_reset = batcher._flusher is None and batcher._flusher_pid is None
1586-
span_buffer_reset = len(batcher._span_buffer) == 0
1587-
running_size_reset = len(batcher._running_size) == 0
1588-
1589-
active_reset = not getattr(batcher._active, "flag", False)
1590-
event_reset = not batcher._flush_event.is_set()
1591-
running_reset = batcher._running is True
1592-
1593-
os._exit(
1594-
0
1595-
if replaced
1596-
and unheld
1597-
and flusher_reset
1598-
and span_buffer_reset
1599-
and running_size_reset
1600-
and active_reset
1601-
and event_reset
1602-
and running_reset
1603-
else 1
1604-
)
1605-
1606-
original_lock.release()
1607-
_, status = os.waitpid(pid, 0)
1608-
assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0

0 commit comments

Comments
 (0)