You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Lock permanently replaces the host application's SIGINT/SIGTERM handlers #1310
When allow_multiple_runs=False, EmissionsTracker constructs a Lock, and Lock.__init__ installs its own SIGINT/SIGTERM handlers with signal.signal(...). This only happens when the tracker is built on the main thread — codecarbon/lock.py:32 guards it with if threading.current_thread() is threading.main_thread(): — but on that path the return value (the previous handler) is discarded and nothing ever restores it.
Two user-visible consequences:
KeyboardInterrupt stops being raised. Python's default SIGINT handler is what turns Ctrl-C into KeyboardInterrupt; after a Lock exists, Ctrl-C raises SystemExit(1) from Lock._handle_exit instead, so except KeyboardInterrupt: blocks in the embedding application no longer run. CodeCarbon's own at codecarbon/cli/monitor.py:95 is affected, and that is what terminates the tracked subprocess.
An application handler installed before the tracker is silently destroyed. A web server, trainer or job runner that registered a graceful-shutdown handler no longer receives SIGTERM.
Reproduction
importsignalfromcodecarbonimportEmissionsTrackersignal.signal(signal.SIGTERM, my_graceful_shutdown)
tracker=EmissionsTracker(allow_multiple_runs=False)
# signal.getsignal(signal.SIGTERM) is now Lock._handle_exit;# my_graceful_shutdown will never run.
Root cause
codecarbon/lock.py:34-35 overwrite both handlers without saving them:
_handle_exit (lock.py:37-41) releases the lock and unconditionally raises SystemExit(1) rather than delegating to whatever was there before, and Lock.release() (lock.py:57-68) only removes the lock file — it never restores signal state. The Lock is created at codecarbon/emissions_tracker.py:170.
Expected vs actual
Expected: CodeCarbon releases its lock file on a signal and then lets the application's original handler (or the Python default, so KeyboardInterrupt is raised) take over; after release() the handlers are back to what they were.
Actual: the original handlers are lost for the lifetime of the process and every signal ends in SystemExit(1).
A secondary, smaller issue in the same constructor: atexit.register(self.release) (lock.py:26-28) is never unregistered, so every Lock ever built stays reachable for the process lifetime.
Activity