1+ import os
12import sys
23import threading
34import signal
67from types import FrameType
78from typing import Any , Callable , Dict , Iterator , Optional , Union
89
9- from dlt .common import logger
1010from dlt .common .exceptions import SignalReceivedException
1111
1212_received_signal : int = 0
1313exit_event = Event ()
1414_signal_counts : Dict [int , int ] = {}
1515_original_handlers : Dict [int , Union [int , Callable [[int , Optional [FrameType ]], Any ]]] = {}
1616
17+ # NOTE: do not use logger and print in signal handlers
1718
18- def signal_receiver (sig : int , frame : FrameType ) -> None :
19+
20+ def _signal_receiver (sig : int , frame : FrameType ) -> None :
1921 """Handle POSIX signals with two-stage escalation.
2022
21- This handler is installed by delayed_signals (). On the first occurrence of a
23+ This handler is installed by intercepted_signals (). On the first occurrence of a
2224 supported signal (eg. SIGINT, SIGTERM) it requests a graceful shutdown by
2325 setting a process-wide flag and waking sleeping threads via exit_event.
2426 A second occurrence of the same signal escalates by delegating to the
@@ -34,49 +36,61 @@ def signal_receiver(sig: int, frame: FrameType) -> None:
3436 Worker threads must cooperatively observe shutdown via raise_if_signalled()
3537 or the signal-aware sleep().
3638 """
37- global _received_signal
38-
3939 # track how many times this signal type has been received
4040 _signal_counts [sig ] = _signal_counts .get (sig , 0 ) + 1
4141
4242 if _signal_counts [sig ] == 1 :
4343 # first signal of this type: set flag and wake threads
44- _received_signal = sig
45- if sig == signal .SIGINT :
46- sig_desc = "CTRL-C"
47- else :
48- sig_desc = f"Signal { sig } "
49- msg = (
50- f"{ sig_desc } received. Trying to shut down gracefully. It may take time to drain job"
51- f" pools. Send { sig_desc } again to force stop."
52- )
44+ set_received_signal (sig )
5345 if sys .stdin .isatty ():
54- # log to console
55- sys .stderr .write (msg )
56- sys .stderr .flush ()
57- else :
58- logger .warning (msg )
46+ # log to console using low level functions that are safe for signal handlers
47+ if sig == signal .SIGINT :
48+ sig_desc = "CTRL-C"
49+ else :
50+ sig_desc = f"Signal { sig } "
51+ msg = (
52+ f"{ sig_desc } received. Trying to shut down gracefully. It may take time to drain"
53+ f" job pools. Send { sig_desc } again to force stop."
54+ )
55+ try :
56+ os .write (sys .stderr .fileno (), msg .encode (encoding = "utf-8" ))
57+ except OSError :
58+ pass
5959 elif _signal_counts [sig ] >= 2 :
60- # Second signal of this type: call original handler
61- logger .debug (f"Second signal { sig } received, calling default handler" )
60+ # second signal of this type: call original handler
6261 original_handler = _original_handlers .get (sig , signal .SIG_DFL )
6362 if callable (original_handler ):
6463 original_handler (sig , frame )
6564 elif original_handler == signal .SIG_DFL :
66- # Restore default and re-raise to trigger default behavior
65+ # restore default and re-raise to trigger default behavior
6766 signal .signal (sig , signal .SIG_DFL )
6867 signal .raise_signal (sig )
6968
7069 exit_event .set ()
71- logger .debug ("Sleeping threads signalled" )
70+
71+
72+ def _clear_signals () -> None :
73+ global _received_signal
74+
75+ _received_signal = 0
76+ _signal_counts .clear ()
77+ _original_handlers .clear ()
78+
79+
80+ def set_received_signal (sig : int ) -> None :
81+ """Called when signal was received"""
82+ global _received_signal
83+
84+ _received_signal = sig
7285
7386
7487def raise_if_signalled () -> None :
75- if _received_signal :
88+ """Raises `SignalReceivedException` if signal was received."""
89+ if was_signal_received ():
7690 raise SignalReceivedException (_received_signal )
7791
7892
79- def signal_received () -> bool :
93+ def was_signal_received () -> bool :
8094 """check if a signal was received"""
8195 return True if _received_signal else False
8296
@@ -93,18 +107,10 @@ def wake_all() -> None:
93107 exit_event .set ()
94108
95109
96- def _clear_signals () -> None :
97- global _received_signal
98-
99- _received_signal = 0
100- _signal_counts .clear ()
101- _original_handlers .clear ()
102-
103-
104110@contextmanager
105- def delayed_signals () -> Iterator [None ]:
106- """Will delay signalling until `raise_if_signalled` is explicitly used or when
107- a second signal with the same int value arrives.
111+ def intercepted_signals () -> Iterator [None ]:
112+ """Will intercept SIGINT and SIGTERM and will delay calling signal handlers until
113+ `raise_if_signalled` is explicitly used or when a second signal with the same int value arrives.
108114
109115 A no-op when not called on main thread.
110116
@@ -115,7 +121,7 @@ def delayed_signals() -> Iterator[None]:
115121 # check if handlers are already installed (nested call)
116122 current_sigint_handler = signal .getsignal (signal .SIGINT )
117123
118- if current_sigint_handler is signal_receiver :
124+ if current_sigint_handler is _signal_receiver :
119125 # already installed, this is a nested call - just yield
120126 yield
121127 return
@@ -129,14 +135,16 @@ def delayed_signals() -> Iterator[None]:
129135 _original_handlers [signal .SIGTERM ] = original_sigterm_handler
130136
131137 try :
132- signal .signal (signal .SIGINT , signal_receiver )
133- signal .signal (signal .SIGTERM , signal_receiver )
138+ signal .signal (signal .SIGINT , _signal_receiver )
139+ signal .signal (signal .SIGTERM , _signal_receiver )
134140 yield
135141 finally :
136142 signal .signal (signal .SIGINT , original_sigint_handler )
137143 signal .signal (signal .SIGTERM , original_sigterm_handler )
138144 _clear_signals ()
139145
140146 else :
147+ from dlt .common import logger
148+
141149 logger .info ("Running in daemon thread, signals not enabled" )
142150 yield
0 commit comments