[DBMON-6882] Add cancellation lifecycle to DatabaseCheck - #24844
[DBMON-6882] Add cancellation lifecycle to DatabaseCheck#24844eric-weaver wants to merge 2 commits into
Conversation
Lift the deferred-teardown cancel protocol out of Postgres and into the shared base class, so every DBM integration gets thread-safe unscheduling instead of each one rediscovering that closing a connection under a running check() crashes the Agent. Integrations release their own resources through the new shutdown() hook. The protocol is inert until an integration adopts it: Postgres still shadows run(), cancel() and _finalize(), and the other DBM checks override cancel() without calling super(). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 17348ab | Docs | Datadog PR Page | Give us feedback! |
evalya-impact-summaryevalya impact analysis |
Validation ReportAll 21 validations passed. Show details
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17348abd81
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return '' | ||
| self._is_running = True | ||
| try: | ||
| return super().run() |
There was a problem hiding this comment.
Avoid double-finalizing Postgres cancellation
When a cancel lands while PostgreSql.run() is in flight, this new DatabaseCheck.run() wrapper now runs inside Postgres's existing wrapper because PostgreSql.run() calls super().run(). The inner finally calls self._finalize() once, then control returns to PostgreSql.run()'s own finally, which still sees _cancelled and calls _finalize() a second time. That re-runs Postgres job shutdown/connection cleanup and can hit the existing self.log.check = None logging hazard, so Postgres cancellation regresses until the subclass is migrated or this path bypasses the new wrapper.
Useful? React with 👍 / 👎.
What does this PR do?
Moves the clean shutdown protocol that Postgres has been carrying into
DatabaseCheck, soevery DBM integration can get thread-safe cancellation without reimplementing it. The protocol
was originally added to Postgres in #23728 (deferred teardown when cancel lands mid-run) and
#23640 (dropping dangling references so the check can be reclaimed), and moved onto the shared
async job registry in #24824.
DatabaseCheckgains:run()— records whether a run is in flight, returns an empty error report without runningthe check once cancelled, and performs the deferred teardown after
check()returns.cancel()— signals the registered async jobs and records the cancellation, but does nodestructive work itself. Teardown is deferred to
_finalize(), which runs inline when thecheck is idle and in
run()otherwise._finalize()— stops the async jobs, callsshutdown(), then drops the state that keepsthe check alive. Runs at most once.
shutdown()— a no-op hook for integrations to release resources they hold for their wholelifetime, such as connections, pools and clients. Called once during teardown, after the
jobs have stopped and never while
check()is running.is_cancelled— public accessor socheck()bodies and collection loops can stop promptly.run_async_jobs()now no-ops once the check is cancelled, so a run that was in flight whenthe cancel arrived does not restart the loops
cancel_async_jobs()just stopped.The implementation carries three fixes over the Postgres original, which Postgres will pick
up when it adopts this:
_finalize()is guarded so teardown runs at most once. Double cancellation already happensin practice (Postgres's
run_one_checkhelper cancels, and theintegration_checkfixturecancels again in teardown) and currently only survives because each individual step
tolerates it.
self.log.check = Nonemoved after the last log statement.CheckLoggingAdapter.processre-reads
self.check.check_idon every call for checks whosecheck_idnever resolved, sonulling it before logging raises
AttributeErrorwhen debug logging is enabled._finalize()clears the async job registry once the jobs have stopped, breaking thecheck -> registry -> job -> check cycle at the base level rather than relying on every job's
shutdown()to null its own back-reference.Motivation
Postgres is the only DBM integration with a real shutdown protocol, and the reason is
specific: the Agent can call
cancel()from another thread whilecheck()is mid-flight, andclosing a libpq connection at that moment segfaults the Agent rather than raising. MySQL, SQL
Server and ClickHouse only signal their jobs, and ClickHouse additionally blocks on job
futures inside
cancel(), which theAgentCheck.cancel()contract warns against. Lifting theprotocol into the base class gives every integration the safe version and lets Postgres delete
its copy, which is what the
TODO: move this lock into the base classadded in #23728 asksfor.
This is additive and inert until an integration adopts it. Postgres keeps its own
run(),cancel()and_finalize(), which shadow the base versions, so its behavior is unchanged.MySQL, SQL Server and ClickHouse all override
cancel()and none callssuper().cancel(), sothe new
cancel()is unreachable for them. They do inherit the newrun(), but its onlyactive effect is one uncontended lock acquisition per run: the deferred-finalize branch
requires
_cancelled, which only the basecancel()sets.DatabaseCheckis the right home for now, but probably not forever. Nothing in the statemachine is DBM-specific — only the
shutdown_async_jobs()call is — so if we want non-DBMintegrations to adopt the same protocol,
AgentCheckis the natural long-term home. Landingit here first keeps the blast radius to the four DBM checks while the pattern proves out, and
promoting it later would be a move rather than a rewrite. The existing
TODO: move diagnosis cleanup into AgentCheck.cancel()in Postgres points the same direction.Review checklist (to be filled by reviewers)
qa/requiredif this PR needs QA validation, orqa/skip-qaif it does not. Exactly one of the two is required.backport/<branch-name>label to the PR and it will automatically open a backport PR once this one is mergedMade with Cursor