Skip to content

fix: flush coverage data on SIGTERM/SIGINT#32362

Open
bartlomieju wants to merge 6 commits intodenoland:mainfrom
bartlomieju:fix/coverage-sigterm-flush
Open

fix: flush coverage data on SIGTERM/SIGINT#32362
bartlomieju wants to merge 6 commits intodenoland:mainfrom
bartlomieju:fix/coverage-sigterm-flush

Conversation

@bartlomieju
Copy link
Member

Summary

Fixes coverage data loss when a Deno process with DENO_COVERAGE_DIR is
terminated via SIGTERM or SIGINT (e.g. by Playwright's webServer teardown,
Docker, systemd, Ctrl+C).

Currently, CoverageCollector::stop_collecting() only runs via the normal exit
path in CliMainWorker::run(). If the process receives a termination signal,
the event loop exits immediately and coverage data is never written to disk.

Approach

When coverage collection is enabled, the event loop in cli/worker.rs uses
tokio::select! to race the normal event loop future against a termination
signal stream. On receiving SIGTERM or SIGINT:

  1. Break out of the event loop
  2. Skip unload/processExit event dispatch (process is being killed)
  3. Call stop_collecting() to flush coverage via V8 inspector
  4. Re-raise the original signal with the default handler so the process exits
    with the correct signal exit code

Changes

  • ext/signals/lib.rs — Added cross-platform helpers to deno_signals:

    • SignalStreamWithKind — async stream that reports which signal was caught
    • termination_signal_stream() — listens for SIGTERM+SIGINT on unix, SIGINT
      on Windows
    • raise_default_signal(signo) — re-raises a signal with the default handler
  • cli/worker.rs — Modified CliMainWorker::run() to intercept termination
    signals when coverage is enabled, flush coverage data, then re-raise the signal

  • tests/specs/coverage/sigterm_flush/ — Spec tests verifying coverage is
    written on SIGTERM for both main worker and web worker scenarios

Limitations

  • Unix only for SIGTERM (Windows has no cross-process SIGTERM equivalent)
  • SIGINT works on both platforms
  • Does not handle SIGKILL or Windows TerminateProcess (uncatchable by design).
    A follow-up PR will add periodic coverage flushing to handle force-kill
    scenarios.

Reproduction

The original issue was discovered with Playwright's webServer config:
Playwright sends SIGTERM (with gracefulShutdown) or SIGKILL (default) when
tearing down the dev server. With DENO_COVERAGE_DIR set in webServer.env,
coverage was never written.

Test plan

  • Spec test: cargo test sigterm_flush — spawns a Deno server with
    DENO_COVERAGE_DIR, sends SIGTERM, verifies coverage files are written
  • Web worker variant: same test but with a web worker, verifies both main
    and worker coverage are flushed (2 files)
  • Manual: run deno run --allow-net server.ts with DENO_COVERAGE_DIR set,
    send SIGTERM, verify .json files appear in coverage dir

🤖 Generated with Claude Code

bartlomieju and others added 6 commits February 26, 2026 16:57
Initial approach: intercept SIGTERM in the event loop when coverage
collection is enabled, break out of the loop, and run the normal
cleanup path (including stop_collecting).

This is a WIP — needs to be reworked to use the deno_signals::before_exit
pattern (like deno_telemetry does) instead of intercepting signals
in the event loop. The challenge is that CoverageCollector uses a
LocalInspectorSession which isn't Send, so it can't be called directly
from the before_exit callback thread.

Includes try_play/ directory with a Playwright + webServer reproduction
case demonstrating the bug.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Deno process with DENO_COVERAGE_DIR set receives SIGTERM
(e.g. from Playwright's webServer teardown), coverage data is now
flushed before exit. Previously the process would terminate without
writing coverage files.

Added sigterm_stream() and raise_sigterm_default() helpers to
deno_signals crate. The worker event loop intercepts SIGTERM when
coverage is enabled, breaks out to run the normal cleanup path
(which flushes coverage via V8 inspector), then re-raises SIGTERM
with the default handler for correct exit status.

Unix-only for now.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the sigterm_flush spec test to also verify that coverage data
from web workers is flushed when the main process receives SIGTERM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the unix-only SIGTERM interception with cross-platform
termination signal handling. Add `termination_signal_stream()` and
`raise_default_signal()` helpers to deno_signals crate, removing
platform-specific code from worker.rs.

On unix: intercepts both SIGTERM and SIGINT
On Windows: intercepts SIGINT (SIGTERM is not catchable on Windows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The try_play/ directory was used for local debugging and shouldn't
be part of the PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Member

@nathanwhit nathanwhit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@bartlomieju
Copy link
Member Author

Needs a bit more work to flush coverage data periodically to guard against SIGKILL (which can't be handled)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants