Skip to content

Suppress logging from post_fork_child/post_fork_parent - #989

Merged
StephenWakely merged 3 commits into
DataDog:masterfrom
seanmuth:seanmuth/quiet-post-fork-logging
Sep 30, 2026
Merged

StephenWakely merged 3 commits into
DataDog:masterfrom
seanmuth:seanmuth/quiet-post-fork-logging

Conversation

@seanmuth

Copy link
Copy Markdown
Contributor

What does this PR do?

Related to #834, a different specific manifestation of the same underlying class — a thread that hangs somewhere reachable from post_fork_child/post_fork_parent, the callbacks registered via os.register_at_fork().

_start_flush_thread and _start_sender_thread both call log.debug(...) on every branch they can take, and both run from post_fork_child/post_fork_parent. logging.Logger.debug() is not safe to call there: it can block acquiring a StreamHandler's own lock, and if some other thread held that lock at the exact instant of fork(), it's left permanently locked in the child — no thread survives fork to release it. Confirmed live: a process using this client hung permanently inside post_fork_child → _start_flush_thread → log.debug, even with buffering, aggregation, and the background sender all disabled — the "everything is disabled" branch still logs unconditionally today, so there was no config that avoided this path entirely.

Description of the Change

Add a quiet=False parameter to both _start_flush_thread and _start_sender_thread, gating every log.debug(...) call inside each behind if not quiet:. post_fork_child/post_fork_parent now call both with quiet=True. Every other caller is unaffected — same logging as before.

Alternate Designs

Could have just deleted the log lines outright — #817 removed a different post-fork log call for being "misleading" for a similar reason. Went with quiet instead so normal (non-fork) construction/config-change logging is untouched; only the specific atfork-reachable path goes quiet.

Possible Drawbacks

Anyone relying on these specific debug-level log lines firing after a fork loses that signal. These aren't documented as a stable interface and are debug-level only, so this seems low-risk.

Verification Process

  • Added test_post_fork_does_not_log (parametrized across the same buffering/sender config combinations as the existing fork tests) asserting log.debug is never called during post_fork_child/post_fork_parent. Confirmed it fails against the pre-fix code with the exact two log calls this PR removes from that path, and passes with the fix.
  • Full existing suite: tests/unit/dogstatsd/test_statsd.py (134 passed, 1 skipped, unrelated), tests/integration/dogstatsd/test_statsd_fork.py (16 passed, including the new test), tests/integration/dogstatsd/test_statsd_sender.py (79 passed) — all green, no regressions.
  • mypy datadog/dogstatsd/base.py clean.

Additional Notes

Generated with AI assistance (Claude Sonnet 5); I reviewed and tested the change before opening this PR.

logging.Logger.debug() is not safe to call from an os.register_at_fork()
callback: it can block acquiring a StreamHandler's own lock, and that lock
is left permanently locked in the child if some other thread held it at
the exact instant of fork (no thread survives fork to release it there).
_start_flush_thread and _start_sender_thread both log on every branch they
can take, and both run from post_fork_child/post_fork_parent, so every
invocation from that path was exposed regardless of config.

Confirmed live: a process using this client hung permanently inside
post_fork_child -> _start_flush_thread -> log.debug, even with buffering,
aggregation, and the background sender all disabled -- the "everything is
disabled" branch still logs unconditionally today, so there was no
combination of settings that avoided this path entirely.

Related to DataDog#834, a similar report against a different call site
(close_socket) reachable from the same post_fork hooks.

Signed-off-by: Stephen Wakely <stephen.wakely@datadoghq.com>
nogates
nogates previously approved these changes Sep 24, 2026

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The post-fork socket-close path can still log and deadlock; related fork-hook documentation also needs correction.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

This PR suppresses debug logging during DogStatsD fork callbacks to help prevent post-fork deadlocks.

Changes:

  • Adds quiet startup controls for flush and sender threads.
  • Uses quiet mode during fork restoration.
  • Adds parametrized fork logging tests.
File Summary
tests/​integration/​dogstatsd/​test_statsd_fork.py Verifies fork callbacks suppress debug logging.
datadog/​dogstatsd/​base.py Implements quiet thread startup during fork handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread datadog/dogstatsd/base.py

@StephenWakely StephenWakely 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.

Can you fix the copilot suggestion as well. We'll need to pass quiet to close_socket as well..

close_socket() still logged at error level on a socket.close() OSError
even when called from post_fork_child()'s os.register_at_fork(after_in_child=...)
handler, the same fork-unsafe logging path already fixed for
_start_flush_thread/_start_sender_thread. Add a quiet flag, gate the two
log.error calls behind it, and pass quiet=True only from post_fork_child.

Signed-off-by: Stephen Wakely <stephen.wakely@datadoghq.com>
@seanmuth

Copy link
Copy Markdown
Contributor Author

@StephenWakely done, also had my claude agent do another pass at the atfork handler callpaths to identify any other potentially blocking/fork-unsafe calls and it didn't flag anything else.

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

No unresolved blocking issues were identified.

Review effort: Lite
Findings: None

Resolved since last review (1)

@seanmuth

Copy link
Copy Markdown
Contributor Author

@StephenWakely not sure what the merge/release workflow looks like for DD, is there anything else you need from me on this PR?

@StephenWakely

Copy link
Copy Markdown
Contributor

@StephenWakely not sure what the merge/release workflow looks like for DD, is there anything else you need from me on this PR?

@seanmuth We should be good from here, thanks. Just a little dance with CI to get it merged and then I'll kick off a new release.

@StephenWakely StephenWakely added the ci/integrations Run integration tests label Sep 30, 2026
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Pipelines

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

Run Integration Tests | integration_tests

View more details · View in GitHub Actions

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 3332069 | Docs | View more details | Give us feedback!

@StephenWakely StephenWakely reopened this Sep 30, 2026
@StephenWakely
StephenWakely merged commit a844964 into DataDog:master Sep 30, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/Fixed Fixed features results into a bug fix version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants