Skip to content

Commit 4dbc80e

Browse files
authored
test: Move batcher fork safety test to batcher tests (#6225)
Just moving one test case to another file.
1 parent 0784b92 commit 4dbc80e

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
@@ -1520,67 +1519,3 @@ def test_default_attributes(sentry_init, capture_envelopes):
15201519
"sentry.release": {"value": "1.0.0", "type": "string"},
15211520
"sentry.origin": {"value": "manual", "type": "string"},
15221521
}
1523-
1524-
1525-
@pytest.mark.skipif(
1526-
sys.platform == "win32"
1527-
or not hasattr(os, "fork")
1528-
or not hasattr(os, "register_at_fork"),
1529-
reason="requires POSIX fork and os.register_at_fork (Python 3.7+)",
1530-
)
1531-
def test_span_batcher_lock_reset_in_child_after_fork(sentry_init):
1532-
"""Regression test for the SpanBatcher fork-deadlock fix.
1533-
1534-
If os.fork() runs while another thread holds SpanBatcher._lock, the
1535-
child inherits the lock locked. The holding thread does not exist in
1536-
the child, so the lock can never be released and _ensure_thread
1537-
deadlocks forever. The after-fork hook must replace the lock with a
1538-
fresh one in the child and reset
1539-
_flusher / _flusher_pid / _span_buffer / _running_size / _active /
1540-
_flush_event.
1541-
"""
1542-
sentry_init(
1543-
traces_sample_rate=1.0,
1544-
_experiments={"trace_lifecycle": "stream"},
1545-
)
1546-
batcher = sentry_sdk.get_client().span_batcher
1547-
assert batcher is not None
1548-
1549-
original_lock = batcher._lock
1550-
original_lock.acquire()
1551-
1552-
batcher._span_buffer["test-trace-id"].append(object())
1553-
batcher._running_size["test-trace-id"] = 42
1554-
batcher._active.flag = True
1555-
batcher._flush_event.set()
1556-
batcher._running = False
1557-
1558-
pid = os.fork()
1559-
if pid == 0:
1560-
replaced = batcher._lock is not original_lock
1561-
unheld = batcher._lock.acquire(blocking=False)
1562-
1563-
flusher_reset = batcher._flusher is None and batcher._flusher_pid is None
1564-
span_buffer_reset = len(batcher._span_buffer) == 0
1565-
running_size_reset = len(batcher._running_size) == 0
1566-
1567-
active_reset = not getattr(batcher._active, "flag", False)
1568-
event_reset = not batcher._flush_event.is_set()
1569-
running_reset = batcher._running is True
1570-
1571-
os._exit(
1572-
0
1573-
if replaced
1574-
and unheld
1575-
and flusher_reset
1576-
and span_buffer_reset
1577-
and running_size_reset
1578-
and active_reset
1579-
and event_reset
1580-
and running_reset
1581-
else 1
1582-
)
1583-
1584-
original_lock.release()
1585-
_, status = os.waitpid(pid, 0)
1586-
assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0

0 commit comments

Comments
 (0)