|
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 |
@@ -837,3 +838,60 @@ def add_to_envelope_with_reentrant_add(envelope): |
837 | 838 | assert reentrant_add_called |
838 | 839 | # If the re-entrancy guard didn't work, this test would hang and it'd |
839 | 840 | # eventually be timed out by pytest-timeout |
| 841 | + |
| 842 | + |
| 843 | +@pytest.mark.skipif( |
| 844 | + sys.platform == "win32" |
| 845 | + or not hasattr(os, "fork") |
| 846 | + or not hasattr(os, "register_at_fork"), |
| 847 | + reason="requires POSIX fork and os.register_at_fork (Python 3.7+)", |
| 848 | +) |
| 849 | +def test_log_batcher_lock_reset_in_child_after_fork(sentry_init): |
| 850 | + """Regression test for the LogBatcher fork-deadlock fix. |
| 851 | +
|
| 852 | + If os.fork() runs while another thread holds LogBatcher._lock, the |
| 853 | + child inherits the lock locked. The holding thread does not exist in |
| 854 | + the child, so the lock can never be released and _ensure_thread |
| 855 | + deadlocks forever. The after-fork hook must replace the lock with a |
| 856 | + fresh one in the child and reset |
| 857 | + _flusher / _flusher_pid / _buffer / _active / _flush_event. |
| 858 | + """ |
| 859 | + sentry_init(enable_logs=True) |
| 860 | + batcher = sentry_sdk.get_client().log_batcher |
| 861 | + assert batcher is not None |
| 862 | + |
| 863 | + original_lock = batcher._lock |
| 864 | + original_lock.acquire() |
| 865 | + |
| 866 | + batcher._buffer.append(object()) |
| 867 | + batcher._active.flag = True |
| 868 | + batcher._flush_event.set() |
| 869 | + batcher._running = False |
| 870 | + |
| 871 | + pid = os.fork() |
| 872 | + if pid == 0: |
| 873 | + replaced = batcher._lock is not original_lock |
| 874 | + unheld = batcher._lock.acquire(blocking=False) |
| 875 | + |
| 876 | + flusher_reset = batcher._flusher is None and batcher._flusher_pid is None |
| 877 | + buffer_reset = len(batcher._buffer) == 0 |
| 878 | + active_reset = not getattr(batcher._active, "flag", False) |
| 879 | + |
| 880 | + event_reset = not batcher._flush_event.is_set() |
| 881 | + running_reset = batcher._running is True |
| 882 | + |
| 883 | + os._exit( |
| 884 | + 0 |
| 885 | + if replaced |
| 886 | + and unheld |
| 887 | + and flusher_reset |
| 888 | + and buffer_reset |
| 889 | + and active_reset |
| 890 | + and event_reset |
| 891 | + and running_reset |
| 892 | + else 1 |
| 893 | + ) |
| 894 | + |
| 895 | + original_lock.release() |
| 896 | + _, status = os.waitpid(pid, 0) |
| 897 | + assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0 |
0 commit comments