Merge pull request #6567 from stefanpieter/fix/sigusr1-stack-dump-no-restart

fix: keep SIGUSR1 stack dumps non-terminating
This commit is contained in:
nesquena-hermes 2026-07-29 10:53:29 -07:00 committed by GitHub
commit 4b3fdde31b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 1 deletions

View file

@ -268,8 +268,11 @@ def install_crash_visibility(
sigusr1 = getattr(signal, "SIGUSR1", None)
if sigusr1 is not None:
try:
# A diagnostic signal must not inherit SIGUSR1's default
# terminate action after the dump. Chaining here made a
# hang probe kill the server and trigger service restarts.
faulthandler.register(
sigusr1, file=_FAULT_STREAM, all_threads=True, chain=True
sigusr1, file=_FAULT_STREAM, all_threads=True, chain=False
)
except Exception:
pass

View file

@ -23,8 +23,12 @@ No live server socket is required — the helpers are exercised directly.
"""
import faulthandler
import logging
import os
import signal
import subprocess
import sys
import threading
from pathlib import Path
import pytest
@ -122,6 +126,41 @@ def test_install_is_idempotent(fresh_hooks):
assert cv.install_crash_visibility(stream=stream, register_sigusr1_dump=False) is False
@pytest.mark.skipif(not hasattr(signal, "SIGUSR1"), reason="SIGUSR1 is POSIX-only")
def test_sigusr1_stack_dump_does_not_terminate_process():
"""The diagnostic signal must dump stacks without chaining to SIG_DFL.
Chaining invokes SIGUSR1's default action after faulthandler writes the dump,
terminating the WebUI process and prompting service managers to restart it.
Exercise the real signal in a child so a regression cannot kill pytest.
"""
script = """
import os
import signal
from api.crash_visibility import install_crash_visibility
# An ignored SIGUSR1 disposition survives exec on POSIX. Force the production
# default so chain=True would terminate this child after writing the dump.
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
install_crash_visibility()
os.kill(os.getpid(), signal.SIGUSR1)
print('survived-sigusr1', flush=True)
"""
completed = subprocess.run(
[sys.executable, "-c", script],
cwd=os.fspath(Path(__file__).resolve().parents[1]),
capture_output=True,
text=True,
timeout=10,
check=False,
)
assert completed.returncode == 0, completed.stderr
assert "survived-sigusr1" in completed.stdout
assert "Current thread" in completed.stderr
assert "most recent call first" in completed.stderr
# ── 2. threading.excepthook ─────────────────────────────────────────────────
def test_thread_excepthook_installed(fresh_hooks):
stream, _ = fresh_hooks