|
1 | 1 | import json |
2 | 2 | import logging |
| 3 | +import os |
3 | 4 | import sys |
4 | 5 | import time |
5 | 6 | from typing import List, Any, Mapping, Union |
@@ -819,3 +820,40 @@ def add_to_envelope_with_reentrant_add(envelope): |
819 | 820 | assert reentrant_add_called |
820 | 821 | # If the re-entrancy guard didn't work, this test would hang and it'd |
821 | 822 | # eventually be timed out by pytest-timeout |
| 823 | + |
| 824 | + |
| 825 | +@pytest.mark.skipif( |
| 826 | + sys.platform == "win32" |
| 827 | + or not hasattr(os, "fork") |
| 828 | + or not hasattr(os, "register_at_fork"), |
| 829 | + reason="requires POSIX fork and os.register_at_fork (Python 3.7+)", |
| 830 | +) |
| 831 | +def test_log_batcher_lock_reset_in_child_after_fork(sentry_init): |
| 832 | + """Regression test for the LogBatcher fork-deadlock fix. |
| 833 | +
|
| 834 | + If os.fork() runs while another thread holds LogBatcher._lock, the |
| 835 | + child inherits the lock locked. The holding thread does not exist in |
| 836 | + the child, so the lock can never be released and _ensure_thread |
| 837 | + deadlocks forever. The after-fork hook must replace the lock with a |
| 838 | + fresh one in the child and reset _flusher / _flusher_pid. |
| 839 | + """ |
| 840 | + sentry_init(enable_logs=True) |
| 841 | + batcher = sentry_sdk.get_client().log_batcher |
| 842 | + assert batcher is not None |
| 843 | + |
| 844 | + original_lock = batcher._lock |
| 845 | + original_lock.acquire() |
| 846 | + pid = os.fork() |
| 847 | + if pid == 0: |
| 848 | + # Child: was the lock object replaced and is the new one not |
| 849 | + # held? Without the fix, _lock is `original_lock` inherited |
| 850 | + # locked, so `replaced` is False. blocking=False guarantees the |
| 851 | + # child can't hang on a regression. |
| 852 | + replaced = batcher._lock is not original_lock |
| 853 | + unheld = batcher._lock.acquire(blocking=False) |
| 854 | + flusher_reset = batcher._flusher is None and batcher._flusher_pid is None |
| 855 | + os._exit(0 if replaced and unheld and flusher_reset else 1) |
| 856 | + |
| 857 | + original_lock.release() |
| 858 | + _, status = os.waitpid(pid, 0) |
| 859 | + assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0 |
0 commit comments