Skip to content

Give every alert-pass read an explicit command deadline (Part of #2874) - #2882

Merged
erikdarlingdata merged 5 commits into
devfrom
fix/2874-service-timeouts
Sep 4, 2026
Merged

erikdarlingdata merged 5 commits into
devfrom
fix/2874-service-timeouts

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

Part of #2874 — one group of several. Does not close it.

The group: the alert evaluation pass, 45 sites across 6 types

Grouped by the budget the sites actually run under, not by file. These seven types make up one alert evaluation pass, which DarlingWorker runs sequentially inside the per-server sweep body:

type sites
DarlingAlertReadAdapter 18
PgAlertStateStore 9
PgMuteRuleStore 6
DarlingSelfAlertEvaluator 6
DarlingPostgresAlertReadAdapter 4
PgAlertHistoryStore 2

All 45 set no CommandTimeout. This is live, not theoretical: on 2026-09-04 the forced-plan read failed five times on the production store, each surfacing as Exception while reading from stream — which is how Npgsql renders its OWN deadline, and reads literally as though the network broke. That is precisely the misdiagnosis #2826 exists to prevent.

Why this group could not take the 60s the two closed passes use

#2810 and #2871 both sit under DarlingWorker.s_analysisTimeout — a 120s CancelAfter that bounds the whole pass no matter how long any single command runs. Their per-command value had to fit below that.

This pass has no enclosing budget at all. EvaluateAlertsAsync is called with the plain stopping token; there is no CancelAfter anywhere on the path. So the per-command deadline is the pass budget, multiplied by however many reads run in sequence — and the body holds one of only MaxConcurrentServerSweeps = 4 fleet permits for its entire duration, with that server's sweep relaunch skipped throughout.

At the inherited 30s that is 45 x 30s of worst-case exposure against 25% of fleet sweep capacity. Copying 60s here would have doubled it.

The bound, both sides

Below — measured. The shipped query strings were dumped from the built assembly by reflection (not retyped) and timed against the production store's three busiest servers, cold and warm, via EXPLAIN (ANALYZE, BUFFERS):

query worst disk blocks
ForcePlanFailuresSql 1,744.9 ms 771,872 (~6.0 GB)
DatabaseStateDeviationsSql 2.8 ms 80
VolumeFreeSpaceSql 1.8 ms 5
PvsPressureSql 1.2 ms 42
CurrentBlockingWaitSql 0.6 ms 90
TempDbSpaceSql 0.5 ms 6

The whole pass's cost is one read, 620x the next slowest. 10s is 5.7x that worst case, so it absorbs a substantial stall rather than only the happy path.

Above — the cadence. s_alertSweepInterval is 30s, so one stalled read must still leave the pass able to finish inside the interval that will start it again.

The asymmetry is why erring SHORT is right, and it reverses #2810's direction. An exceeded deadline skips one alert check, logs it, and retries 30s later — one cycle of delay on one alert. A long-running read holds a fleet sweep permit and delays collection for every other server queued behind it. The recoverable failure is strictly cheaper, so this is the first value in the family set below what it inherited.

What the data cannot say. Every observed failure was killed at the 30s ceiling, so the record is right-censored — nothing establishes whether a stalled read wanted 35s or 300s. 10s is chosen from the measured cost and the cadence above, not fitted to a distribution the data cannot show.

#2874's census undercounts, and the pin covers what it missed

The issue counted 133 untimed sites by scanning new NpgsqlCommand(. That is not the only shape — NpgsqlDataSource.CreateCommand(sql) hands out a command with the same inherited default, and the census misses it entirely. Four of this group's own sites are that shape.

A repo-wide recount across both shapes is on the issue. The pin here matches both, so a guard keyed to one spelling cannot declare this family clean while a fifth of a file stays on the default — the #2786 shape exactly.

The pin scans by statement, not by a line window. The first draft used a 12-line window and reported three patched sites as offenders, because these sites embed verbatim SQL and the construction routinely spans 20+ lines. Widening the window would be worse: it would read the next command's deadline and report an untimed site as clean.

Review correction (applied)

Review caught that PgPlanForceActionStore is not in this pass at all — its only caller is PlanForceBot.RunAfterAnalysisAsync, dispatched as the analysis pass's post-pass hook. Verified: the hook is built at DarlingWorker.cs:4084 closing over the plain stoppingToken, so although invoked lexically inside the try owning passCts, it runs outside that 120s CancelAfter.

Those 4 sites were dropped from this PR rather than renaming the constant to something pass-agnostic. The two passes share the unbudgeted shape but not the cadence, and the upper bound here is derived specifically from the 30s s_alertSweepInterval — a pass-agnostic name would have made that bound look general when it is not. They need their own measurement and are left for their own group on #2874, with the exclusion documented in both the constant and the pin so they are not re-added by resemblance.

Verification

Darling.Tests is net10.0-windows and cannot run on macOS, so the assertions were replicated in a net10.0 harness against the real built assembly, reading the constant by reflection so each rebuild is visibly picked up.

Proved red three ways, each failing a different assertion:

variant structural lower band upper band
baseline (45 sites, 10s) PASS PASS PASS
revert one site FAIL (offenders=1) PASS PASS
restore 30s — PASS FAIL
set 2s — FAIL PASS

Note the restore after the first variant over-reverted (a plain git checkout took the file back to origin/dev, dropping all six of its patches, not the one removed); the follow-up verification run caught it at 6 offenders and it was re-applied.

Scope

133 -> the true figure is higher; this group closes 45. Remaining groups and the corrected census are recorded on #2874. Lite is out of scope — DuckDBCommand.CommandTimeout defaults to 0, so the mechanism cannot occur there.

No migration rung, no StorageVersion.SchemaVersion bump. CHANGELOG staged 2 insertions / 0 deletions with a plain git add — #2858's renormalization is holding.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy

All forty-nine commands across the seven alert-pass types set no CommandTimeout,
so every one inherited Npgsql's undocumented 30s default. On 2026-09-04 the
forced-plan read failed five times on the production store, each surfacing as
"Exception while reading from stream" — how Npgsql renders its own deadline, and
the misdiagnosis #2826 exists to prevent.

Unlike #2810 and #2871, which sit under DarlingWorker.s_analysisTimeout's 120s
CancelAfter, this pass has NO enclosing budget: EvaluateAlertsAsync is called
with the plain stopping token, so the per-command deadline is the pass budget
multiplied by the number of sequential reads — 49 x 30s of exposure while the
body holds one of only four fleet sweep permits and that server's relaunch is
skipped throughout.

The value is therefore the first in this family set BELOW what it inherited.
10s is bounded under by the measured worst case (shipped queries timed on the
production store's three busiest servers: the forced-plan check at 1,744.9ms
cold over ~6.0GB dominates; every other read lands under 3ms) and bounded over
by the 30s s_alertSweepInterval the pass runs on. The asymmetry justifies
erring short — an exceeded deadline skips one alert check and retries 30s
later, while a long-running read starves fleet-wide collection.

Every observed failure was killed AT the 30s ceiling, so the record is
right-censored: 10s is chosen from the measured cost and the cadence, not
fitted to a distribution the data cannot show.

Pinned structurally over BOTH construction shapes. #2874's census counted only
`new NpgsqlCommand(`, missing NpgsqlDataSource.CreateCommand(sql) — the same
inherited default, and four of this group's own sites. The pin scans by
statement rather than a line window, because these sites embed verbatim SQL and
a window wide enough to reach the initializer would read the NEXT command's
deadline and report an untimed site clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
replica_role, parameter_sensitivity_cofired, outcome, detail, related_action_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
RETURNING action_id", connection);
RETURNING action_id", connection) { CommandTimeout = DarlingAlertReadAdapter.AlertPassCommandTimeoutSeconds };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor scope note: PgPlanForceActionStore isn't actually reachable from EvaluateAlertsAsync — grepping its callers, the only one is PlanForceBot.RunAfterAnalysisAsync (wired in via RunScheduledAnalysisAsync, DarlingWorker.cs:4081), which runs after the AN3 analysis pass's own passCts/120s budget has already completed, on the plain stoppingToken. So this store's 4 sites don't belong to "the alert evaluation pass" the PR body and AlertPassCommandTimeoutSeconds's doc comment describe (30s sweep cadence, MaxConcurrentServerSweeps fleet permits, etc.) — they belong to the post-analysis force-plan pass, which happens to have a similar "no enclosing budget" shape but for different reasons (different cadence, different permit).

Tightening the timeout here is still reasonable (10s < the previous 30s default either way), but the constant name and its justification are borrowed from a pass this store doesn't run in. Worth either renaming to something pass-agnostic (e.g. UnbudgetedReadCommandTimeoutSeconds) or documenting PlanForceBot's own budget shape separately, so the next reader doesn't assume this store shares the alert pass's 30s-cadence bound.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and verified rather than taken on trust: PgPlanForceActionStore's only caller is PlanForceBot, and the hook is constructed at DarlingWorker.cs:4084 closing over the plain stoppingToken — so although it is invoked lexically inside the try that owns passCts, it runs outside that 120s CancelAfter's cancellation. Unreachable from EvaluateAlertsAsync.

I took the third option rather than either you offered: dropped those 4 sites from this PR. Renaming to something pass-agnostic would have kept the value while quietly widening the claim — the two passes share the unbudgeted shape but not the cadence, and my upper bound is derived specifically from the 30s s_alertSweepInterval. A pass-agnostic name would have made that bound look general when it is not.

Doing them properly needs their own measurement and their own cadence reasoning (analysis interval, clamped 5-360 min), which this PR has not done. They are left for their own group on #2874, and the exclusion is now documented in both the constant and the pin's file list — specifically so the next reader does not re-add them by resemblance, which is exactly what I did.

Group is now 45 sites across 6 types. Pin re-verified against the real build: 45 sites, 0 offenders.

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review

Verified the mechanical claim first: grepped the 7 listed files for new NpgsqlCommand(/.CreateCommand( and for CommandTimeout = ...AlertPassCommandTimeoutSeconds -- counts match exactly (18/9/6/6/4/4/2 = 49 in, 49 out) in every file. The value (10s, bounded 5s-30s) and the test's statement-span scanner (handles verbatim/regular string literals and nested parens/braces correctly, verified by reading) both check out. No T-SQL in this diff, so the T-SQL style rules don't apply; the C# additions follow the established internal const int ...TimeoutSeconds pattern already used by #2810/#2871 (FactCommandTimeoutSeconds, AnalysisCommandTimeoutSeconds). Lite is correctly out of scope -- confirmed no DuckDBCommand.CommandTimeout is set anywhere in Lite/, and the repo's own comment at Lite/Services/RemoteCollectorService.cs:1313-1318 documents the same 0-means-unlimited default the PR body cites.

Correctness gap: one site in the exact same pass was missed, and it's invisible to the new pin

DarlingWorker.EvaluateAlertsAsync (DarlingWorker.cs:2868) -- the very method this PR is about -- calls ReadLatestCpuAsync as its first statement, unconditionally, for every server on every sweep:

var (sqlCpu, totalCpu) = await ReadLatestCpuAsync(runtime.ServerId, cancellationToken);

ReadLatestCpuAsync (DarlingWorker.cs:3842-3865) builds its own NpgsqlCommand against cpu_utilization_stats and never sets CommandTimeout:

using var command = new NpgsqlCommand(@"
SELECT sqlserver_cpu_utilization, other_process_cpu_utilization
FROM cpu_utilization_stats
WHERE server_id = $1
ORDER BY sample_time DESC
LIMIT 1", connection);

This is the same inherited-30s-default mechanism the PR fixes everywhere else, on a read that runs inside the identical unbudgeted, fleet-permit-holding pass the PR's own reasoning is built around -- it just lives in DarlingWorker.cs rather than one of the 7 files AlertPassCommandTimeoutTests.s_alertPassSources enumerates, so it's a 50th untimed site that the new structural pin cannot see and will not catch if it regresses further. Worth adding CommandTimeout = DarlingAlertReadAdapter.AlertPassCommandTimeoutSeconds here and either adding DarlingWorker.cs (scoped to this method) to the pin's source list or covering it some other way, otherwise "every read in the alert evaluation pass now carries an explicit deadline" isn't quite true yet.

Minor: scope of PgPlanForceActionStore

Left as an inline comment -- that store's 4 sites are actually called from PlanForceBot.RunAfterAnalysisAsync (the post-analysis force-plan pass), not from EvaluateAlertsAsync. The tighter timeout is still fine there, but the constant's justification (30s sweep cadence, MaxConcurrentServerSweeps fleet permits) doesn't actually describe that call path.

No SQL injection / secrets / process concerns -- all parameterized, no new external I/O. No Lite/Darling parity issue beyond the note above.

erikdarlingdata and others added 2 commits September 3, 2026 23:08
Review catch, and correct. PgPlanForceActionStore is not reachable from
EvaluateAlertsAsync at all — its only caller is PlanForceBot.RunAfterAnalysisAsync,
dispatched as the analysis pass's post-pass hook. Verified: the hook is
constructed at DarlingWorker.cs:4084 closing over the plain stoppingToken, so
although it is invoked lexically inside the try that owns passCts, it runs
outside that 120s CancelAfter's cancellation.

It therefore shares the unbudgeted shape but NOT the cadence: it runs on the
analysis interval (clamped 5-360 minutes), not this pass's 30s
s_alertSweepInterval. The upper bound in AlertPassCommandTimeoutSeconds is
derived from that 30s cadence, so applying it to those four sites would have
attached a justification borrowed from a pass they do not run in.

Dropping them rather than renaming the constant: they need their own measurement
and their own cadence reasoning, which this PR has not done. Left for their own
group on #2874, and the exclusion is documented in both the constant and the pin
so the next reader does not re-add them by resemblance.

Group is now 45 sites across 6 types. Pin re-verified: 45 sites, 0 offenders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewed. This is a narrow, mechanical change (Darling-only, C#) and it holds up:

  • Coverage verified: grepped the six alert-pass files post-patch — all 45 new NpgsqlCommand(...) / .CreateCommand(...) sites carry CommandTimeout = AlertPassCommandTimeoutSeconds (18/4/9/6/2/6 by file, matching the PR description's table). No leftover untimed construction in the pass.
  • Narrative checked against code: DarlingWorker.cs:1825 calls EvaluateAlertsAsync(engine, server, config, stoppingToken) with the plain token — confirmed no enclosing CancelAfter for this pass, unlike the analysis pass's passCts.CancelAfter(s_analysisTimeout) at line 4145. The PgPlanForceActionStore exclusion is also accurate: its 4 sites remain untouched, and its only caller (PlanForceBot.RunAfterAnalysisAsync) is confirmed dispatched off the analysis pass's post-pass hook, not this one.
  • Accessibility: AlertPassCommandTimeoutSeconds is internal, matching the existing PgFactCollector.FactCommandTimeoutSeconds pattern, and PerformanceMonitor.Darling.Service.csproj already has InternalsVisibleTo for Darling.Tests, so the new test's cross-project reference resolves fine.
  • Test heuristic: walked through StatementSpanFrom's bracket/string-skipping logic by hand against both command-construction shapes (inline object-initializer and CreateCommand + follow-up assignment) — it correctly scopes to each command's own statement(s) without leaking into the next command's timeout, including for using (...) block forms where the semicolon count starts inside the block body.
  • Lite parity: no drift — Lite is explicitly and correctly out of scope, since DuckDBCommand.CommandTimeout defaults to 0 and the inherited-30s-default failure mode this PR fixes cannot occur there.
  • CommandTimeout ordering: for the NpgsqlDataSource.CreateCommand sites in DarlingPostgresAlertReadAdapter.cs, the timeout is set before ExecuteReaderAsync/ExecuteScalarAsync in every case, so it takes effect as intended.

No correctness, security, or performance issues found. Nothing to flag inline.

Comment on lines +159 to +189
private static string StatementSpanFrom(string text, int start, int statements)
{
var depth = 0;
var seen = 0;
var i = start;

while (i < text.Length)
{
var c = text[i];

if (c == '@' && i + 1 < text.Length && text[i + 1] == '"')
{
i = SkipVerbatimString(text, i + 2);
continue;
}

if (c == '"')
{
i = SkipRegularString(text, i + 1);
continue;
}

if (c is '(' or '[' or '{')
{
depth++;
}
else if (c is ')' or ']' or '}')
{
depth--;
}
else if (c == ';' && depth <= 0 && ++seen >= statements)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor robustness gap in the guard itself: StatementSpanFrom skips verbatim (@"...") and regular ("...") string content when counting semicolons, but it does not skip C# // or /* ... */ comments. A semicolon inside a comment placed between a command construction and its follow-up CommandTimeout = ...; statement (the two-statement window the CreateCommand shape relies on) would end the scanned span early, so the pin could report a correctly-timed site as an offender.

None of the current 45 sites trigger this — I checked each CreateCommand site in DarlingPostgresAlertReadAdapter.cs and none has an interposed comment — but it's a latent false-positive (CI flake) waiting for the next contributor who adds an explanatory comment in that exact spot, which this codebase's style otherwise encourages. Worth a short-circuit for // and /* */ alongside the existing string skips, given how much this guard is relied on to stay accurate.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and proved load-bearing rather than assumed. Built the exact shape you described — a CreateCommand site with an interposed comment containing a semicolon — and confirmed the scanner reports it as an offender WITHOUT comment-skipping and clean WITH it.

You are right that it is the false-positive direction that matters: a guard that fails a green build on correct code is worse than one that misses, because it trains people to distrust the pin. And the gap is exactly where this codebase would put a comment, since the two-statement window exists for the CreateCommand shape whose deadline is the statement after the construction.

// and /* */ are now skipped alongside the string literals. I also added a Theory over StatementSpanFrom itself — forty-five sites' correctness is asserted through that scanner, so its blind spots are worth pinning directly: the comment gap, the verbatim-SQL construction whose deadline sits past any fixed line window, and a genuinely untimed site as the negative control.

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewed the diff and cross-checked against the repo state (not just the diff hunks).

Coverage verified by hand. Grepped all six alert-pass source files for both new NpgsqlCommand( and .CreateCommand( and confirmed every construction site — all 45 — now sets CommandTimeout = AlertPassCommandTimeoutSeconds. No missed sites, no false additions.

Exclusion claim checked. PgPlanForceActionStore's only caller is PlanForceBot.RunAfterAnalysisAsync, and that closure is built at DarlingWorker.cs:4084 over the plain stoppingToken, confirming it's outside passCts/s_analysisTimeout as the PR states — correctly left out of this group.

Constants checked against the doc comment's own numbers. s_alertSweepInterval = 30s, s_analysisTimeout = 120s, MaxConcurrentServerSweeps = 4 — all match what the new doc comment on AlertPassCommandTimeoutSeconds claims. The 5s/30s test band (TheAlertPassDeadline_StaysInsideItsJustifiedBand) is internally consistent with the measured 1.7s worst case and the 30s cadence.

Lite parity. No Lite counterpart exists for DarlingSelfAlertEvaluator, and the other touched types' Lite twins (LiteAlertReadAdapter, LiteAlertStateStore, DuckDbMuteRuleStore, DuckDbAlertHistoryStore) contain zero CommandTimeout/command-construction sites of this shape — consistent with the PR's claim that DuckDB's driver doesn't have this failure mode. No parity gap.

Security/correctness. All touched sites remain fully parameterized ($1, $2, AddWithValue); no new string concatenation of untrusted input. The one dynamic-SQL site (PgAlertHistoryStore's dedup LIKE clause) is pre-existing and untouched other than the added timeout.

One inline nitpick on the new test's statement-span scanner (a latent false-positive risk, not tripped by anything in this PR). Otherwise this is thorough, well-bounded, and matches the stated conventions — nothing else to flag.

StatementSpanFrom skipped string literals but not // or /* */ comments, so a
semicolon inside a comment placed between a CreateCommand construction and its
follow-up CommandTimeout statement would end the scanned span early and report a
correctly-timed site as an offender.

None of the current 45 sites trigger it, but it is latent precisely where this
codebase's style puts an explanatory comment — the two-statement window exists
for the CreateCommand shape, whose deadline is the statement AFTER the
construction, and that gap is exactly where someone would explain why. A false
positive in a guard is worse than a miss: it fails a green build on correct code
and trains people to distrust the pin.

Proved load-bearing on the exact shape the review described: without
comment-skipping the scanner reports a correctly-timed CreateCommand site as an
offender; with it, clean.

Added a Theory over the scanner itself, since forty-five sites' correctness is
now asserted through it — covering the comment gap, the verbatim-SQL
construction whose deadline sits past any fixed line window, and a genuinely
untimed site as the negative control.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX6HyjsuDCs15qGB2rh4Gy
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewed the diff against CONTRIBUTING.md conventions, Lite/Darling parity, correctness, and security.

Findings: none. This is a tightly-scoped, well-verified change and I could not find a defect in it. Specifically checked and confirmed against the actual code (not just the PR narrative):

  • All 45 command-construction sites across the 6 listed types (DarlingAlertReadAdapter 18, PgAlertStateStore 9, PgMuteRuleStore 6, DarlingSelfAlertEvaluator 6, DarlingPostgresAlertReadAdapter 4, PgAlertHistoryStore 2) now set CommandTimeout = AlertPassCommandTimeoutSeconds, with no duplicate/missed sites (verified by diffing ctor-shape counts against CommandTimeout assignment counts per file).
  • DarlingWorker.MaxConcurrentServerSweeps (4) and s_alertSweepInterval (30s), cited in the new doc comment, match the actual constants in DarlingWorker.cs.
  • EvaluateAlertsAsync is genuinely called with the plain stoppingToken at DarlingWorker.cs:1825, with no enclosing CancelAfter anywhere on that path — the "no pass budget" premise the 10s value is derived from holds up.
  • The PgPlanForceActionStore exclusion is correct: its only caller (PlanForceBot.RunAfterAnalysisAsync) is wired up at DarlingWorker.cs:4084 closing over the plain stoppingToken, outside the analysis pass's passCts/120s CancelAfter — so it's right to leave those 4 sites out of this group rather than fold them in under a borrowed justification.
  • internal const int AlertPassCommandTimeoutSeconds is reachable from Darling.Tests via the existing InternalsVisibleTo entry in PerformanceMonitor.Darling.Service.csproj.
  • Lite parity: no Lite-side change is needed here. The "Lite is out of scope because DuckDBCommand.CommandTimeout defaults to 0" claim matches an already-documented fact elsewhere in the Lite codebase (RemoteCollectorService.cs:1316), so this isn't a new/unverified assertion, and it's a legitimate reason this group doesn't need a Lite counterpart.
  • All parameters go through AddWithValue with positional ($1, $2, ...) placeholders — no string-built SQL from external input, no injection surface introduced.
  • The new AlertPassCommandTimeoutTests scanner (statement-span-based, comment- and string-literal-aware) is sound for the shapes it's meant to catch; its own regression tests cover the comment-gap and verbatim-SQL cases called out in the commit message.

No inline comments to post since nothing rose to a reportable finding.

@erikdarlingdata
erikdarlingdata merged commit 8e046fa into dev Sep 4, 2026
6 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/2874-service-timeouts branch September 4, 2026 03:51
erikdarlingdata added a commit that referenced this pull request Sep 4, 2026
…w source

Three corrections, two from other lanes and one from review.

Review caught that TryReadEndpointTogglesAsync's linked CTS and its command
deadline were the same duration, so which one fires is a race - and the arms
report differently. The CTS surfaces as OperationCanceledException and takes
the "did not answer within N seconds" arm; an Npgsql CommandTimeout expiry
falls through to the general arm and renders "Exception while reading from
stream", which is the #2826 costume, handed to an installer by the one site
whose purpose is to explain the failure. Flagged non-blocking; it is not,
because it undercut the reason the constant exists. The budgeted site now
takes CliBudgetBackstopSeconds = CliStoreReadSeconds + 5, which is the same
ordering rule the HypoPG forward path uses against the server-side GUC with a
client-side CTS as the authoritative bound instead. Pinned as an ordering AND
a placement, since the ordering alone would be satisfied by the backstop
leaking onto one of the two verbs no budget encloses.

The pin matched its value regexes over StatementSpanFrom's span cut from RAW
source, so a CommandTimeout written in a COMMENT inside the two-statement
window made an untimed site read clean - #2940's finding, inherited from
every landed pin in this sweep. StripCommentsAndStrings is length-preserving,
so the span is now cut from the stripped text at the same offsets. Proven on
real source by commenting out this group's own hypopg_reset() deadline.

And store connection acquisition is out of both store-side floors: #2940
measured the connect phase tracking the connection string's Timeout rather
than CommandTimeout, so #2819's 673-893ms is outside what these constants
bound. The force-plan floor is now #2882's 1,744.9ms production cold read
instead of a container figure, and the note says plainly that the ceiling
fixes that number while the floor only rules out the bottom two seconds.
pull Bot pushed a commit to ehtick/PerformanceMonitor that referenced this pull request Sep 10, 2026
…lingdata#2874)

DarlingWorker.ReadLatestCpuAsync is the first read EvaluateAlertsAsync
performs, on every server, on every 30s tick, and it set no
CommandTimeout - so it inherited Npgsql's undocumented 30s default and
would surface a deadline as "Exception while reading from stream".

It takes DarlingAlertReadAdapter.AlertPassCommandTimeoutSeconds, the
value erikdarlingdata#2882 derived for exactly this regime: no enclosing budget,
bounded below by a measured 1,744.9ms worst-case read and above by the
30s s_alertSweepInterval the pass runs on. No new number.

AlertPassCommandTimeoutTests scoped itself with a hardcoded array of six
filenames and DarlingWorker.cs was not one, so the guard declared the
pass clean while this site sat on the default. The scope now also walks
the transitive closure of calls out of EvaluateAlertsAsync, so the claim
is computed rather than restated: a read added to any member the pass
reaches is covered when it is written. A whole-file sweep of
DarlingWorker.cs could not do this - it holds seven command sites across
four budgets, six of them deliberately owned by other groups.
pull Bot pushed a commit to ehtick/PerformanceMonitor that referenced this pull request Sep 10, 2026
…er regimes

Thirteen command sites, four derivations, because these regimes disagree
about which direction is dangerous.

hypopg_reset() is the inverted one. It runs post-rollback, so the SET LOCAL
statement_timeout no longer applies (measured: SHOW reads 0 and pg_sleep(8)
completes in 8.01s immediately after ROLLBACK), and it takes
CancellationToken.None so no token bounds it either. Its CommandTimeout is
the only bound it has. A client-side timeout on a RESPONSIVE backend leaves
the connection Open and the phantom hypothetical index in the pool - same
backend pid on re-borrow - while a responsive backend would have completed a
0.24-0.93ms memory free. So the value goes UP, to 6x the forward path. It
stays finite because a timeout against an unresponsive backend breaks the
connection and the pool discards it, which is the correct outcome, while 0
would pin a command-plane worker on an unreachable server forever.

The forward path goes the other way, 60s to 20s, derived strictly ABOVE the
15s server-side GUC so PostgreSQL's diagnosable 57014 keeps winning over
Npgsql's "Exception while reading from stream".

PostAnalysisForcePlanSeconds bounds what erikdarlingdata#2882 deferred, against the 120s
analysis pass the hook rides on rather than a cadence. CliStoreReadSeconds
is not a new number - TryReadEndpointTogglesAsync already derived ten
seconds and prints it to the operator, and the command inside it plus two
sibling verbs never got it.

And --recompress-plan-dim's plan-codec preflight now reports its failure
instead of throwing out of the process: it was the one store call in that
verb with no handler, and Program.cs's verb dispatch has none either, so a
deadline there would only have changed how long the stack trace took.

No schema change, no version bump.
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