Skip to content

Commit

Permalink
better testing and threadsafety
Browse files Browse the repository at this point in the history
  • Loading branch information
holmanb committed Jan 8, 2025
1 parent 033cecf commit 87b6c4c
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 7 deletions.
14 changes: 11 additions & 3 deletions cloudinit/signal_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import contextlib
import inspect
import threading
import logging
import signal
import sys
Expand Down Expand Up @@ -36,6 +37,7 @@ class ExitBehavior(NamedTuple):
SIGNAL_EXIT_BEHAVIOR_CRASH: Final = ExitBehavior(1, logging.ERROR)
SIGNAL_EXIT_BEHAVIOR_QUIET: Final = ExitBehavior(0, logging.INFO)
_SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_CRASH
_SUSPEND_WRITE_LOCK = threading.RLock()


def inspect_handler(sig: Union[int, Callable, None]) -> None:
Expand Down Expand Up @@ -99,8 +101,14 @@ def suspend_crash():
This allow signal handling without a crash where it is expected. The
call stack is still printed if signal is received during this context, but
the return code is 0 and no traceback is printed.
Threadsafe.
"""
global _SIGNAL_EXIT_BEHAVIOR
_SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_QUIET
yield
_SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_CRASH

# If multiple threads simultaneously were to modify this
# global state, this function would not behave as expected.
with _SUSPEND_WRITE_LOCK:
_SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_QUIET
yield
_SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_CRASH
31 changes: 27 additions & 4 deletions tests/unittests/test_signal_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

from cloudinit import signal_handler

REENTRANT = "reentrant"

@patch.object(signal_handler.sys, "exit", Mock())
class TestSignalHandler:

@pytest.mark.parametrize(
Expand All @@ -21,7 +21,30 @@ class TestSignalHandler:
(1, inspect.currentframe()),
],
)
def test_suspend_signal(self, m_args):
@pytest.mark.parametrize(
"m_suspended",
[
(REENTRANT, 0),
(True, 0),
(False, 1),
],
)
def test_suspend_signal(self, m_args, m_suspended):
"""suspend_crash should prevent crashing (exit 1) on signal
otherwise cloud-init should exit 1
"""
sig, frame = m_args
with signal_handler.suspend_crash():
signal_handler._handle_exit(sig, frame)
suspended, rc = m_suspended

with patch.object(signal_handler.sys, "exit", Mock()) as m_exit:
if suspended is True:
with signal_handler.suspend_crash():
signal_handler._handle_exit(sig, frame)
elif suspended == REENTRANT:
with signal_handler.suspend_crash(
), signal_handler.suspend_crash():
signal_handler._handle_exit(sig, frame)
else:
signal_handler._handle_exit(sig, frame)
m_exit.assert_called_with(rc)

0 comments on commit 87b6c4c

Please sign in to comment.