Give every Darling storage command an explicit deadline (Part of #2874) - #2888
Conversation
Seventy of the project's 124 command sites inherited Npgsql's undocumented 30s default. Five budget regimes, each bound by its own deliberate constant: the MCP read surface at a new 30s StorageCommandDeadlines.McpReadSeconds (measured 97-685ms verified, 35.2s harder superset), migration scaffolding at the existing 300s (closing a startup race on pg_advisory_lock), TimescaleDB setup/catalog reads at their existing constants, the self-test at its 8s per-layer budget, plan-dim maintenance at a hoisted 300s, and the slice repair's rail-lift at its session's 900s. Directory-scoped pin over both construction shapes, span walker terminating at depth <= 0 - fixture-pinned against the nested-using shape that made two depth-clamping scanners each miss a real site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
| /// <summary> | ||
| /// One bound for every non-VACUUM statement in this maintenance pass (#2874). Same value the | ||
| /// estimate already chose deliberately; hoisted so the survey, fetch and update loops cannot | ||
| /// silently fall back to Npgsql's inherited 30 s default. VACUUM FULL keeps its explicit | ||
| /// <c>CommandTimeout = 0</c> — unlimited is the deliberate choice there, not an omission. | ||
| /// </summary> | ||
| private const int MaintenanceStatementTimeoutSeconds = 300; |
There was a problem hiding this comment.
This new doc comment (lines 121-126) is inserted directly after the existing doc comment for EstimateCompactedSql (the "fast estimate... sampled rows" block just above), with no declaration in between. In C#, adjacent /// blocks merge into a single doc comment for the next declaration — so both <summary> blocks now attach to MaintenanceStatementTimeoutSeconds here, and EstimateCompactedSql (a public const string, a few lines below) loses its documentation entirely. Since CONTRIBUTING.md calls for XML docs on public APIs and this codebase leans on comments to carry the "why", worth moving this summary below the EstimateCompactedSql declaration so each member keeps its own doc.
| } | ||
|
|
||
| using (var acquireLock = new NpgsqlCommand("SELECT pg_advisory_lock($1)", connection)) | ||
| using (var acquireLock = new NpgsqlCommand("SELECT pg_advisory_lock($1)", connection) { CommandTimeout = MigrationCommandTimeoutSeconds }) |
There was a problem hiding this comment.
This closes the specific case described in the PR body (a sibling migrating for >30s), but the bound only matches a single statement's budget, not the full session's. The advisory lock is acquired once, and MigrateLockedAsync then applies every pending rung sequentially in one session, each individually allowed up to MigrationCommandTimeoutSeconds (300s). If a store is several rungs behind and more than one of those rungs is non-trivial (today only V23's migrate_data conversion is called out as potentially slow, but nothing prevents a future rung from also being data-moving), the total hold time on this lock can exceed 300s even though every individual statement stayed within its own budget — and a sibling waiting here with the same 300s CommandTimeout would still die mid-wait, just less often than before. Worth considering whether the lock-wait timeout should scale with the number of pending rungs (or just be given deliberately more headroom than a single statement's bound) rather than mirroring MigrationCommandTimeoutSeconds directly.
|
|
||
| /// <summary> | ||
| /// Every command in <c>PerformanceMonitor.Darling.Storage</c> must carry an EXPLICIT deadline | ||
| /// (#2874). Sixty-nine of the project's 124 command sites ran on Npgsql's undocumented 30 s |
There was a problem hiding this comment.
This doc comment (and the two others at lines 47 and 122) say "Sixty-nine"/"69" sites, but the CHANGELOG entry and the PR title/description both say "seventy"/"70" untimed sites. Since this codebase treats these comments as load-bearing measurements rather than decoration (per CONTRIBUTING.md's "comments carry measurements... issue numbers"), worth reconciling which count is actually correct before merge so the test file and the changelog don't quietly disagree about the census this PR is closing out.
|
Reviewed this as a mechanical, well-scoped sweep giving every Darling storage command an explicit Three inline notes, none blocking but worth a look before merge:
Parity check: this only touches Darling's Npgsql layer. Confirmed Lite doesn't need a counterpart change here — No security or T-SQL style concerns — this PR doesn't touch any T-SQL, and no new SQL injection surface (all commands remain parameterized; the one string-interpolated |
- The 4th scanner fixture asserted False but its own shape put the deadline reachable within the 2-statement span (deadline was a sibling member, not a neighbour past a closing brace). Rebuilt the fixture so the two statements after the untimed construction are both inside its using-block — a correct depth<=0 walk sees no deadline and reports the site, which is the real QueryStoreSliceRepair shape. - MaintenanceStatementTimeoutSeconds' doc block landed between EstimateCompactedSql's summary and its member, stacking two summaries on one member (DocCommentHygieneTests). Moved the const above the estimate's summary so each block documents its own member. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
58cd0a5 to
1593e4f
Compare
…t's (Part of #2874) The pg_advisory_lock acquire inherited MigrationCommandTimeoutSeconds, which bounds ONE statement - but the lock is taken once and held while MigrateLockedAsync applies EVERY pending rung in the same session, so what a sibling waits for there is a whole multi-rung upgrade. At 300 s a store several rungs behind could hold it past the waiter while every individual statement stayed inside its own limit. Copying a bound across two different quantities is exactly the thing this repo's threshold rule forbids, so the wait now has its own name and its own derivation. MigrationLockWaitTimeoutSeconds = 5 x MigrationCommandTimeoutSeconds. Floored on what the lock can legitimately be held for, counted from the ladder rather than assumed: exactly FOUR of the 108 rungs touch pre-existing data - V22's index across every chunk of the populated index_object_stats hypertable, V23's create_hypertable migrate_data, V39's two partial indexes over query_stats and procedure_stats, and V104's index on pg_deadlocks. The other 104 create the table they then index, or are metadata-only ADD COLUMN / SET SCHEMA / DROP NOT NULL / view refreshes; the whole 108-rung ladder measured 0.301 s end to end on a fresh PostgreSQL 17.11 / TimescaleDB 2.29.2 store. Each of the four is one command already capped at the statement bound, so four multiples is the floor and the fifth is margin for the next data-moving rung. Seeded locally for scale, warm: V22 1.29 s over 907 MB / 90 chunks, V23 9.37 s over a 608 MB heap, V39 1.44 s over 1.26 GB - against 2.72 GB, 0.69 GB and 24.5 GB in those same tables on the live 42-server store. Capped from above by nothing, which is the finding rather than an omission. MigrateAsync has exactly one production caller, DarlingWorker.RunCollectionLoopAsync, on the plain stopping token: no CancelAfter, no configured HostOptions.StartupTimeout, no health check or readiness probe, no container HEALTHCHECK, no orchestrator manifest, and the installers' WaitForStatus('Running') returns before the first migration statement runs because the worker is a BackgroundService. So the value comes from the asymmetry: a waiter that dies mid-wait hits LogCritical and returns out of the collection loop, collecting nothing until an operator restarts it, while one that waits longer only delays its own first cycle - its MCP and web surfaces already started independently. Long but finite, so a wedged holder still yields a readable deadline rather than the silent hang #2874 exists to remove. Proven wired rather than merely declared. With the statement bound temporarily at 3 s and the lock wait at 12 s, a contended MigrateAsync against a held advisory lock failed at 12.02 s; reverting only the acquire to the old constant moved that to 3.02 s. Both surfaced as "Exception while reading from stream" with an inner TimeoutException - the misdiagnosis this issue exists to remove. 1500 was read back out of the rebuilt assembly by reflection, not trusted from source. The CHANGELOG said this change was "closing" the mid-lock-wait startup failure. It is not: a fifth data-moving rung, or one rung needing longer than its own bound, still outlasts the wait. Reworded to narrows, with the derivation stated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… reports (Part of #2874) The pin's doc comment said sixty-nine of 124 sites were untimed and that 49 of those 69 were the .CreateCommand( shape, while the CHANGELOG and PR body said seventy. One had to be wrong; seventy is right. Re-derived empirically rather than re-counted by eye. The pin's own regexes and its depth <= 0 statement-span walker were lifted verbatim into a net10.0 harness and run over the merge-base tree (8e046fa), which is the only tree the claim describes - post-fix the count is zero by construction: 124 sites total (new NpgsqlCommand( 74, .CreateCommand( 50) 70 untimed (new NpgsqlCommand( 20, .CreateCommand( 50) So 50 of 70, not 49 of 69. Every one of the project's 50 .CreateCommand( sites was untimed, which is why that bucket cannot be 49 under any scanner variant. The 69 is traceable: it is an intermediate figure from a partly-corrected scan. Running the same walker with its depth counter clamped at zero - the bug d13005f and 1593e4f describe - reports 68, missing exactly two sites, PgMigrations.cs:3981 (the version read) and QueryStoreSliceRepair.cs:321 (the rail-lift). a4bd263's comment named the first, 1593e4f's named the second; the total was written when only one of the two had been found and never refreshed. The 49 then follows by subtracting the correct new NpgsqlCommand( bucket (20) from the stale total. 367 of 371 is left alone: it is verified, and it is Darling-wide rather than project-scoped. The .CreateCommand( shape across the three production Darling projects is .Service 131, .Viewer 190, .Storage 50 = 371 total, of which 127 + 190 + 50 = 367 were untimed. The 124 is also confirmed. The harness additionally reproduces the issue's original new-NpgsqlCommand-only figures for .Storage (20) and .Viewer (2) exactly, which is the check that says it is counting the same way the census did. Doc comment only - no assertion, regex or walker changed, and the pin still reports 0 untimed over the current tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
| /// The explicit command deadline for the store readers that serve the MCP surface (#2874) — the | ||
| /// <c>DarlingPg*Reader</c> family, <c>DarlingPgTrendReader</c> and <c>QueryStoreTrendRouting</c>. | ||
| /// | ||
| /// <para>Before this, all forty-nine of those commands set no <c>CommandTimeout</c> and so inherited |
There was a problem hiding this comment.
Minor count mismatch: this says "all forty-nine of those commands set no CommandTimeout," but the actual sweep here touches 48 sites, not 49 — grep -c "CommandTimeout = StorageCommandDeadlines.McpReadSeconds" across the diff returns 48, matching the PR description's own table ("MCP read surface … | 48"). Worth fixing since this codebase's own convention (and this very PR's body) treats exact counts in comments as load-bearing — a stale/off-by-one count here is the same failure mode the PR spends several paragraphs correcting for the census numbers.
|
Reviewed. Well-scoped, well-documented sweep -- every new CommandTimeout assignment follows the codebase's existing per-regime pattern. Independent spot-check: a raw scan for command constructions across Darling/PerformanceMonitor.Darling.Storage/*.cs returns exactly 124 sites, matching the PR's claimed total, and 48 sites are wired to StorageCommandDeadlines.McpReadSeconds, matching the table in the description. Notes:
|
StorageCommandDeadlines' doc comment said "all forty-nine of those commands set no CommandTimeout". The family is 48: 46 DarlingPg*Reader sites (DarlingPgTrendReader carrying 9 of them) plus QueryStoreTrendRouting's 2. Confirmed three independent ways - the merge-base census bucket for DarlingPg*Reader + QueryStoreTrendRouting, a grep of the applied sites in the merged tree (grep -c "CommandTimeout = StorageCommandDeadlines.McpReadSeconds" = 48 across 29 files), and the regime table in the PR body, which was itself corrected from 49 to 48 earlier in this branch and now sums to 70. The same off-by-one and the same cause as the 69/49 figures already corrected here: 49 came from the stale untimed total, and spelling it out as a word is why the earlier numeric sweep for the census fix missed it. Swept the whole PR for spelled-out counts this time as well as digits; this was the only remaining one. Doc comment only - no constant, assertion or site changed, and the pin still reports 0 untimed over the project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed. This is a clean, well-scoped change — verified the following, no issues found:
No T-SQL changed (pure C#), so the T-SQL style conventions don't apply here. Didn't spot any correctness, security, or performance regressions in the diff. |
erikdarlingdata#2894's option 3. erikdarlingdata#2888 sized the advisory-lock ACQUIRE budget as 5 * MigrationCommandTimeoutSeconds from a census of data-moving rungs, and nothing in the build noticed when that census went stale: the lock is taken ONCE and held while every pending rung applies in the same session, so a fifth data-moving rung silently spends the one multiple of margin, and the waiter's death takes the instance out of the collection loop with no retry. MigrationDataMovingRungCensusPins scans every rung's SHIPPED SQL - the real PgMigrations.Scripts strings, so the ten generator-assembled rungs are covered rather than transcribed - for fourteen data-moving shapes, and requires the matching rung set to equal a declared census carrying each rung's reason and whether it sets the floor. A second assertion ties the constant's multiple to the floor-setting count, read out of PgMigrations.cs so the derived form itself is pinned rather than the compiled int. The CREATE INDEX discrimination is the design: 130 of the ladder's 134 CREATE INDEX statements index a table the SAME rung creates, which is free, so every targeted shape resolves its table and is dropped when that rung created it. Identity is the bare table name, because V1 creates the collector tables unqualified in public and V8 moves them to collect (37 bare names are created under both spellings). Re-derived census on dev at schema 110: 109 rungs (V1-V110, V45 permanently absent), SIX rungs touch data an earlier rung created, not four. The four the constant rests on stand (V22, V23, V39, V104, all over populated hypertables); V62's CHECK constraint on the single-row config.config_service and V77's two watermark DELETEs on collect.collector_state are real DML on pre-existing rows and are now declared at zero cost rather than absent from the register. V110 is not among them - ten nullable integer ADD COLUMNs with no DEFAULT plus a CREATE OR REPLACE VIEW is catalog-only. Two corrections to the constant's own doc comment fall out: the ladder is 109 rungs rather than 108, and MigrationCommandTimeoutSeconds is a per-RUNG bound rather than a per-statement one, since the applier wraps each rung's whole SQL in one NpgsqlCommand - so V39's two indexes cost one multiple rather than two and the four-plus-one derivation holds. Neither of erikdarlingdata#2894's other two shapes is built. The residual is narrowed, not closed: a rung that genuinely needs longer than its own 300 s bound still outlasts the wait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine sites across four regimes inherited Npgsql's undocumented 30 s default. Two of them sit under an enclosing budget that never bounded them, which is the finding worth keeping. BackfillSliceDeadline is 300 s over a Query Store backfill slice, but AbandonableStep ABANDONS rather than cancels: it races the work against a Task.Delay and returns without signalling anything. Measured against a live store rather than reasoned about -- a 20 s statement with CommandTimeout = 0 under a 3 s deadline returned Abandoned at 3.0 s while pg_stat_activity still reported that backend state='active', and the same statement with CommandTimeout = 5 faulted at 5.0 s with the backend gone. The command deadline is the only instrument. With the shipped shape -- no deadline, 300 s budget -- a 40 s statement faulted at 30.0 s with "Exception while reading from stream": the 300 s was never reached. Both backfill reads are unbounded across retention on query_store_stats and both are in erikdarlingdata#2795's production cancellation census -- the candidate-database scan's own form 631 times in one day, the MIN ceiling read's shape-twin MAX 2,092 times at 40,743-50,560 ms cold on 62.5 GB / 19 chunks. The candidate scan's collection_time predicate is inert (a 7-day window against 4-day raw retention), and the MIN cannot be bounded the way erikdarlingdata#2344 and erikdarlingdata#2795 bounded their MAX siblings because it exists to find the OLDEST stored row. So 120 s: above the default rather than below it, 2.4x the measured cold worst, strictly under the abandon threshold so the statement dies before the loop walks away from it. The command plane's 5-minute claim lease turned out to be the wrong instrument to derive against, and that was measured too. Replaying the shipped claim, reaper and report SQL: the reaper marks a still-running command terminal failed and a second worker then finds nothing to claim, because it deliberately does not re-queue to pending -- so there is no double-execution to guard. What it does is misreport, and ReportCommandSql has no status guard, so a late report flips the row failed -> succeeded after a viewer has read the failure. A per-command deadline narrows that rather than closing it, following erikdarlingdata#2888's lock wait and erikdarlingdata#2901's own command plane. 5 s, floored on the 3.9 ms cold erikdarlingdata#2901 measured for the identical single-row config_command shape and capped by the plane's own 5 s poll cadence, on a single-threaded loop whose reaper runs first every tick. The actual-plan resolve is its own regime because its floor exceeds the plane's whole value: none of its three resolvers predicates on collection_time, so each is a LIMIT 1 over every chunk in retention. 45 s is what the command's budget has left after the 120 s re-execution plus the claim and report inside the viewer's 180 s poll. ReadConfigVersionAsync is claimed here rather than with the startup group on this project's own rule -- which token does the site receive, and what re-runs it. It takes the plain stopping token and the sweep re-runs it every 15 s forever, while the other twelve sites in that file run once per process start. 3 s, tighter than the plane despite the same millisecond floor: the read is awaited on the serial sweep thread ahead of every server's launch, so an overrun is fleet-wide collection latency while a failure costs one tick of config-change delay. Pinned by (member, constant) PAIRS rather than by "has a deadline", so a site cross-wired to a neighbouring regime's number fails. The value bands read their premises out of source where the field is private, so raising the sweep interval fails the beacon's band instead of silently invalidating it. One scanner change, found by a fixture: every pin in this sweep matches its value regex over StatementSpanFrom's RAW span, so a comment inside the two-statement window that spells the deadline satisfies the regex and an untimed site reads as clean. Controlled both ways -- the same source reads TIMED over the raw span and as an offender over the stripped one. This pin takes the span from stripped text.
Part of #2874 — group 2 of the untimed-command sweep: PerformanceMonitor.Darling.Storage, 70 sites.
The census, re-derived
The issue's original count said 20; the corrected census said 67. Sweeping both construction shapes on current
devwith statement-span semantics found 124 sites, 70 untimed — and the last of those 70 was found only by the C# scanner: two independent Python scanners each missed one real site by leaking a statement span across a closing brace (details below).Five budget regimes, deliberate bounds — not one blanket value
DarlingPg*Reader, trend reads, QS trend routing)StorageCommandDeadlines.McpReadSeconds = 30PgMigrations)MigrationCommandTimeoutSeconds(300)PgMigrations)MigrationLockWaitTimeoutSeconds=5 x MigrationCommandTimeoutSeconds(1500)SetupTimeoutSeconds(300)JobCatalogReadTimeoutSeconds(30)min(collection_time)runs 685 ms cold on the largest store's 23 GB hypertable — chunk exclusion answers it, so this is NOT the #2795 shape, and 30s carries ~44x headroom. Measured rather than assumed because this read gates retention arming (#1680): a wrong bound here would hold retention silently.(int)NetworkProbeTimeout.TotalSeconds= 8)MaintenanceStatementTimeoutSeconds = 300CommandTimeout = 0— unlimited is the deliberate choice there, not an omission.SliceStatementTimeoutSeconds(900)The new value, bounded on both sides
McpReadSeconds = 30:minclass on the largest store). The shipped trend reads are all per-server and time-windowed; a deliberately HARDER superset — the same aggregate unfiltered, fleet-wide, over seven days — measured 35.2 s, bounding any single shipped read far below it. ≥8x headroom over the most pessimistic estimate, ~44x over anything observed. (Measurement scope, stated honestly: the per-server windowed read itself returned in ~50 ms wall-clock on three runs but its output capture failed each time, so the floor rests on the verified reads plus the superset bound, not on that number.)CancelAfterand nothing restarts them — a stalled read holds a pooled store connection and hangs the MCP tool until the client gives up. 60 s is where a budget-rescued pass put a read (PgBaselineProvider inherits Npgsql's 30s default CommandTimeout, and is hitting it in production #2871); an unbudgeted interactive read sits strictly under that.The lock wait, bounded on both sides (review finding)
MigrationLockWaitTimeoutSeconds = 5 x MigrationCommandTimeoutSeconds(1500 s). The first pass gave thepg_advisory_lockacquire the statement bound, which is the wrong quantity: the lock is taken ONCE and held whileMigrateLockedAsyncapplies EVERY pending rung in the same session, so a several-rung upgrade could outlast the waiter while every individual statement stayed inside its own limit.index_object_statshypertable), V23 (create_hypertable ... migrate_data => true), V39 (two partial indexes overquery_statsandprocedure_stats), V104 (index onpg_deadlocks). The other 104 create the table they then index, or are metadata-onlyADD COLUMN/ALTER ... SET SCHEMA/DROP NOT NULL/ view refreshes — the whole 108-rung ladder measured 0.301 s end to end on a fresh PostgreSQL 17.11 / TimescaleDB 2.29.2 store (local Docker rig), advisory lock and version stamps included. Each of the four is one command already capped at the statement bound, so four multiples is the floor and the fifth is margin for the next data-moving rung. Seeded locally for scale, warm: V22 1.29 s over 907 MB / 90 chunks, V23 9.37 s over a 608 MB heap, V39 1.44 s over 1.26 GB — against 2.72 GB, 0.69 GB and 24.5 GB in those same tables on the live 42-server store.MigrationCommandTimeoutSeconds' own framing. Its comment singles out V23 as the data-moving rung; V22, V39 and V104 build btree indexes over already-populated hypertables, which TimescaleDB propagates to every existing chunk. V39's two targets are the store's largest fact tables.MigrateAsynchas exactly one production caller,DarlingWorker.RunCollectionLoopAsync, on the plain stopping token: noCancelAfteron the path, no configuredHostOptions.StartupTimeout(framework default is infinite), no health check or readiness probe in the repo, no containerHEALTHCHECK, no orchestrator manifest. The installers' 60 s / 2 minWaitForStatus('Running')do not bound it either — the worker is aBackgroundService, so the service reports Running before the first migration statement runs. So the value comes from the asymmetry: a waiter that dies mid-wait hitsLogCriticaland returns out of the collection loop, collecting NOTHING until an operator restarts it with no retry; one that waits longer only delays its own first cycle, its MCP and web surfaces having started independently. Long but finite, so a wedged holder still yields a readable deadline rather than a silent hang.MigrateAsyncagainst a held advisory lock failed at 12.02 s; reverting only the acquire to the old constant moved that to 3.02 s. Both surfaced asException while reading from streamwith an innerTimeoutException— the exact misdiagnosis 133 NpgsqlCommand sites still inherit Npgsql's undocumented 30s default timeout #2874 exists to remove. 1500 read back out of the rebuilt assembly by reflection (MVID changed across rebuilds, so each provably took).pg_try_advisory_lockwith a diagnostic retry loop, is deliberately NOT built here.Census correction (review finding)
The pin's doc comment said 69 untimed and 49 of the
.CreateCommand(shape while the CHANGELOG and this body said 70. Seventy is right. Re-derived by lifting the pin's own regexes anddepth <= 0span walker verbatim into anet10.0harness and running it over the merge-base tree (8e046fa3) — the only tree the claim describes, since post-fix the count is zero by construction:new NpgsqlCommand(.CreateCommand(So 50 of 70, not 49 of 69 — every one of the project's 50
.CreateCommand(sites was untimed, which is why that bucket cannot be 49 under any scanner variant. The 69 is traceable to a partly-corrected scan: the same walker with its depth counter clamped at zero reports 68, missing exactlyPgMigrations.cs:3981(the version read) andQueryStoreSliceRepair.cs:321(the rail-lift).a4bd263c's comment named the first,1593e4f8's named the second; the total was written when only one of the two had been found and never refreshed, and 49 follows by subtracting the correct 20 from the stale 69.367 of 371is verified and unchanged — it is Darling-wide, not project-scoped:.CreateCommand(across the three production Darling projects is.Service131 +.Viewer190 +.Storage50 = 371, of which 127 + 190 + 50 = 367 were untimed. The harness also reproduces the issue's original new-NpgsqlCommand-only figures for.Storage(20) and.Viewer(2) exactly, which is the check that says it counts the way the census did.The scanner finding worth keeping
Site 70 (
QueryStoreSliceRepair'sSET LOCALrail-lift, inside ausing (...) { }statement) was judged already-timed by two independent Python scanners — one clamping nesting depth at zero, one terminating only at exactly zero — because both leaked the statement span across the enclosing block's closing brace into a neighbour that DOES set a deadline. The CI-provendepth <= 0walker from #2882 judged it correctly. The pin's span walker uses those semantics, and a fixture now reproduces the nested-usingshape so the variant that misses it can't come back.Pin
StorageCommandTimeoutTests— directory-scoped where the alert-pass pin is name-scoped, deliberately: the claim here is a property of the project ("every command's deadline was chosen"), not of a budget boundary no filename expresses, so a future file is covered the day it appears. Sweeps both construction shapes; floors on total site count and file count so an empty sweep fails loudly. The regime constants are NOT frozen here — only the new constant's band is pinned.Proven red three ways, each failing a different assertion, via a
net10.0harness running the identical C# walker against the real tree and reflecting the constant from the rebuilt assembly (printed 30 → 2 → 120 → 30 across rebuilds, so each rebuild provably took):DarlingPgWaitReader.cs:83Not touched
InsertCollectionLogSqland its writers — no site in that family was untimed, no parameter handling changed anywhere in this PR, and the arity pin over its writers runs unchanged in CI..Service(198) and.Viewer(192) remain; census map updated on #2874.Verification scope: both projects build with
EnableWindowsTargeting; the Windows suites cannot run on macOS, so the xUnit copy of the scanner is exercised by CI while the identical-algorithm harness above ran locally. Store measurements were read-only (default_transaction_read_only = on, statement timeouts set).🤖 Generated with Claude Code
https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy