Repository navigation
Bound pg_index_bloat by bytes and classify the deadline it dies on - #3003
Conversation
The 200-index cycle budget bounds a count where the cost is per page. On the one target with pgstatindex installed, 200 sub-ceiling indexes admit 286 GB in a single LEFT JOIN LATERAL, so every run since the collector shipped spent the full 300-second deadline and recorded zero rows. Add a cumulative sub-ceiling byte budget beside the count, gated on the LATERAL rather than on skipped_reason alone. Charging only sub-ceiling indexes is load-bearing: the over-ceiling ones are never read, and on that target they total 71 GB against a 20 GB budget. Classify the timeout. A client-side deadline arrives as an NpgsqlException wrapping a TimeoutException with no SQLSTATE, so PostgresFaultOutcome's parameter type excluded it however wide the SQLSTATE map grew; it now gets an authored sentence naming the collector, the database, the mechanism and the measured elapsed time. ConnectionFatal stays with the general catch, which is where the reprobe lives. Record real elapsed time on both ERROR arms. Three literal zeros made a 300-second death and an instantly-refused connection store byte-identical rows, and duration_ms is sqlMs + storageMs. Part of #2997
Review summaryReviewed the byte-budget gate on Correctness — SQL ( Correctness — Darling runner ( One correctness finding (inline): One minor nit (inline): a comment in Lite/Darling parity: No missing-index DMV suggestions, no security issues, no performance regressions identified (the running-total window is negligible next to the actual |
The authored PostgreSQL command-timeout arm is another early-return fault arm, so it passes drain: null and fetchPhases: null for the same reason the other seven do. Both counts move 7 -> 8; the count is the point of that pin, so it is bumped rather than loosened. StartupFailureTriageTests counted every Stopwatch.StartNew() in the file as a proxy for a retry site. Every retry budget is a stopwatch, but not every stopwatch is a retry budget, so a per-run clock in the collector fault arms made a startup-triage pin red about something it has no opinion on. Narrowed to the RetryBudget suffix all three sites share, which is what its own sentence already claimed to measure. Part of #2997
The arm fires for every PostgreSQL collector classified as CommandTimeout, but only pg_index_bloat and pg_buffer_usage set CommandTimeoutSecondsOverride; the rest fall back to DarlingCollectorRunner.CommandTimeoutSeconds. Naming the override was therefore false for most collectors that can reach the arm, and pointed an operator at a setting that collector does not have. The message now says 'its command timeout', and the measured elapsed time already reports what the deadline actually was rather than what was configured somewhere. Pinned both ways: the explanation must not contain the override's name. Also corrects a comment that referenced a test name which was never written. Part of #2997
The comment block above the ranking still described a single count budget and the failure it was built to stop, which is no longer the current state: there are two, and the byte one is the one that bounds the work. SQL comment only - the statement is byte-identical, re-checked by dumping BuildQuery(ctx).Text and running it against PostgreSQL 17.11 again. Part of #2997
|
Reviewed the diff ( Byte budget (
Timeout classification (
Parity — pg_index_bloat and the Postgres timeout-classification path are Darling-only (Lite has no Minor, non-blocking nit: the new catch's filter hardcodes Nice test coverage on the tricky part (the FILTER/window-function interaction) via |
Review asked whether it was an oversight that this filter hardcodes false while the PostgresException arm above passes the collector's real value. It is not: the flag only decides the 55P03 branch inside Classify, so it cannot change a CommandTimeout answer, and both plain-Exception filters in this method pass false for that reason. Answered in place rather than in a comment thread, since the next reader will have the code and not the thread. Comment only. Part of #2997
|
Reviewed the diff (DarlingWorker.cs's new command-timeout catch arm + elapsed-time fix, and PgIndexBloatCollector.cs's byte-budget CTE) against CONTRIBUTING.md's conventions and for Lite/Darling parity. Summary: Correctness — solid. Traced the SQLSTATE/exception-classification path end to end:
Lite/Darling parity — checked, no drift.
One nit posted inline on No security, injection, or SQL-Server-missing-index concerns. All SQL is compile-time literals/constants (no dynamic string building from user input). Test coverage for this change is unusually thorough and the PR description already documents two rounds of self-review with mutation-style verification — nothing else stood out. |
Review asked whether the two 20 GB constants being equal was deliberate. It is, and equality is a FLOOR rather than a coincidence: a cycle budget under the per-index ceiling opens a band in which an index can never be measured at all. It is under the ceiling, so it is a legitimate candidate and earns no 'too large' reason, yet it alone exceeds the whole cycle, so it is over budget on its own first row on every run. It would carry 'not measured this cycle' forever - a reason whose entire claim is that the index is deferred, attached to one that is permanently skipped, which is the unmeasured-versus-healthy confusion skipped_reason exists to stop, one level up. Pinned as an inequality, because raising the budget above the ceiling is a legitimate tuning move once a SUCCESS row supplies a real duration; only the floor is load-bearing. Also records the accepted consequence review named: at equality a single index just under the ceiling consumes almost the whole cycle, which on the target this was built for means one index per run. Correct for a largest-first budget, and the tail's coverage depends on rotation that does not exist yet. Doc comment and test only - the query text is byte-identical. Part of #2997
|
Reviewed the diff in depth (SQL in Catch-arm ordering (
Byte-budget SQL ( Parity — One non-blocking observation, already called out explicitly in the PR description so not a new finding: with |
|
Reviewed the diff against CONTRIBUTING.md (T-SQL style rules don't directly apply here since this is PostgreSQL collector SQL, but checked the Postgres-specific conventions: naive-UTC timestamps N/A, no migration to schema-qualify, no SQL logic (
Parity: Test pins: verified independently — No correctness, security, or parity issues found. Nice catch on the |
The doc on TheByteBudget_AccumulatesLargestFirst claimed an unframed window sum returns the whole total on every row. That is only true without an ORDER BY; with one, the default frame is RANGE, which is a running total that groups peers. Review raised the RANGE distinction and the first correction I wrote had it backwards - that a tie would admit MORE bytes. Measured on PostgreSQL 17.11 with a real tie (two schemas, same index name, 1,138,688 bytes each): ROWS gives 1,138,688 then 2,277,376, while the default RANGE frame gives 2,277,376 on BOTH rows. So RANGE charges the tie's entire total to its first member and the pair goes all-or-nothing against the budget - skipped together even where one would have fitted. It under-admits, and the numbers are in the comment now instead of a direction I guessed. Test doc comment only. Part of #2997
|
Reviewed the diff. One correctness finding posted inline on Everything else checked out:
Worth noting the PR description itself already discloses a related, secondary wrinkle — that the recorded elapsed time can be a sum across multiple per-database timeout attempts rather than a single command's deadline — so I haven't re-flagged that separately. |
The collision called out in the PR description landed: #2993's log_timezone PERMISSIONS arm and this branch's PostgreSQL command-timeout arm each added one early-return fault arm, and each lane independently moved this literal from 7 to 8. Both arms belong in the count, so the merged file has nine and the pin says nine. The conflict is the pin working - neither lane could see the other, and without a counted assertion the number would simply have been wrong.
Review found that both PostgreSQL fault arms read the database from ServerRuntime.ConnectedDatabase, which is init-only and stamped once at the initial connect-and-probe. A RunsPerDatabase collector never uses that connection - the per-database loop opens one per database - and when every database fails it rethrows the first failure bare. So the handlers named a database that had nothing to do with the fault, confidently. Seven collectors fan out that way, pg_index_bloat among them, which made it the wrong database for the exact collector this branch is about. It was also already wrong for #2638's 'run CREATE EXTENSION in database X' sentence, which exists specifically to stop an operator checking the wrong database - so that arm is fixed here too rather than left as the same defect under another message. The name rides on Exception.Data, not a wrapper exception, because every arm of PostgresTargetProvider.Classify keys on the exception's TYPE: wrapping the failure would silently re-route the fault to a different handler in order to improve a sentence. Reading is total, so the collectors that do not fan out behave exactly as before. Part of #2997
|
Reviewed the diff against Correctness (SQL) — Traced the new byte-budget window carefully:
Correctness (C#) — Walked the new
Lite/Darling parity — No drift. Style — SQL follows the file's existing conventions (uppercase keywords, Minor/non-blocking: the new |
Review flagged the one loose cross-reference on the branch: the cycle-budget doc pointed at ':61' for the per-index ceiling's unmeasured 'a few seconds of I/O' estimate. A bare line number silently points at the wrong text the next time anything above it moves, which is the same failure as the stale test name earlier in this branch. Now a cref to MeasureCeilingBytes, which the compiler checks. Doc comment only - the query text is byte-identical. Part of #2997
Review summaryReviewed the diff (SQL byte-budget change in Lite/Darling parity: No gap. The SQL/budget change lives in SQL correctness ( C# fault handling (
One finding posted inline: the new No missing-index DMV recommendations, no secrets/credentials, no obvious performance regressions — the byte budget is strictly a cost-reduction change. |
Review found the new arm claiming more than its filter guarantees. Classify's first branch answers CommandTimeout for ANY TimeoutException, and EnumeratedCollectorDriver.ItemBudgetException throws a bare one for the in-process per-item wall-clock budget - a cut this service makes on itself, which never reached the database and involves no Npgsql read. The authored sentence would have told an operator that Npgsql cancelled a read mid-stream about a budget we abandoned ourselves. Latent today, because no PostgreSQL collector declares PerItemWallClockBudget. That is exactly the kind of true-when-written a message should not rest on, and the branch already stamps that budget exception with its database, so the two mechanisms are one edit away from meeting. Both real deadlines keep the arm: PostgresException derives from NpgsqlException, verified against the shipped Npgsql 10.0.3 rather than assumed, so SQLSTATE 57014 still lands here. The bare TimeoutException now falls to the general catch, which classifies it CommandTimeout as well - so it still forces no reprobe - and reports the budget's own message with the real elapsed time instead of a borrowed one. Part of #2997
ReviewWent through the full diff ( Byte-budget SQL (
Each reason condition is the exact complement of its join condition, and the Fault classification (
Lite/Darling parity — no drift. Security — no user input reaches the new SQL (the byte/count literals are compile-time constants baked into both the C# constants and the query text, cross-pinned by Nice catches in the review history baked into this branch already (the |
pg_index_bloathas produced zero rows fleet-wide since it shipped. On the one target withpgstatindexinstalled, all eleven runs arestatus=ERROR,rows=0,Exception while reading from stream— including every run after #2618's command-timeout override landed. This bounds the work by bytes, gives the deadline a readable classification, and stops the ERROR arms from recording a literal zero where the duration goes.Part of #2997.
The 300-second reading, and what the store can and cannot confirm
The issue's evidence points at a timeout at the 300-second override rather than a network fault, and source agrees:
CommandTimeoutSecondsOverride => 300is present, wired (DarlingCollectorRunner.cs:1012honours it on the per-database path), and pinned. Npgsql's CommandTimeout is a socket read timeout that every backend message restarts, so it bounds backend silence — andpgstatindexis silent for the whole scan, so it gets no reprieve.The failure durations cannot be read from the store rows, and that is the third defect rather than a gap in the investigation.
RunOneAsync's general catch calledLogCollectionAsync(..., "ERROR", 0, 0, 0, ...), andduration_msis written assqlMs + storageMs. So all eleven rows carryduration_ms = 0,sql_duration_ms = 0,rows_collected = 0: the measurement that would distinguish a 300-second death from an instantly-refused connection was destroyed at write time. The only surviving evidence for the duration is the indirect one the issue already reports — the 316-second sweep freeze, measured from other collectors' gaps. That is consistent with a single database hitting the deadline, and after this change the same run records its own elapsed time directly.One amplification worth stating: because
RunsPerDatabaseis true and the per-database loop counts a failure and continues, a target where several databases have the extension pays the deadline per database, and only rethrows once all of them have failed. The 316-second observation implies one attempted database on that target; it is not the ceiling.What changed
1. The budget bounds bytes, not just a count. 200 indexes is a count-based budget on a per-page cost. It bounds pages only where count correlates with bytes, and the one target with the extension is the counterexample: 200 sub-ceiling indexes admit 286 GB in a single
LEFT JOIN LATERAL. The count budget stays for the legibility argument at:176— an operator can predict "the 200 biggest" — and a cumulative sub-ceiling byte budget now sits beside it, gated on the LATERAL rather than onskipped_reasonalone. Gating only the label is the shape that shipped: correct text on a statement that reads everything anyway.Charging only sub-ceiling indexes to the running total is load-bearing, not tidiness. An over-ceiling index is never handed to
pgstatindex, so it costs no pages; on that target the three over-ceiling indexes total 71 GB against a 20 GB budget, so an unfiltered running total would exhaust the allowance before the first measurable index and return zero measurements for a second, entirely new reason.The 20 GB figure is deliberately conservative and the doc comment says why: the only hard datum is negative — 286 GB did not finish in 300 s, so effective throughput is below 0.95 GB/s and the true value is unknown, because a cut-off run bounds only its own duration. The
:61"a few seconds of I/O" estimate was never measured and the live failure bounds it well away from that. What would justify raising it is a SUCCESS row's ownsql_duration_ms— which did not exist before item 3 below, and now does.2. The deadline is classified. #2618 wanted "a classified timeout rather than a dropped connection" and could not get one:
PostgresFaultOutcometakes aPostgresException, and a client-side deadline is anNpgsqlExceptionwrapping aTimeoutExceptionwith no SQLSTATE. The gap was the parameter type, so no amount of widening the SQLSTATE map could ever have reached it. A dedicated arm now catches it and composes an authored sentence naming the collector, the database, the mechanism, and the measured elapsed time, and it distinguishes the two deadlines that both classify asCommandTimeout— SQLSTATE 57014 is the target's ownstatement_timeoutand is fixed there; the client-side one is ours.Two deliberate narrowings against the issue's suggested shape:
ERROR. The store has five statuses and none means "ran out of time".PERMISSIONSis the non-fatal-degradation bucket and would exclude this from the error counts, the health band and the collection-failure self-alerts — which is exactly where a collector that has never once succeeded belongs. What changes is that the row is readable, not which bucket it lands in.ConnectionFatalis left with the general catch, which is where the reprobe lives. Classifying it in the new arm would have takenserver.Runtime = nullaway from it and re-created the bug that arm exists to fix. The provider already classifies a deadline asCommandTimeoutrather thanConnectionFatalso a slow statement cannot force a reconnect storm, and the new arm preserves that by not reprobing.3. Both ERROR arms record real elapsed time. A stopwatch read only by the fault arms; the success path keeps reporting the runner's own measured sql/storage split, because a wall clock including dispatch overhead would quietly widen three published numbers to satisfy a log line.
4. The message names the database that actually faulted. Found in review, and it is the sharpest defect on the branch.
ServerRuntime.ConnectedDatabaseisinit-only and stamped once during the initial connect-and-probe, from whatever database the probe landed on. ARunsPerDatabasecollector never uses that connection — the per-database loop opens one per database — and when every database fails it rethrows the first failure bare. So both PostgreSQL fault arms named a database that had nothing to do with the fault, with full confidence. Seven collectors fan out that way,pg_index_bloatamong them, which made it wrong for exactly the collector and target this PR is about.Both arms are fixed, not just the new one. The
ObjectMissingarm has had the same defect since #2638 — the arm whose entire purpose is stopping an operator from checking the wrong database, telling them toCREATE EXTENSIONin the probe's database on every permission-degraded target. Repairing the new arm and filing that one would have been shipping half a fix for a single defect.The name rides on
Exception.Datarather than a wrapper exception because every arm ofPostgresTargetProvider.Classifykeys on the exception's type: wrapping would change the type all of those checks see and silently re-route the fault to a different handler in order to improve a sentence.StampingDoesNotChangeHowTheFaultClassifiespins that. Reading is total, so the collectors that do not fan out behave exactly as before.5. Only an Npgsql fault earns the Npgsql narrative. Also from review.
Classify's first branch answersCommandTimeoutfor anyTimeoutException, andEnumeratedCollectorDriver.ItemBudgetExceptionthrows a bare one for the in-process per-item wall-clock budget — a cut the service makes on itself, which never reached the database and involves no Npgsql read. The arm would have told an operator that Npgsql cancelled a read mid-stream about a budget we abandoned ourselves. Latent, because no PostgreSQL collector declaresPerItemWallClockBudget— but this branch already stamps that samebudgetFailurewith its database name, so the two mechanisms are one edit from meeting.The filter now carries
ex is NpgsqlException. Verified against the shipped assembly rather than assumed: Npgsql 10.0.3 givesPostgresException : NpgsqlException = True, so SQLSTATE 57014 still qualifies, while the bareTimeoutExceptiondoes not and falls to the general catch — which classifies itCommandTimeouttoo, so it still forces no reprobe, and now reports the budget's own message with a real duration.Out of scope, and why
The sweep freeze is not addressed directly, deliberately. 300 s of a 60,000 ms sweep budget blocking every 1-minute collector is real, and the mechanism that would fix it structurally is detaching the collector the way #2717 does for
plan_correction. That is an independent architectural change with its own gate and its own risk, and it would be riding a fix whose entire purpose is that the run no longer takes 300 seconds. If the byte budget works there is nothing left to detach around; if it does not, the freeze is the symptom that says so, and detaching would have hidden it. Worth revisiting only if a bounded run still shows up in the sweep.Largest-first is deterministic, so the measured set never rotates. That is pre-existing from #2617, but the byte budget makes it sharper: on a target with 286 GB of sub-ceiling index, a 20 GB cycle measures roughly the same top handful every day and the tail is never measured at all — honestly labelled, but never measured. Not fixed here because rotation is a third design axis the issue does not raise and it would need its own state.
Two shared source pins move, and one of them collides with another lane
The new catch arm is another early-return fault arm, so it passes
drain: nullandfetchPhases: nullfor exactly the reason the other seven do — nothing was drained, no item completed.CollectionLogDrainForensicsStoreTests.ThePeerMarkIsCapturedAtDispatchNotAtCompletioncounts both, and the count is the point of that pin ("counted rather than merely present"), so it is bumped 7 to 8 rather than loosened. The test's other assertions all still pass unchanged, includingDoesNotContain("sweepPeerMaxMs: null")— the new arm carries the body's mark like every other arm.This collision has since landed and been resolved at nine.
fix/2993merged as #3001, and the merge conflicted on exactly this literal: both lanes added one early-return fault arm and each had independently moved the pin from 7 to 8. Both arms belong in the count, so the merged file holds nine and the pin now says nine (028337e). The conflict is the pin doing its job: neither lane could see the other, and without a counted assertion the number would simply have been wrong in whichever merged second. Nothing here touched their branch.StartupFailureTriageTests.TheThreeSitesHaveDistinctBudgetVariablesis a different case and was narrowed rather than bumped. It assertedEqual(3, CountOf(source, "System.Diagnostics.Stopwatch.StartNew();"))as a proxy for "exactly three triaged retry sites". Every retry budget in that file is a stopwatch, but not every stopwatch is a retry budget, so a per-run clock added to the collector fault arms — hundreds of lines and one concern away from startup triage — turned that pin red about something it has no opinion on. Bumping it to 4 would have pinned "there are exactly four stopwatches in DarlingWorker.cs", which is a meaningless coupling that the next unrelated timing breaks again. It now counts"RetryBudget = System.Diagnostics.Stopwatch.StartNew();", the suffix all three sites share, which is what its own sentence already claimed to measure and still forces a fourth retry site through it.Review dispositions
Two findings on the first pass, both fixed in
bbb2453and both replied to in-thread and resolved:pg_index_bloatandpg_buffer_usagesetCommandTimeoutSecondsOverride— the rest fall back toDarlingCollectorRunner.CommandTimeoutSeconds. It now says "its command timeout" and a pin asserts the override's name appears in neither branch of the explanation. Reinstating the old wording turnsTheTimeoutExplanationDistinguishesTheServersDeadlineFromOursred; verified.CycleByteBudgetIsThePinnedConstant, a draft name). Corrected toTheBudgetLiterals_AgreeWithTheirConstants. Not treated as a nit: a comment pointing at a pin is worth writing only if the pin is findable, and a wrong name reads as authoritative.Third pass asked whether
CycleMeasureBudgetBytesandMeasureCeilingBytesboth being 20 GB was deliberate, given each doc comment argues its number independently. Asking it produced something better than a confirmation, in1249648: the equality is a floor, for a reason neither comment gave. A cycle budget below the per-index ceiling opens a band in which an index can never be measured at all — under the ceiling so it earns no "too large" reason, yet alone exceeding the whole cycle so it is over budget on its own first row every run, carryingnot measured this cycle (work budget)in perpetuity. That is a reason whose entire claim is that the index is deferred, attached to one that is permanently skipped: the unmeasured-versus-healthy confusionskipped_reasonexists to prevent, one level up. Now pinned byTheCycleBudget_IsNeverBelowThePerIndexCeilingas an inequality, since raising the budget above the ceiling is legitimate tuning once a SUCCESS row supplies a duration; only the floor is load-bearing. Dropping the constant to 10 GB turns it red with the reason in the failure message.The same review named a real coverage consequence, now recorded on the constant as accepted: at equality a single index just under the ceiling consumes almost the whole cycle, and on this target the largest sub-ceiling index is 19.68 GB, so that run measures essentially one index rather than the ~14 the average implies. Correct for a largest-first budget, and it sharpens the rotation limitation named above.
Later passes raised the
ROWSversus defaultRANGEframe, which caught a false statement in one of my own test doc comments and then a second, opposite false statement in the first correction. The comment had claimed an unframed window sum returns the whole total on every row; that holds only without anORDER BY, since with one the default frame isRANGE, a running total that groups peers. My first fix then asserted a tie would admit more bytes. Measuring settled it, in4ed8278: on PostgreSQL 17.11 with a real tie — two schemas holding an index of the same name and the same 1,138,688 bytes, which is reachable becauseindex_nameis unique per schema rather than per database —ROWSgives 1,138,688 then 2,277,376 while the defaultRANGEgives 2,277,376 on both rows. SoRANGEcharges the tie's entire total to its first member and the pair goes all-or-nothing against the budget, skipped together where one would have fitted. It under-admits. The explicitROWSframe was already correct and is pinned byTheByteBudget_AccumulatesLargestFirst; only the explanation was wrong, twice, and the measured numbers are in it now rather than a direction I reasoned to.Second pass found nothing to block on and one non-blocking nit: the new filter hardcodes
yieldsOnLockTimeout: falsewhile thePostgresExceptionarm above passes the collector's real value. Keptfalse, and said why in the code (4fcd14d) rather than only in a thread. The flag only decides the55P03branch insideClassify, so it cannot change aCommandTimeoutanswer; passing the real value would imply the yield question bears on this filter. Both plain-Exceptionfilters in that method passfalsefor that reason — the pre-existing general catch included — and it is the SQLSTATE arm that is the exception, because it dispatches on the whole fault map.Verification
Everything below ran on macOS; the Windows-only suites cannot execute here, so the real test files were compiled into throwaway
net10.0xunit v3 console hosts staged outside the repo with the required<AssemblyName>and<Compile Include>pointing at the real repo paths.Lite.Testshost, focused: 16 tests, 0 failed (11 pre-existing + 5 new).Darling.Testshost, expanded to every one of the 25 test files that source-scanDarlingWorker.csplus their helpers: 432 tests, 0 failed, 9 skipped. The only failures were 7 inDarlingServiceInstallLocationTests, which asserts Windows path semantics (Expected "C:/Users"/Actual "C:\\/Users") and passes on the Windows runner — a macOS artifact, and CI confirms it.CollectionLogDrainForensicsStoreTestsandDarlingEmptyEnumerationNoteTestsreference the WPF Viewer, so they cannot compile into anet10.0host that actually runs on macOS. Its eight assertions were instead evaluated directly against the realDarlingWorker.cs— the same file it reads throughReadSource— and all eight pass, including the two counts at 8. CI is the arbiter for the test itself.Red proven per mutation, one at a time, each with the anchor count and the source hash, judged by which assertion failed:
ONTheCycleBudget_BoundsBytes_NotJustIndexCount,TheBudgetLiterals_AgreeWithTheirConstantsFILTER (WHERE index_bytes < ceiling)TheByteBudget_ChargesOnlyTheIndexesItWillActuallyRead,TheBudgetLiterals_AgreeWithTheirConstants"ERROR", 0, 0, 0on the general armTheErrorArmsRecordRealElapsedTimeRatherThanLiteralZerosTheErrorArmsRecordRealElapsedTimeRatherThanLiteralZerosTheTimeoutExplanationNamesTheCollectorTheDatabaseAndTheMeasuredElapsedTime,TheTimeoutExplanationDistinguishesTheServersDeadlineFromOursThe assertion that fails if the byte budget is reverted is
TheCycleBudget_BoundsBytes_NotJustIndexCount, which requires the byte term inside the LATERALONand not merely the column's presence.Live SQL, both majors the fleet runs. The shipped string was dumped from
BuildQuery(ctx).Text— never a retyped copy — and run verbatim against real PostgreSQL 17.11 and 16.15 in containers withpgstattupleinstalled, against btree, GIN, BRIN and hash indexes. Identical results on both: three btree indexes measured, the non-btree ones never reaching the function,avg_leaf_density~90 on freshly built indexes as the type header claims. This matters because CI never parses a collector query — theDarling PostgreSQL testsjob exercises the store, not a target — sosum(...) FILTER (...) OVER (...)had no other route to being proven to parse.The budget was then forced to bind, by substitution into that same shipped string, with a scenario built to discriminate the
FILTERrather than merely exercise it: with the ceiling lowered so the largest index is over it, the two smaller indexes are both measured, which is only possible if the over-ceiling one was not charged. The negative control — the identical probe with theFILTERremoved and nothing else changed — produces the predicted wrong answer, skipping both and measuring nothing.CI, and one failure that is not this branch
First run on
4bbddf6:Lite.Tests3,271 tests, 0 failed, 0 skipped andDashboard.Tests782, 0 failed both green.Darling.Testsfailed on the two source pins above, now fixed;Darling PostgreSQL testswent green at 236s onbbb2453once they were, which is the same suite plus a live store.The new tests are confirmed to have executed, by count rather than by name. The last green
devrun (33956294976) reportsLite.Tests3,266 andDarling.Tests7,490; this branch reports 3,271 and 7,496 — exactly +5 and +6, which is exactly what was added, withSkipped: 0on Lite and no new entry in Darling's skip list. A name grep is not available here: under Microsoft.Testing.Platform the runner prints no per-testPassedlines, so the delta plus the skip counts is the evidence.The live-store job additionally failed
QueryStoreCorrectedRollupLiveTests.RetentionSweep_TreatsAFailedProbeAsUnknown_NeverAsACoverageRegressionwithInvalidOperationException: Connection is not openfromIsArmedAsync. That is not this branch: nothing here touches the Query Store rollup or its retention sweep, the failure is a dropped store connection rather than an assertion, and the same test does not fail in the Windowsbuildjob, where it is skipped for want of a live store. Treated as a flake pending the re-run on this head; if it reproduces it belongs to whoever owns that sweep, not to this change.Not verified
The pgmon store could not be read this session (no pgmon MCP in this session's tool set, and AWS CLI calls were blocked in this environment), so the eleven rows are taken from the issue rather than re-read, and the
duration_ms = 0claim is established from source rather than from the rows. Nothing here has run against the affected target; the observation that decides is its next scheduled attempt.When the fix can first be observed
The affected target's next run tonight executes the currently-deployed build, not this branch. The deciding observation is the first daily run after the next nightly reaches the box. Tonight's expected failure is not evidence that this fix did not work.