Skip to content

Lock permanently replaces the host application's SIGINT/SIGTERM handlers #1310

Open

Description

@davidberenstein1957

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:

  1. 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.
  2. 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

import signal
from codecarbon import EmissionsTracker

signal.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:

signal.signal(signal.SIGINT, self._handle_exit)
signal.signal(signal.SIGTERM, self._handle_exit)

_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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions