Skip to content

fix(agent-data-plane): handle SIGTERM for graceful shutdown - #2322

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
mainfrom
jszwedko/handle-sigterm-adp
Aug 13, 2026
Merged

fix(agent-data-plane): handle SIGTERM for graceful shutdown#2322
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
mainfrom
jszwedko/handle-sigterm-adp

Conversation

@jszwedko

@jszwedko jszwedko commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Human Summary

Adds support for handling SIGTERM on *nix platforms, which we expect to receive when being managed by SystemD or s6, which ADP is packaged with. On Windows, we where we are managed by procmgr we expect to receive a CTRL_BREAK_EVENT (ref) so handle that instead.

For dogstatsd replay we currently only support Linux so we only additionally handle SIGTERM. We expect to only be run interactively, but the process may still receive a SIGTERM from out-of-band.

AI Summary

ADP's production binaries only listened for SIGINT when deciding to enter graceful shutdown, but SIGINT is not the signal used in any of ADP's real deployment paths — systemd, container runtimes, and Kubernetes all send SIGTERM to request shutdown (during service stops/restarts, rollouts, evictions, node drains, and container termination). Since SIGTERM used its default disposition, ADP was killed outright instead of draining its topology, so buffered data could be lost and the shutdown-timeout/forceful-abort diagnostics never had a chance to fire. This wires SIGTERM into the same graceful-shutdown path already used for SIGINT, so the two behave identically. The dogstatsd replay debug subcommand gets the same treatment for consistency, since it also has state that should be flushed on interruption. SIGPIPE handling is being tracked separately and isn't part of this change.

Verified on a systemd-managed host by installing the current Datadog Agent package and swapping in a build of agent-data-plane with this fix:

  • Before the fix: systemctl stop datadog-agent-data-plane (and systemctl restart datadog-agent, which cascades a stop via BindsTo) killed the process in single-digit milliseconds with code=killed, signal=TERM and no shutdown-path log lines at all.
  • After the fix: the same systemctl stop produces Received SIGTERM, shutting down..., a clean topology drain (listeners and HTTP servers stopping), Agent Data Plane shut down successfully., and the process exits with status=0/SUCCESS.

Closes #2319.

Test plan

  • Verified under real systemd: patched agent-data-plane binary running as the actual datadog-agent-data-plane.service unit now drains and exits cleanly on systemctl stop / systemctl restart datadog-agent, versus being hard-killed before the fix.
  • Verified standalone: direct SIGTERM to the binary now triggers the same graceful-shutdown log sequence as SIGINT.

SIGTERM is the signal used by systemd, container runtimes, and Kubernetes to
request shutdown, but ADP only listened for SIGINT, so it terminated
immediately without draining the topology or honoring the shutdown timeout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pr-commenter

pr-commenter Bot commented Aug 12, 2026

Copy link
Copy Markdown

Binary Size Analysis (Agent Data Plane)

Baseline: ca31d5f · Comparison: 75b8f23 · diff
Analysis Configuration: stripped binaries · Pass/Fail Threshold: +5%
Sizes: 41.48 MiB (baseline) vs 41.55 MiB (comparison)
Size Change: +78.31 KiB (+0.18%)

✅ Binary size difference within threshold

Changes by Module
Module File Size Symbols
core +51.22 KiB 4240
anon.e254d0c82dc2bd8175b93ef7e3b1fb53.15.llvm.15624024362873184253 +17.53 KiB 1
anon.77fe6d9fde1247be6ecad764c1853154.512.llvm.7927372867784889850 -17.44 KiB 1
anon.77fe6d9fde1247be6ecad764c1853154.595.llvm.7927372867784889850 -12.38 KiB 1
anon.1d6497f9a04757386c21a0e9071d0246.666.llvm.6229194563851314955 +12.29 KiB 1
anon.77fe6d9fde1247be6ecad764c1853154.293.llvm.7927372867784889850 -9.81 KiB 1
anon.e254d0c82dc2bd8175b93ef7e3b1fb53.604.llvm.15624024362873184253 +9.80 KiB 1
agent_data_plane::internal::env +9.67 KiB 25
anon.e254d0c82dc2bd8175b93ef7e3b1fb53.19.llvm.15624024362873184253 +9.25 KiB 1
anon.77fe6d9fde1247be6ecad764c1853154.509.llvm.7927372867784889850 -9.25 KiB 1
anon.b2a88b0dfc3186b1df2976c29a0605be.290.llvm.9466618047265295794 -8.49 KiB 1
anon.58d7e071c6f28db3af2cf9de910d5af7.1.llvm.4032246748914016819 +8.40 KiB 1
agent_data_plane_config::_::_<impl serde_core +7.91 KiB 1
serde_core -7.66 KiB 110
tonic -7.41 KiB 242
anon.b2a88b0dfc3186b1df2976c29a0605be.21.llvm.9466618047265295794 -6.76 KiB 1
anon.58d7e071c6f28db3af2cf9de910d5af7.11.llvm.4032246748914016819 +6.67 KiB 1
anon.b2a88b0dfc3186b1df2976c29a0605be.292.llvm.9466618047265295794 -6.17 KiB 1
hashbrown +6.16 KiB 533
anon.58d7e071c6f28db3af2cf9de910d5af7.443.llvm.4032246748914016819 +6.08 KiB 1
Detailed Symbol Changes
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +82.6Ki  +0.6% +81.2Ki    [14312 Others]
 +51e2% +24.7Ki +67e2% +24.7Ki    agent_data_plane::state::metrics::rules::get_datadog_agent_remappings::h4ff388cc81845209
  [NEW] +17.5Ki  [NEW]     +76    anon.e254d0c82dc2bd8175b93ef7e3b1fb53.15.llvm.15624024362873184253
  [NEW] +14.7Ki  [NEW] +14.6Ki    agent_data_plane::state::metrics::rules::get_compat_remappings::hc7620de93b7e14a3
  [NEW] +12.3Ki  [NEW]     +81    anon.1d6497f9a04757386c21a0e9071d0246.666.llvm.6229194563851314955
  +940% +10.1Ki +12e2% +10.1Ki    _<agent_data_plane::internal::env::autodiscovery::AutodiscoveryEventBroadcaster as saluki_core::runtime::supervisor::Supervisable>::initialize::_{{closure}}::_{{closure}}::h68625f13ed344402
  [NEW] +9.80Ki  [NEW]     +79    anon.e254d0c82dc2bd8175b93ef7e3b1fb53.604.llvm.15624024362873184253
  [NEW] +9.25Ki  [NEW]     +91    anon.e254d0c82dc2bd8175b93ef7e3b1fb53.19.llvm.15624024362873184253
  [NEW] +8.40Ki  [NEW]     +81    anon.58d7e071c6f28db3af2cf9de910d5af7.1.llvm.4032246748914016819
  [NEW] +7.91Ki  [NEW] +7.73Ki    agent_data_plane_config::_::_<impl serde_core::ser::Serialize for agent_data_plane_config::SalukiConfiguration>::serialize::h7d4f52af0bf356b0
  [DEL] -6.76Ki  [DEL]     -77    anon.b2a88b0dfc3186b1df2976c29a0605be.21.llvm.9466618047265295794
  [DEL] -6.81Ki  [DEL] -6.69Ki    agent_data_plane::state::metrics::rules::transaction::get_transaction_remappings::hc6f843957d9f0dc4
  [DEL] -8.49Ki  [DEL]     -81    anon.b2a88b0dfc3186b1df2976c29a0605be.290.llvm.9466618047265295794
  [DEL] -8.56Ki  [DEL] -8.47Ki    serde_core::ser::SerializeMap::serialize_entry::he2bb07469f432877
  [DEL] -9.25Ki  [DEL]     -91    anon.77fe6d9fde1247be6ecad764c1853154.509.llvm.7927372867784889850
  [DEL] -9.81Ki  [DEL]     -79    anon.77fe6d9fde1247be6ecad764c1853154.293.llvm.7927372867784889850
  [DEL] -11.5Ki  [DEL] -11.4Ki    _<core::future::poll_fn::PollFn<F> as core::future::future::Future>::poll::h59f7f6776ace1297
  [DEL] -12.4Ki  [DEL]     -81    anon.77fe6d9fde1247be6ecad764c1853154.595.llvm.7927372867784889850
  [DEL] -13.0Ki  [DEL] -12.9Ki    agent_data_plane::state::metrics::rules::dogstatsd::get_dogstatsd_remappings::hfb3b72311acf3cad
  [DEL] -14.9Ki  [DEL] -14.8Ki    agent_data_plane::state::metrics::rules::compat::get_compat_remappings::h19651ee55cb484c9
  [DEL] -17.4Ki  [DEL]     -76    anon.77fe6d9fde1247be6ecad764c1853154.512.llvm.7927372867784889850
  +0.2% +78.3Ki  +0.2% +84.0Ki    TOTAL

