Repository navigation
Darling: split query_store off the sequential per-server collection body - #2701
Conversation
…ody (#2700) A heavy query_store run (100-230+s on a leaflogix-driven batch, vs. a ~5-35s mean) was awaited inline in RunDueCollectorsAsync's sequential foreach. Since the outer launch loop will not relaunch a server's sweep body while it is still running (INV-2), one slow query_store run stalled that server's ENTIRE collection - every other due collector, not just query_store's own row - for its full duration. That pushed several servers' last_collection stale enough to false-trip the fleet's 15-minute Offline threshold with zero actual collector failures (confirmed via get_collection_health's BODY_OVERRUN diagnostic, which already names "moving or splitting that collector so it stops sharing a cycle" as the fix). query_store now runs detached, reusing the existing per-server QueryStoreServerGate (#2165) so a still-in-flight previous tick skips rather than overlapping. Its window is watermark-driven (#1960), so a run that outlives the sweep resumes correctly on its own gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| OperationCanceledException from surfacing as an unobserved task exception. */ | ||
| if (IsQueryStoreCollector(name)) | ||
| { | ||
| _ = RunDetachedQueryStoreAsync(server, runner, name, cancellationToken); |
There was a problem hiding this comment.
The detached query_store task is invisible to the #1553 shutdown drain.
Every other fire-and-track unit in this file is deliberately handed to something shutdown awaits:
server.InFlightSweepis drained viaTask.WhenAny(Task.WhenAll(inFlightSweeps), Task.Delay(s_shutdownDrainBudget, ...))(~line 1582).- The Query Store backfill loop is drained with
await backfillLoop;(~line 1604). - The baseline backfill task is drained the same way (~line 1611).
_ = RunDetachedQueryStoreAsync(...) here is the only fire-and-forget in the file that isn't assigned to anything a shutdown path awaits. Because RunDueCollectorsAsync (and therefore ProcessServerSweepAsync/InFlightSweep) now returns as soon as it fires this off instead of waiting on it, the shutdown drain's Task.WhenAll(inFlightSweeps) resolves almost immediately — it no longer represents "is this server's collection still running." ExecuteAsync can return, and the host can proceed to tear down _postgres and other shared resources, while a 100–230s query_store run is still mid-flight and still calling DarlingObservability.LogCollectionAsync(_postgres!, ...).
RunDetachedQueryStoreAsync's own catch (OperationCanceledException) only fires once the underlying command actually reacts to cancellationToken cancellation, which takes real time — time the drain budget existed to provide, and which this task no longer gets on the shutdown path (only what remains of the process/host lifetime).
Given the codebase's own established convention for every other fire-and-track task, this looks like a real gap rather than an intentional exception: consider tracking these detached tasks (e.g., a ConcurrentBag/list per server or fleet-wide, drained the same bounded way as inFlightSweeps) so shutdown still gets a chance to let an in-flight query_store run wind down before storage is torn down.
|
Reviewed the diff (single file: Main finding (posted inline): the new Things I checked and consider fine:
Lower-confidence note, not filed as a blocking finding: since No T-SQL in this diff, so the CONTRIBUTING.md T-SQL style items don't apply. |
…2717) plan_correction's own SQL has no further tuning headroom: it's already correctly seek-based post-#2687, averaging ~1 second. But on a server whose Query Store carries an abnormally large distinct-plan population (the same leaflogix-class decimal-parameter-instability signature already root-caused for query_store elsewhere in the fleet), it can spike to 20+ seconds - confirmed on one affected server: avg 1057ms, max 21351ms, 97% of that worst cycle attributable to one database (independently confirmed via query_store_plan_map: 36,232 distinct plan_ids, 28,260 distinct digests). That is the identical bimodal shape #2701 detached query_store for, and plan_correction was never included in that fix - it was still awaited inline in RunDueCollectorsAsync's sequential foreach, still able to block every other due collector on an affected server. Generalizes the fix instead of duplicating it: DetachedCollectorGate is a collector-agnostic sibling of QueryStoreServerGate, keyed by (server, collector name) so a future third collector needs only its own IsXCollector check. QueryStoreServerGate itself is untouched - it has an orthogonal second job (excluding the separate backfill loop) this type doesn't need to solve. RunDetachedQueryStoreAsync is renamed RunDetachedAsync and shared by both detached collectors rather than duplicated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Detach plan_correction from the sequential body, generalizing #2701
…ird-party name from two comments erikdarlingdata#2490's guard matches only the full hostname SHAPE, so three references that are not hostname-shaped survived erikdarlingdata#2900's pass. CHANGELOG.md's erikdarlingdata#2699 server list carried two bare short names whose slugs are not on the allowlist. Both now read allowlisted slugs that named no other server anywhere in the tree, so neither swap merges two distinct identities: epsilon-01 (epsilon occurred exactly once before this change, in the allowlist declaration itself) and dummy-01 (dummy occurred only as the adjective - "dummy SQL", "dummy 0-columns", "dummy test data"). DarlingWorker's plan_correction note and DetachedCollectorGate's erikdarlingdata#2701 paragraph used a third-party product name as a workload descriptor. Both now read "workload-class", which keeps each signature named by its own mechanism (distinct-plan-population, decimal-parameter-instability) rather than by a vendor. Every measurement in both comments is unchanged. apex is untouched everywhere else: it is this codebase's term for a blocking chain's head blocker (~150 uses), and the fleet's "apex box"/"apex replica" superlative. Only the apex-01 in that one server list was an identifier. No schema change, no migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #2700.
Problem
`RunDueCollectorsAsync` runs every due collector for a server sequentially in one `foreach`, awaited inline, inside `ProcessServerSweepAsync`. The outer launch loop will not relaunch a server's sweep body while the previous one is still running (INV-2, "one body per server").
`query_store`'s per-run duration is bimodal: mean ~5-35s, but a heavy batch runs 100-230+ seconds. Because it was awaited inline in the same sequential foreach as every other due collector (query_stats, procedure_stats, wait_stats — all 1-minute cadence), and because the whole body can't relaunch until it returns, a single heavy query_store run stalled that server's ENTIRE collection for its full duration, not just its own row. On use1 today this pushed 11 servers' `last_collection` stale enough to false-trip the fleet's 15-minute Offline threshold, with zero actual collector failures — confirmed per-server via `get_collection_health`'s existing `BODY_OVERRUN` sweep-pressure diagnostic, which already names the fix in its own generated text: "moving or splitting that collector so it stops sharing a cycle."
Fix
`query_store` is now fired detached rather than awaited inline. It reuses the existing per-server `QueryStoreServerGate` (#2165) for mutual exclusion — a still-in-flight previous tick skips rather than overlapping — and its window is watermark-driven (#1960), so a detached run that outlives the sweep resumes correctly from its own boundary on the next tick. Every other due collector in the foreach now runs on schedule regardless of how long query_store takes, and the body itself returns promptly so the outer launch loop keeps relaunching normally.
A small
RunDetachedQueryStoreAsyncwrapper exists only to keep a shutdown-timeOperationCanceledExceptionfrom surfacing as an unobserved task exception —RunOneAsync's own catch-all already contains every other fault.Test plan
dotnet build(project + full solution) clean, 0 errorsDarling.Testslocally (this Mac lacks the WindowsDesktop runtime the net10.0-windows TFM needs) — relying on CIDarlingSweepSchedulingTests.cs): the stateful scheduling body has heavy dependencies and is asserted by full-diff review rather than unit tests; the change is a minimal, surgical inline-await → detached-fire-and-forget swap reusing an already-tested gate, not new scheduling logic.🤖 Generated with Claude Code