"""Graceful shutdown handling for fault-tolerant translation. Provides signal handling to allow clean shutdown when the process receives SIGINT (Ctrl+C) or SIGTERM, ensuring job state is saved. """ from __future__ import annotations import signal import threading from types import FrameType from typing import Any, Callable class GracefulShutdown: """Context manager for graceful shutdown handling. Captures SIGINT and SIGTERM signals and sets a flag that can be checked by worker code to stop gracefully. Example: with GracefulShutdown() as shutdown: while not shutdown.should_stop: # Do work... process_next_verse() Attributes: should_stop: True when shutdown has been requested. signal_received: The signal number that was received, or None. """ def __init__(self) -> None: """Initialize the graceful shutdown handler.""" self._should_stop = threading.Event() self._signal_received: int | None = None self._original_handlers: dict[int, Any] = {} self._lock = threading.Lock() @property def should_stop(self) -> bool: """Check if shutdown has been requested. Returns: True if SIGINT or SIGTERM was received. """ return self._should_stop.is_set() @property def signal_received(self) -> int | None: """Get the signal number that triggered shutdown. Returns: Signal number (e.g., signal.SIGINT) or None if no signal received. """ return self._signal_received def request_stop(self) -> None: """Programmatically request a stop (without signal). Useful for testing or other shutdown triggers. """ self._should_stop.set() def wait_for_stop(self, timeout: float | None = None) -> bool: """Wait for shutdown signal. Args: timeout: Maximum time to wait in seconds, or None for indefinite. Returns: True if stop was signaled, False if timeout occurred. """ return self._should_stop.wait(timeout=timeout) def _signal_handler(self, signum: int, frame: FrameType | None) -> None: """Handle shutdown signals. Args: signum: Signal number received. frame: Current stack frame (unused). """ with self._lock: if not self._should_stop.is_set(): self._signal_received = signum self._should_stop.set() def __enter__(self) -> GracefulShutdown: """Enter the context manager and install signal handlers. Signal handlers can only be installed from the main thread. When used from a non-main thread, the context manager still works but without signal handling (should_stop can still be set via request_stop()). Returns: Self for use in context manager. """ if threading.current_thread() is threading.main_thread(): # Store original handlers self._original_handlers[signal.SIGINT] = signal.signal( signal.SIGINT, self._signal_handler ) self._original_handlers[signal.SIGTERM] = signal.signal( signal.SIGTERM, self._signal_handler ) return self def __exit__( self, exc_type: type | None, exc_val: BaseException | None, exc_tb: Any | None, ) -> bool: """Exit the context manager and restore signal handlers. Args: exc_type: Exception type if an exception occurred. exc_val: Exception value if an exception occurred. exc_tb: Exception traceback if an exception occurred. Returns: False to propagate any exception. """ # Restore original handlers for signum, handler in self._original_handlers.items(): signal.signal(signum, handler) self._original_handlers.clear() return False class ShutdownCallback: """Utility for registering shutdown callbacks. Allows registering cleanup functions to be called when shutdown occurs. Example: shutdown = GracefulShutdown() callbacks = ShutdownCallback(shutdown) callbacks.register(save_state) callbacks.register(close_connections) with shutdown: while not shutdown.should_stop: do_work() callbacks.run_all() # Called after exiting context """ def __init__(self, shutdown: GracefulShutdown) -> None: """Initialize with a GracefulShutdown instance. Args: shutdown: The shutdown handler to monitor. """ self._shutdown = shutdown self._callbacks: list[Callable[[], None]] = [] self._lock = threading.Lock() def register(self, callback: Callable[[], None]) -> None: """Register a callback to run on shutdown. Args: callback: Function to call during shutdown cleanup. """ with self._lock: self._callbacks.append(callback) def run_all(self) -> list[Exception]: """Run all registered callbacks. Runs each callback, collecting any exceptions that occur. All callbacks are attempted even if some fail. Returns: List of exceptions that occurred (empty if all succeeded). """ exceptions = [] with self._lock: for callback in self._callbacks: try: callback() except Exception as e: exceptions.append(e) return exceptions def ignore_signals_in_child() -> None: """Configure signal handling for child processes. Child processes in a multiprocessing pool should ignore SIGINT/SIGTERM and let the parent handle shutdown coordination. """ signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGTERM, signal.SIG_IGN)