@pr-commenter

pr-commenter Bot commented Aug 12, 2026

Copy link
Copy Markdown

Regression Detector (Agent Data Plane)

Run ID: a0478c55-88a2-4362-95de-f02911b98016
Baseline: ca31d5fc · Comparison: 75b8f233 · diff

Optimization Goals: ✅ No significant changes detected

Fine details of change detection per experiment (5)

Experiments configured erratic: true are tagged (ignored) and skipped when determining which experiments regressed or improved. Experiments which are detected as erratic at runtime are tagged (erratic) to flag that the run's sample dispersion was high, but their regression / improvement signal still counts.

experiment goal Δ mean % links
quality_gates_rss_idle memory ⚪ +0.19 metrics profiles logs
quality_gates_rss_dsd_low memory ⚪ +0.18 metrics profiles logs
quality_gates_rss_dsd_medium memory ⚪ -0.22 metrics profiles logs
quality_gates_rss_dsd_heavy memory ⚪ -0.53 metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory ⚪ -1.05 metrics profiles logs
Bounds Checks: ✅ Passed (5)
experiment check replicates observed links
quality_gates_rss_dsd_heavy memory_usage 10/10 ✅ 230 MiB ≤ 250 MiB metrics profiles logs
quality_gates_rss_dsd_low memory_usage 10/10 ✅ 51.1 MiB ≤ 60 MiB metrics profiles logs
quality_gates_rss_dsd_medium memory_usage 10/10 ✅ 91.8 MiB ≤ 100 MiB metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory_usage 10/10 ✅ 378 MiB ≤ 420 MiB metrics profiles logs
quality_gates_rss_idle memory_usage 10/10 ✅ 31.9 MiB ≤ 40 MiB metrics profiles logs
Explanation

A change is flagged as a regression when |Δ mean %| > 5.00% in the regressing direction for its optimization goal AND SMP marks the experiment as a regression (is_regression: true). Improvements use the matching criteria for the improving direction. Experiments configured erratic: true (tagged (ignored)) are skipped outright; experiments detected as erratic at runtime (tagged (erratic)) still count, since that flag describes sample dispersion rather than directional certainty. The Δ mean % cell is colored accordingly: 🟢 = improvement, 🔴 = regression, ⚪ = neutral. Reduction in CPU or memory is an improvement; reduction in ingress throughput is a regression.

jszwedko and others added 2 commits August 12, 2026 15:32
…dows

