Repository navigation
PostgreSQL I/O family: an operator now learns how slow each data-file read was, on measured Aurora-storage bars, and hears why when the pass cannot know — timing off or a pre-16 major (partial #3691) - #3726
Merged
Conversation
…aded on measured Aurora-storage bars, honest when timing is off or the major is too old (partial #3691) Fills lane 11's stubs: PgTargetFactCollector.Io (reset-aware per-identity differencing of pg_io_stats, the trackedness three-way), PgTargetScorer.Io (10 / 30 ms measured bars with the population named, the 1,000-op floor, three corroborators), PgTargetAdvice.Io (value-stated, Aurora vs stock lever), PgTargetRelationshipGraph.Io (IO waits -> latency -> cache pressure / planner knobs as context), PgTargetAnomalyDetector.Io + PgTargetBaselineProvider.Io (hourly ms/read, hour-of-day tier in practice), the lane-11 AnomalyThresholds block (2 ms floor, 20 ms fallback, measured). One delegating case in PgTargetAdvice.Anomaly.cs routes ANOMALY_PG_IO_LATENCY to its family's composer. Census pins moved deliberately in PgTargetAnomalyTests and PgTargetSharedSwitchRoutingTests; PgTargetIoTests is new.
This was referenced Sep 19, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… PRs across the #3653, #3542 and #3691 waves, #3704, #3710, #3712 and #3735, spliced once (#3751) Sixty-five PRs merged to dev since #3672 (the first splice) across two seats - the coordinator's #3653 brains-review residue lanes and the wave-2 worker's #3542 / #3691 PostgreSQL-target engine lanes - and by rule none touched this file; each returned its entry to a buffer. This is the one splice. Text-only; one file; nothing outside ## [Unreleased] and the definition block moves. [Unreleased] gains a ### Changed between ### Added and ### Fixed (Keep-a-Changelog order) for the nine entries whose subject is a designed behaviour that now behaves differently - routing (#3712 confidence chooses the channel), gating (#3692 tempdb persistence, #3734 blocking/LRQ gates, #3724 peak-and-mean), the wire shape (#3719 one error envelope), the review guard's drift arm (#3673), lineage metadata (#3723) and two grading changes (#3693, #3747). Twenty-two new capabilities append to ### Added (the sixteen PostgreSQL-target fact families and rungs, CONFIG_CHANGED, the recurrence label, the V134/V135 columns, the review traps file, the measurement census); thirty-two defects append to ### Fixed. Order within each section is ascending PR number, as #3672 did. Every entry is one line and verbatim from its buffer (trailing whitespace only). Four PR pairs fold into one entry as the buffers wrote them (#3738+#3746, #3683+#3686, #3681+#3685, #3726+#3729); #3695 carries three entries. #3673 has no issue and cites none, like the file's ninety-six other ref-less entries. Five definitions added ([#3691], [#3704], [#3710], [#3712], [#3735]) at the end of the [Unreleased] run, which stays one ascending run (3517..3735). No archive touched; the split was not run. tools/changelog/changelog_archive.py verify: all PASS (1,483 bold entries, floor 1,329; 1,363 distinct refs resolve; 1,372 definitions; CRLF throughout; 409,263 bytes under the ceiling). Client-hygiene grep over the file: zero hits.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Headline mechanism. A PostgreSQL operator now learns how slow each data-file read was — milliseconds per read over the window, from
pg_stat_io(pg_io_stats, PostgreSQL 16+) — graded on bars measured on the dogfood fleet's Aurora storage, and, when the pass cannot know, exactly why it cannot:track_io_timingis off (reads were counted, none were timed), or the major has nopg_stat_ioat all. Lane 11 of #3691 fills every stub the v2 plumbing (#3715) declared for thepg_iofamily and adds its anomaly. PARTIAL for #3691 — checklist line:IO latency facts + anomaly. Do not close.What the family concludes, and from which table
PG_IO_READ_LATENCY_MS—Fact.Valueis Δread_time_ms÷ Δreadsover the window, frompg_io_stats(object_type = 'relation', everybackend_type×context), the reset-aware per-identityLAGdifference ofPgTargetFactCollector.Write.cs/ the Buffer partial's eviction CTE (GREATEST(raw, 0),ROW_NUMBER() > 1 AND stats_reset IS DISTINCT FROM LAG(stats_reset); the NULL→timestamp first-reset trap thatDarlingPgDatabaseReader.PgDatabaseSqlrecords is not re-derived). Summed over identities first, then divided in C# — so "0 ÷ 0" and "0 ÷ N" stay different shapes. A per-operation figure needs no time divisor, which is why it is the value (#3538 A7);ops_per_secdivides byObservedDurationMs, never the nominal window.The trackedness three-way, decided in order (the #3541 A8 lesson: never 0.000 ms).
PG_SERVER_MAJOR_VERSIONfact): the read fact is emittedunavailable = 1,reason_pg_stat_io_absent = 1, no store read. A registry with NO major is not "old" — the rows decide.reads IS NULLthroughout): no fact — structurally absent is the D6 disclosure's job.track_io_timingis off →unavailable = 1,reason_track_io_timing_off = 1,Value = 0as the placeholder of a fact that makes no claim; the scorer refuses it, the advice renders the count of untimed reads and points at theCONFIG_PG_TRACK_IO_TIMINGcard (lane 2's — no duplicate). The knob's value, when the config snapshot was collected, rides beside the data-side decision astrack_io_timing_config— corroboration, never the decision (the knob is reloadable and a window can straddle the flip; the advice says which case it sees).latency_measured = 1), graded only when the window's operations clear the floor (insufficient_opsotherwise — stated, not graded).PG_IO_WRITE_LATENCY_MS— same flags, same quotient, emitted only when the engine reported a write counter at all (on Aurora the write side ofpg_stat_iois NULL — backends do not write data files there — so the fact is absent, not zero). Never graded in v2: the measured population does not report it and no stock bar has been read; a bar here would be an unmeasured number wearing a lineage comment. Context for the checkpoint story; no graph edge (an edge into a base-0 fact could never fire).ANOMALY_PG_IO_LATENCY— the window's peak HOURLY ms per read (hours under the floor not rated) against thepg_io_read_latencybucket through the sharedAnomalyGate, the z-score shape lane 9's session detector takes; registered deviation-scored by the plumbing; folds onto the read fact throughAnomalyToFamilies. No amplifier arm of its own — the impact lives in the parent, which is never capped.The lineage shape of every bar
IoReadLatencyWarningMsIoReadLatencyCriticalMsIoMinimumOps1000is pinned equal to the constant byPgTargetIoTests)IoStatIoMinimumMajorpg_stat_ioarrived in 16PgIoLatencyFloorMsPgIoLatencyFallbackMsIoCorroborationBoostGraded facts and fired anomalies carry
threshold_lineage = 1— the bars ARE measured; the honesty is in naming the population. Every scorer comment, the advice and this PR say it: these are Aurora-storage shapes; stock local-disk PostgreSQL is a different population (local NVMe reads under 1 ms; the advice tells a stock operator to treat the warning line as generous and read the number against the volume class). Self-gating below the warning bar (the session / CPU families' shape): 0 under 10 ms, 0.5 at it, 1.0 at 30 — a fired latency fact always means slow storage by the measured population's own standard.The co-fire gating
IoAmplifierson the read fact only — three corroborators at 0.3 each, every predicate reading a sibling'sBaseSeverity(neverSeverity, lane 4's finding): the anomaly co-fired (unusual for THIS storage, not its routine); an IO wait crossed its own bar (theIOtype rollup OR any namedIO:standout —PgTargetScorer.Waits.csgrades one wait once, so exactly one of them is the fired one);PG_BUFFER_CACHE_PRESSUREfired (the cache is sending the reads). Executed: warning alone 0.5; + anomaly 0.65; all three 0.95; critical + all three 1.9 — one condition, one story, past the notify line without touching the cap.Graph (
BuildIoEdges, categoryio_latency):PG_WAIT_IO_DATAFILEREAD → PG_IO_READ_LATENCY_MS(these waits are long because each read is slow, not only because there are many) — the standout only, never theIOtype rollup: lane 5's rule that a rollup roots nothing of its own (PgTargetWaitTestspinsGetAllEdges(PG_WAIT_IO)empty; the first CI run caught a rollup edge here and it was removed, the rollup staying a corroborator on the amplifiers, which is a predicate and not a story edge);PG_IO_READ_LATENCY_MS → PG_BUFFER_CACHE_PRESSURE(on toshared_buffersthrough the memory chain);PG_IO_READ_LATENCY_MS → CONFIG_PG_EFFECTIVE_CACHE_SIZE/→ CONFIG_PG_RANDOM_PAGE_COSTas context — each fires on the knob's own engine-defined verdict, and neither knob is amplified by latency (D5 holds: the convention card stays a 0.4 leaf of the latency story; latency alone never lifts a knob past the incident line). Predicates read verdicts, never a bar of their own.Baseline (
pg_io_read_latency): hourly ms/read (per-identityMAX(counter)per hour, hour-over-hour difference clamped at 0, summed, floor applied), one CTE chain into the ONERobustTierScaffold. Stated consequence: one hourly row per hour-of-week bucket per week is 4–5 samples in 30 days, underBaselineMath.RestoreThreshold(15), soSelectBucketcollapses to the hour-of-day tier (confidence 0.85) on every pass — the live test pinsTier == HourOnly, and the arm's comment names the alternative (a 15-minute bucket with a 250-read floor would put ~17 samples in each hour-of-week bucket) as a coordinator's call, not this lane's.Advice
Value-stated, one rendering per trackedness shape, none printing a latency it does not have: "PostgreSQL 15 has no pg_stat_io (16+)" with what that major offers instead (
blk_read_time); "50,000 reads counted over the 4 observed hours, none timed (track_io_timing is off)" with the knob's counter-objective (one clock read per I/O —pg_test_timingfirst); "45.0 ms per operation over only 12 reads — under the floor, not graded"; and the measured block: "Data-file reads averaged 18.4 ms per operation over the 4 observed hours against a 1.3 ms routine for this hour of the week (Aurora storage tier)" — the routine clause off the co-fired anomaly's baseline median. The Aurora / stock split is in the lever: on Aurora there is no disk to swap (working set,shared_buffers, the query); on stock, compare with what the volume class advertises before touching PostgreSQL.fsync/synchronous_commit/full_page_writesare named as posture, never a lever; no DDL.next_toolsare the plumbing's rows (get_pg_io_stats,get_pg_io_trend,get_pg_server_config); the prose namesget_pg_top_queries(byshared_blks_read) andget_pg_buffer_usage, both verified against the tool inventory.Boundary extensions (each one line; the coordinator should read these first)
PerformanceMonitor.Analysis/PgTargetAdvice.Anomaly.cs(lane 9's file): ONE delegatingcase PgTargetFactKeys.AnomalyIoLatency: return ComposeIoLatencyAnomaly(factsByKey);plus the doc line it falsified ("outside the five" → six). The shared prefix routing sends everyANOMALY_PG_key intoComposeAnomaly, whosedefaultisnull, so without this arm the anomaly card ships with no advice — the exit criterion ("advice in PostgreSQL nouns") is unreachable from the lane's files. The composer itself lives inPgTargetAdvice.Io.cs. The plumbing left no hook here; reported so the next family lanes get one.Darling/Darling.Tests/PgTargetAnomalyTests.cs(census pins the plumbing said each lane moves):pg_io_read_latencyjoins the six served metrics and the (metric, table) census, leaves the inert-stub loop;IoLatencyWindowSqljoins the detector-SQL closure; the twoPg*constants join theAnomalyThresholdsroster with the floor < fallback pin.Darling/Darling.Tests/PgTargetSharedSwitchRoutingTests.cs: the lane-11 delegation line movesNull → NotNull(both keys), and the anomaly is pinned to compose through the PostgreSQL block and never the SQL Server "Anomalous spike" composer.AnomalyThresholds.cs: the// lane 11block at the END of the PG section, as the STEP brief assigns.What it does NOT do
No write-latency bar (stated above). No change to
PgTargetScorer.Anomaly.cs's amplifier table —ANOMALY_PG_IO_LATENCYhas no corroborators of its own by design. No stock-PostgreSQL population (Aurora shapes, named as such). No change to the root partials,FactScorer,FactAdvice,InferenceEngine, the reconciler,PgTargetFactKeys, orPgTargetFactCollectorTests. No rung (StorageVersionat V132). The hour-of-day tier consequence is stated, not fixed. The stock-target 1.30 anomaly ceiling stays #3691's v3 item. A SQL Server target's pass is byte-identical by construction: every new arm is behind apg_source or aPG_/ANOMALY_PG_key, and the routing pins that proved v1's byte-identity pass unchanged.Test plan
New:
Darling/Darling.Tests/PgTargetIoTests.cs(19 tests, 1 gated live). Scorer: the self-gating ramp at 9.99 / 10 / 20 / 25 / 30 / 45 ms with the measured stamp; under-floor stated-not-graded with the stamp; unavailable (either reason) as context with NO stamp andValue0; the write fact never graded, no amplifiers. Amplifiers throughFactScorer.ScoreAllwith sibling fixtures shaped so the SHARED scorer grades them (a wait at its own concerning bar, the cache composite's hit-ratio arm at its bar, a knob at the shipped default): 0.5 / 0.65 (each of the three) / 0.95 / 1.9; a Lock wait does not count. The anomaly: deviation-scored,AnomalyToFamiliesfold, 1.0 saturated with no amplifier results, 0.5 at the cutoff, the exceedance ramp between the bar and saturation. Constants: floor < warning < fallback < critical, major 16, the SQL literal equalsIoMinimumOps, the population named where the lineage census reads. The three reads by text (per-identity partition, reset shape, relation filter,IS NOT NULLtrackedness, no quotient in SQL; hourly CTEs shared between baseline and detector; scaffold suffix; fences, token,ObservedDurationMs, lineage stamp). Graph: quiet set has no active edge; fired set walks the standout edge in and both onward edges; the IO rollup, the write fact and the anomaly have no edges; exactly three edges out of the read fact. Advice: every trackedness rendering by exact string, never "0 ms" on the untimed shape, Aurora vs stock, the routine clause, the anomaly's headline / investigation / first-occurrence / static, never "Anomalous spike".The gated exit criterion (
ThirtyOneDaysAtOnePointFiveMs_ThenFourHoursAtTwentyFive_…): 31 days of one-minutepg_io_statsfor an Aurora 17 cluster — two reading identities at 1.3–1.7 ms cycling by the hour, a checkpointer with NULLreads, writes NULL everywhere, astats_resetmid-history — then 250 minutes at 25 ms; a PG 15 server with no I/O rows; a PG 18 server whose reads climb withread_time_mspinned at 0. Asserts the collector's read fact at exactly 25.0 ms / 26,400 reads / 2 identities / no write fact; the absent and untimed shapes on the other two servers (24,000 untimed reads,Value0); the bucket trustworthy at the hour-of-day tier; the detector at the display cap withthreshold_lineage = 1; and through the REALanalyze_serveranchored at the window's end:PG_IO_READ_LATENCY_MSrooting apg_iofinding ≥ 1.1375 (0.875 × 1.3, the anomaly co-fire),ANOMALY_PG_IO_LATENCYsharing itsincident_id, both advices in PostgreSQL nouns, everynext_toolsentryget_pg_*,get_analysis_factsshowing the one fact with its measured lineage.Executed here, not only compiled: a net10.0 harness (
AssemblyName=Darling.Tests, the 36-file PgTarget census set + this file) against a throwawaytimescale/timescaledb:2.28.1-pg18on Docker contextdesktop-linux, migrated to V132 by the fixture, torn down after — 441 / 442 green, the one red being the harness csproj trippingDocCommentHygieneTests.TheDerivedProjectListCoversEveryProjectInTheTree(the artifact #3665 and #3715 recorded). That includes everyDARLING_TEST_PGe2e in the set (lane 9's, the plumbing's, lanes 6/7's) and this lane's. The three SQL texts were additionally run raw against a planted table with a mid-window reset, a NULL-readsidentity and atemp relationidentity (excluded), and the sums checked by hand (32,900 reads / 42,770 ms = 1.3 ms; the reset interval dropped; 1 reset counted). Builds, zero warnings of ours:PerformanceMonitor.Analysis,PerformanceMonitor.Darling.Analysis,PerformanceMonitor.Darling.Service,Darling.Tests,Lite.Tests(the one xUnit2000 inLogTailOverlapThresholdPinTests.cs:95is pre-existing on dev),deprecated/Dashboard.Tests. First execution of the rest of the repo's censuses is CI's Windows job. No production store or fleet server was read; the bars come from the coordinator's published calibration file.Which component(s) does this affect?
How was this tested?
Above. Throwaway container only; no production read.
Checklist
dotnet build -c Debug)Partial for #3691 — IO latency facts + anomaly. CHANGELOG deliberately untouched — the coordinator splices this wave's entries.