Skip to content

Queries-tab web reads stop their store query when the request is abandoned (#4203) - #4350

Merged
erikdarlingdata merged 7 commits into
devfrom
fix/4203-web-read-cancellation-2
Sep 25, 2026
Merged

erikdarlingdata merged 7 commits into
devfrom
fix/4203-web-read-cancellation-2

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #4203. STACKED on #4347: merge #4347 first.

Why

The web viewer's Queries tab issues /api/read/* calls that run a store query on Darling's behalf. When the
browser abandons the request (tab closed, navigated away, filter changed before the first answer came back),
the store query used to keep running to completion with nothing left to read the result — wasted work on the
monitoring store for every one of these reads. C1's PR #4347 built the pattern and the pin
(WebReadCancellationPinTests) on the Configuration tab's tools; this lane converts 8 of the 12 Queries-tab
tools in its slice to the same pattern.

What changes

Each of these 8 tools now takes CancellationToken cancellationToken = default as its last parameter, threads
it to DarlingServerResolver.ResolveOrErrorAsync and down to every store call below it (including the
concurrent CPU-attribution reads the two top-by-cpu tools run via Task.WhenAll), and its catch-all is
catch (Exception ex) when (ex is not OperationCanceledException) so cancellation is not turned into an error
payload. Each tool's BuildReadDispatch entry in DarlingWebEndpoints.cs now passes c.RequestAborted, and
each name is removed from CancellationAllowlist:

  • get_active_queries
  • get_plan_corrections
  • get_query_heatmap
  • get_query_store_clutter
  • get_top_queries_by_cpu
  • get_top_procedures_by_cpu
  • get_query_store_regressions
  • get_long_query_completions

CancellationAllowlist now holds 114 names (down from 122 at this branch's base). Four tools from this lane's
original 12-tool slice are not converted and stay in the allowlist for a follow-up lane:
get_query_duration_trend, get_procedure_duration_trend, get_query_store_duration_trend, get_query_trend
(all in DarlingMcpTrendTools.cs). get_query_store_top also stays, per the brief: PR #4341 is rewriting its
reader, and it converts after that merges.

Two test fixes came from merging origin/fix/4203-web-read-cancellation partway through this lane (commit
2cdce525, from the coordinator's correction after #4347's CI found three pin classes that break when a tool
gains the cancellationToken argument): PgTargetMcpSurfaceTests, McpServiceParameterDiSeatCensusTests and
EngineCapabilityMissTests (EngineCapabilityReadWiringTests) all came in via that merge, not from this lane.

Three more test fixes are this lane's own, all the same class of defect (a pin that matches a call's exact
source text, which the new cancellationToken argument changes) surfacing in tools this lane converted:

  • QueryStoreClutterTests.TheToolBody_ReadsTheFleetOverTheSameAnchoredWindow pinned the fleet-read call text
    literally; updated to expect cancellationToken in it. The window-anchoring behavior the test actually
    checks (the fleet reads use the same requestedStart/now as the main reads) is unchanged.
  • DarlingWebEndpointsTests.ReadEndpoints_ActiveQueries_KeepsTheTwoThousandCharacterWebPreview pinned the whole
    get_active_queries dispatch line; updated to expect the trailing c.RequestAborted. The 2000-character web
    preview budget the test actually checks is unchanged.
  • EngineCapabilityReadWiringTests' WiringCall regex (already widened by the merge above to allow a
    trailing cancellationToken argument) still mis-parsed get_query_store_clutter's
    NotCollectedStatusAsync(..., DarlingQueryStoreClutterReader.CollectorName, cancellationToken) call: its
    collector argument is a type-qualified const the regex's local consts lookup can't resolve (that lookup
    only sees private const string X = "..."; declared in the same file), so before this fix the regex
    backtracked past the dot and captured the literal word cancellationToken as if it were the collector name,
    which then failed the "is a real CollectorCatalog name" assertion. This exact call, unchanged apart from the
    new trailing argument, was simply invisible to this census before (the regex found no match at all for a
    dotted const with no argument after it) — a latent gap, not a behavior change. Widened the regex's
    bare-identifier branch with a negative lookahead so it never captures cancellationToken/ct/
    CancellationToken.None as the primary collector-name group, which restores the prior silent-skip for this
    one dotted-const call rather than asserting a wrong name, while every properly-resolvable call (everything
    else in both SKUs) is still fully validated.

CHANGELOG entry

SECTION: Fixed
ENTRY:

Test plan

  • WebReadCancellationPinTests (its dispatch theory covers every tool outside the allowlist, including all 8 this lane converted)
  • Each converted tool's own non-live test classes: DarlingMcpPlanCorrectionToolsTests, PlanCorrectionsWebDefaultTests, DarlingQueryHeatmapSurfaceAndSqlTests, QueryStoreClutterTests, QueryStoreClutterViewerSurfacesTests, ViewerQueryTrendsSqlTests, ViewerQueryHeatmapSqlTests, ViewerQuerySnapshotsSqlTests, ViewerActiveQueriesDisplayTests, McpToolGuideHeadsSqlCoreActiveQueriesTests, DarlingMcpDataToolsTests, DarlingQueryStoreRegressionsTests, McpToolGuideHeads.LongQuery
  • McpPayloadContractCensusTests, PgTargetMcpSurfaceTests, McpServiceParameterDiSeatCensusTests, EngineCapabilityReadWiringTests (the three the coordinator flagged, plus the census)
  • *WebEndpoint*, *ReadDispatch*, StorageCommandTimeoutTests, AlertReadFailureSurfaceTests, DocCommentHygiene*
  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s) (checked after each batch)
  • git merge origin/dev — not run. This lane's watchdog fired past 200k tokens partway through wiring up the second batch of 4 tools; per the token guardrails ("past 200k... don't start another build-test cycle"), I committed and pushed what was done and stopped rather than opening a merge that could need its own build-fix cycle.
  • Full Darling suite — not run, same reason. All above were targeted -class runs against the MTP executable (Darling.Tests.exe), no rig started (per the brief, none needed for this slice).

No PostgreSQL rig was started (per the brief: the pin uses a closed local port).

What's left in #4203

Coordinator: please double-check

  • That git merge origin/dev and the full suite run clean before this leaves draft (neither ran in this lane).
  • The EngineCapabilityReadWiringTests regex widening (an exemption on cancellationToken/ct/
    CancellationToken.None as a bare collector-name match) — this is shared test infrastructure, not scoped to
    one tool, so worth a second look even though it's the same class of fix as the coordinator's own merged
    commit.
  • The CHANGELOG entry's PR number placeholder (#4350) — I don't know this PR's actual number until gh pr create
    returns it; if it differs, the entry's [#...] label and REF line need the real number.

C2b (census fix)

CI run 36187700714 failed two tests on both Darling jobs:
RuntimePreconditionReadWiringTests.EveryWiredRead_NamesARealCollector and
RuntimePreconditionReadWiringTests.BothSkus_WireTheSameSharedReads.

Cause. RuntimePreconditionMissTests.cs's PreconditionCall regex took the LAST argument of
RuntimePrecondition.StatusAsync(...) as the collector name. This slice passes the request's
cancellationToken as a trailing argument on get_long_query_completions's and
get_query_store_clutter_report's calls, so the census read "cancellationToken" as the collector name
instead of the real one.

Fix. PreconditionCall now accepts an optional trailing token argument after the real name, positional
(cancellationToken) or named (cancellationToken: ct), and still requires the real name to come before it.
The bare-identifier name branch keeps the negative lookahead that already protects
EngineCapabilityMissTests's sibling regex (WiringCall) for NotCollectedStatusAsync, which this same PR
already carries and which CI did not flag, so this brings RuntimePreconditionMissTests.cs in line with the
same fix rather than inventing a new shape. Added a [Theory] self-test,
PreconditionCall_ReadsTheNameAcrossTrailingTokenShapes, that runs the regex directly against
StatusAsync(a, b, "x"), StatusAsync(a, b, "x", cancellationToken),
StatusAsync(a, b, "x", cancellationToken: ct) and StatusAsync(a, b, name, cancellationToken), asserting
each reads the name, not the token.

I grepped RuntimePreconditionMissTests.cs and every *Census*.cs/*Wiring*.cs file under
Darling/Darling.Tests for other regexes assuming the collector name is the last argument before ). The
other two RuntimePrecondition-related regexes in the same file (CapabilityCall, QueryStoreCall) only
check that a call exists — they never capture a trailing argument as a name — and EngineCapabilityMissTests's
WiringCall already carries the equivalent fix. McpPayloadContractCensusTests's SharedPassThroughProducer
matches only up through the call's opening paren, never a captured argument. PgTargetMcpSurfaceTests.cs and
McpServiceParameterDiSeatCensusTests.cs build no ad hoc regexes over these calls at all. No other instance of
the defect found.

Tests. Built Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s).

  • CollectorRuntimePreconditionTests + RuntimePreconditionReadWiringTests + RuntimePreconditionMissLivePostgresTests (every class in RuntimePreconditionMissTests.cs): 43 total, 0 failed, 3 skipped (live-Postgres, no rig started per this lane's brief).
  • PgTargetMcpSurfaceTests + McpServiceParameterDiSeatCensusTests + EngineCapabilityReadWiringTests + McpPayloadContractCensusTests: 77 total, 0 failed, 0 skipped.
  • Did not run the full suite (out of this lane's scope; CI already ran all 14,301 tests on the prior head and found only these two failures).

pm-pr lane report

Resolved this PR's conflict with dev after #4347 (the first #4203 slice) squash-merged, since dev now held #4347's changes as one commit while this branch still carried the original commits.

  • New head: 01ae1fc2624a7bd94dc9dc6e1034347aefceddd5 (merge of origin/dev into fix/4203-web-read-cancellation-2, fast-forward push, no force).
  • Conflicted files and resolution:
  • Allowlist check (git diff origin/dev -- Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs | grep -E '^[-+]\s*"'): exactly 8 removals, no additions — get_active_queries, get_plan_corrections, get_query_heatmap, get_query_store_regressions, get_query_store_clutter, get_long_query_completions, get_top_procedures_by_cpu, get_top_queries_by_cpu. Matches this PR's own slice; the ratchet only shrank.
  • Ran locally: dotnet build Darling/Darling.Tests/Darling.Tests.csproj -p:EnableWindowsTargeting=true → Build succeeded, 0 Warning(s), 0 Error(s). Could not run WebReadCancellationPinTests, PgTargetMcpSurfaceTests, McpServiceParameterDiSeatCensusTests, EngineCapabilityReadWiringTests, or RuntimePreconditionReadWiringTests on this Mac host — the built test binary requires Microsoft.WindowsDesktop.App 10.0.0, which isn't installed on this machine (only Microsoft.NETCore.App and Microsoft.AspNetCore.App). CI decides on those.
  • No product-code changes; resolution only.

erikdarlingdata and others added 6 commits September 25, 2026 16:05
… slice) (#4203)

Threads CancellationToken from /api/read's RequestAborted down to Npgsql for
audit_config and the rest of the web viewer's Configuration tab, plus the
shared DarlingServerResolver every tool resolves a server name through. Adds
WebReadCancellationPinTests, a code-driven ratchet (dispatch table + tool
method reflection) with an allowlist for the tools later lanes still need to
convert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
PgTargetMcpSurfaceTests: the audit_config anchor now matches
PostgresTargetFactsAsync's new cancellationToken argument.

McpServiceParameterDiSeatCensusTests: exclude CancellationToken from the
DI-seat census. The MCP SDK binds it to the call's own cancellation, the
same way it binds IMcpServer; it is never a DI service and never a client
argument.

EngineCapabilityMissTests (EngineCapabilityReadWiringTests): WiringCall now
allows the collector argument to be followed by the call's cancellation
token, so the five NotCollectedStatusAsync calls this PR touched still
report their real collector name instead of the literal token identifier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
get_active_queries, get_plan_corrections, get_query_heatmap and
get_query_store_clutter now take a CancellationToken and pass it down to
every store call, so an abandoned web request stops its query instead of
running to completion. Removes the four from CancellationAllowlist.

Also repins two tests the new call shapes broke: QueryStoreClutterTests'
anchored-window pin (now expects the token in the fleet-read call text) and
DarlingWebEndpointsTests' active-queries preview pin (now expects
c.RequestAborted). Widens EngineCapabilityReadWiringTests' WiringCall regex
so a NotCollectedStatusAsync call whose collector argument is a
type-qualified const (get_query_store_clutter's
DarlingQueryStoreClutterReader.CollectorName, which its local consts lookup
cannot resolve) doesn't backtrack past the dot and mis-capture the trailing
cancellationToken argument as the collector name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
get_top_queries_by_cpu, get_top_procedures_by_cpu, get_query_store_regressions
and get_long_query_completions now take a CancellationToken and pass it down
to every store call (including the concurrent CPU-attribution reads in the
two top-by-cpu tools), so an abandoned web request stops its query instead
of running to completion. Removes the four from CancellationAllowlist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
RuntimePreconditionReadWiringTests took the LAST argument of
RuntimePrecondition.StatusAsync(...) as the collector name. This PR
passes the request's cancellation token as a trailing argument, so
the census read "cancellationToken" as the collector name instead of
the real one, failing EveryWiredRead_NamesARealCollector and
BothSkus_WireTheSameSharedReads.

PreconditionCall now accepts an optional trailing token argument,
positional or named, after the real name, mirroring the same fix
EngineCapabilityMissTests already carries for NotCollectedStatusAsync.
A new inline-data self-test pins the four call shapes directly against
the regex.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…ellation-2

# Conflicts:
#	Darling/Darling.Tests/EngineCapabilityMissTests.cs
#	Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 23:20
@erikdarlingdata
erikdarlingdata merged commit 9f6c265 into dev Sep 25, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4203-web-read-cancellation-2 branch September 25, 2026 23:20
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.

1 participant