On Windows, ADP is managed as a subprocess by dd-procmgr, which requests a
graceful stop by sending CTRL_BREAK_EVENT (not CTRL_C_EVENT) via
GenerateConsoleCtrlEvent. The previous fallback only listened for
tokio::signal::ctrl_c(), so this request went unnoticed and ADP would sit
until dd-procmgr's stop timeout expired and force-killed it via its Job
Object, reproducing the same "no graceful shutdown" behavior as SIGTERM
on Unix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ulary

The new SIGTERM/CTRL_BREAK doc comment on wait_for_shutdown_signal uses
these terms, which Vale's spelling check doesn't recognize, failing
check-docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jszwedko
jszwedko marked this pull request as ready for review August 12, 2026 22:51
@jszwedko
jszwedko requested a review from a team as a code owner August 12, 2026 22:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75b8f233af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
use tokio::signal::unix::{signal, SignalKind};

let mut sigterm = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Install the SIGTERM listener before bootstrap work

When the service is stopped while ADP is still bootstrapping—for example, while handle_run_command is waiting for the initial Agent configuration—this future has not yet been polled, so the SIGTERM listener is not installed and the kernel terminates ADP immediately. Register the listener before the startup awaits and select the startup path against it so SIGTERM consistently enters graceful shutdown rather than only working after Agent Data Plane running. is logged.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Mmm, this is a good catch, but is a pre-existing bug. I'll open a stacked PR to address it to keep the changes isolated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Actually, thinking about this more, if the SIGTERM is received before the supervisor starts, I don't think there is a need to handle the signal for graceful shutdown. It's fine if the process just exits. cc/ @tobz for thoughts given the interaction with the supervisor.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if the SIGTERM is received before the supervisor starts I don't think there is a need to handle the signal for graceful shutdown

Sounds right to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, we only care about intercepting typical shutdown-indicating signals if we're at a point where we need to shutdown in an orderly fashion... so anything before running the "main loop" (run the root supervisor, etc) is generally fair game for abrupt shutdown.

@datadog-datadog-prod-us1-2 datadog-datadog-prod-us1-2 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

Direct and process-group SIGTERM reach graceful shutdown while preserving SIGINT behavior, and interrupted DogStatsD replay reaches cleanup. The Unix and Windows signals also match those emitted by the real process manager.

Was this helpful? React 👍 or 👎

📊 Validated against 6 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 75b8f23 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit d9a74ed into main Aug 13, 2026
106 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the jszwedko/handle-sigterm-adp branch August 13, 2026 14:10
dd-octo-sts Bot pushed a commit that referenced this pull request Aug 13, 2026
## Human Summary

Adds support for handling SIGTERM on *nix platforms, which we expect to receive when being managed by SystemD or s6, which ADP is packaged with. On Windows, we where we are managed by [procmgr](https://github.com/DataDog/datadog-agent/tree/main/pkg/procmgr) we expect to receive a CTRL_BREAK_EVENT ([ref](https://github.com/DataDog/datadog-agent/blob/eb013ebe547c7eb01868d377e266e47f420ee445/pkg/procmgr/rust/src/platform/windows.rs#L215)) so handle that instead.

For `dogstatsd replay` we currently only support Linux so we only additionally handle SIGTERM. We expect to only be run interactively, but the process may still receive a SIGTERM from out-of-band.

## AI Summary

ADP's production binaries only listened for `SIGINT` when deciding to enter graceful shutdown, but `SIGINT` is not the signal used in any of ADP's real deployment paths — systemd, container runtimes, and Kubernetes all send `SIGTERM` to request shutdown (during service stops/restarts, rollouts, evictions, node drains, and container termination). Since `SIGTERM` used its default disposition, ADP was killed outright instead of draining its topology, so buffered data could be lost and the shutdown-timeout/forceful-abort diagnostics never had a chance to fire. This wires `SIGTERM` into the same graceful-shutdown path already used for `SIGINT`, so the two behave identically. The `dogstatsd replay` debug subcommand gets the same treatment for consistency, since it also has state that should be flushed on interruption. `SIGPIPE` handling is being tracked separately and isn't part of this change.

Verified on a systemd-managed host by installing the current Datadog Agent package and swapping in a build of `agent-data-plane` with this fix:

- Before the fix: `systemctl stop datadog-agent-data-plane` (and `systemctl restart datadog-agent`, which cascades a stop via `BindsTo`) killed the process in single-digit milliseconds with `code=killed, signal=TERM` and no shutdown-path log lines at all.
- After the fix: the same `systemctl stop` produces `Received SIGTERM, shutting down...`, a clean topology drain (listeners and HTTP servers stopping), `Agent Data Plane shut down successfully.`, and the process exits with `status=0/SUCCESS`.

Closes #2319.

## Test plan

- [x] Verified under real systemd: patched `agent-data-plane` binary running as the actual `datadog-agent-data-plane.service` unit now drains and exits cleanly on `systemctl stop` / `systemctl restart datadog-agent`, versus being hard-killed before the fix.
- [x] Verified standalone: direct `SIGTERM` to the binary now triggers the same graceful-shutdown log sequence as `SIGINT`.

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com> d9a74ed
jszwedko added a commit that referenced this pull request Aug 13, 2026
…`run` and `dogstatsd replay`

Per discussion on #2322, a signal received before the supervisor starts doesn't need graceful handling, so this drops the earlier eager pre-registration approach and instead extracts the existing lazy signal-wait logic into a shared helper reused by both the `run` and `dogstatsd replay` commands, removing the duplication.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Aug 13, 2026
## Human Summary

Refactors the shutdown wait handler into a shared module that can be used where ever we need to wait to ensure we consistently rely on the same signals, cross-platform.

## AI Summary

#2322 added SIGTERM/SIGINT handling for graceful shutdown, but the same signal-waiting logic was duplicated between the `run` command and `dogstatsd replay`'s cancellation setup. An earlier version of this PR tried to close a related gap -- a signal delivered before the supervisor starts falling through to the OS's default disposition -- by registering signal handlers eagerly at process start. Per [discussion on #2322](#2322 (comment)), that gap doesn't need graceful handling: an abrupt exit before the supervisor is running is acceptable. This PR instead does a pure refactor, extracting the existing lazy signal-wait logic into a shared `saluki_app::util::wait_for_shutdown_signal` helper used by both `run` and `dogstatsd replay`, removing the duplication and making it reusable by any binary built on the saluki crates, not just agent-data-plane.

## Test plan

- [x] `cargo check --workspace`, `cargo clippy -p agent-data-plane -p saluki-app --no-deps` pass.
- No new automated tests added; this is a non-behavioral refactor of existing signal-handling code.

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com>
dd-octo-sts Bot pushed a commit that referenced this pull request Aug 13, 2026
## Human Summary

Refactors the shutdown wait handler into a shared module that can be used where ever we need to wait to ensure we consistently rely on the same signals, cross-platform.

## AI Summary

#2322 added SIGTERM/SIGINT handling for graceful shutdown, but the same signal-waiting logic was duplicated between the `run` command and `dogstatsd replay`'s cancellation setup. An earlier version of this PR tried to close a related gap -- a signal delivered before the supervisor starts falling through to the OS's default disposition -- by registering signal handlers eagerly at process start. Per [discussion on #2322](#2322 (comment)), that gap doesn't need graceful handling: an abrupt exit before the supervisor is running is acceptable. This PR instead does a pure refactor, extracting the existing lazy signal-wait logic into a shared `saluki_app::util::wait_for_shutdown_signal` helper used by both `run` and `dogstatsd replay`, removing the duplication and making it reusable by any binary built on the saluki crates, not just agent-data-plane.

## Test plan

- [x] `cargo check --workspace`, `cargo clippy -p agent-data-plane -p saluki-app --no-deps` pass.
- No new automated tests added; this is a non-behavioral refactor of existing signal-handling code.

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com> e760adf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle SIGTERM in ADP.

3 participants