Repository navigation
A PostgreSQL target learns its own normal: five baselines over the pg_* raw hypertables and five anomaly detectors whose facts score, escape the cap, corroborate, fold onto their parents and page through the shared machinery — so a 6× surge at 3 am is no longer indistinguishable from a quiet Tuesday (#3542 lane 9) - #3689
Merged
Merged
Conversation
…CTEs over the pg_* raw hypertables under the one RobustTierScaffold (TPS and deadlocks/hour off the reset-aware per-database difference, the capture's total_sessions, the Aurora all-types wait rate under the three-state interval with CPU excluded, percent of the configured ACU ceiling), five detectors through the shared AnomalyGate writing the same metadata the #3584 ramp and extremity escape read, a PostgreSQL ratio ramp with no sentinel for a first occurrence, the load-family co-fires confirmed by a capacity reading and never a raw one, PG_CPU_PERCENT graded only when the capacity percent was measured, and advice in PostgreSQL nouns (#3542 lane 9) Unmeasured floors and bars carry their lineage and every anomaly fact carries threshold_lineage = 0. Stock PostgreSQL's sampled wait estimate gets no anomaly baseline in v1.
This was referenced Sep 19, 2026
Closed
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…ch arms, censuses re-pinned (partial #3691) (#3715) Declares the v2 sources (pg_io, pg_replication, pg_bloat), keys and baseline metric names once; stubs the I/O, replication and bloat families in their own partials with "/* filled by lane N */" markers (collector, scorer, advice, graph) and the three v2 detectors / baseline arms (Io, Replication, Wal) as reachable inert partials; wires one delegating arm per shared switch; pre-fills the tool-recommendation rows. One behavioural item, the v1 residue from #3689 s5: ANOMALY_PG_WAIT_PROFILE now folds onto the wait card its dominant contributor names (standout then rollup) and can leave the 1.49 tuning-class cap on its own evidence through the same three readings its SQL Server twin has, anchored on its own firing multiple. Censuses re-pinned deliberately: collect surface 12 -> 15, KnownSources 26 -> 29 (both SKUs' descriptions), anomaly keys 5 -> 8, sources 11 -> 14, the FactScorer delegation count, the v2 whitelist tables by name.
This was referenced Sep 19, 2026
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?
PARTIAL for #3542 — lane 9 of v1, the last content lane. A PostgreSQL target's
analyze_serverpass now baselines five of its own metrics over its own 30 days of one-minute history and fires five anomaly detectors against those buckets — and, decisively, a PostgreSQL anomaly can now score, escape the tuning-class cap, corroborate its siblings, fold onto its regular parent, and page, through the same shared scorer, reconciler and notification machinery the SQL Server engine uses, without any of that machinery knowing which engine it is looking at.The lie this stops telling
Before this lane,
PgTargetBaselineProvideranswered null for every metric andPgTargetAnomalyDetectorreturned an empty list, so a PostgreSQL target had no notion of its own normal: a 6× throughput surge at 3 am, a connection pool that tripled, a serverless instance pinned at its configured ceiling for the first time in a month — none of it was distinguishable from a quiet Tuesday unless it also crossed an absolute bar some other family owned. And had the anomalies existed without this lane's scorer arms, #3584's exact defect would have shipped again for PostgreSQL: everyANOMALY_PG_*fact falls past the SQL Server literal lists, scores 0 unless it happens to carry aratio, and is never released from the 1.49 cap — the product's best per-server-calibrated statistics structurally its quietest.What each family concludes, and from which table
pg_tps→ANOMALY_PG_TPSpg_database_statsPG_TPSfact takes (LAGunderPARTITION BY database_name,GREATEST(raw, 0),ROW_NUMBER() > 1 AND stats_reset IS DISTINCT FROM LAG(stats_reset)— the NULL→timestamp first-reset trap that read records, not re-derived), summed and rated over the collection's own gappg_deadlock_rate→ANOMALY_PG_DEADLOCK_RATEpg_database_statsPG_DEADLOCK_RATE's unit and the ratio is unit-free; neverpg_deadlocksfor the rate (adversarial item C)pg_session_count→ANOMALY_PG_SESSION_SPIKEpg_session_statestotal_sessions(MAXpercollection_timeis a pick, not an aggregate — the V86 design); the exception-table caveat (quiet minutes absent; background processes counted) is stated on both sidespg_wait_ms_per_sec→ANOMALY_PG_WAIT_PROFILEpg_wait_stats(Aurora only)NULLIF, NULL →LAG, a restart collection is not a sample), CPU excluded exactly as the wait facts' share denominator excludes itpg_cpu→ANOMALY_PG_CPU_SPIKE, and thePG_CPU_PERCENTfactpg_cpu_utilization(Aurora only)acu_utilization_percent— percent of the CONFIGURED capacity ceiling — nevercpu_percent(#3281: on the serverless class the whole measured fleet runs,cpu_percentreads 100 whenever one core is busy for a minute while the instance sits at a third of its ceiling). The raw reading rides as "a core was pinned" and is never gradedEvery
cleanCTE ends in the ONERobustTierScaffold(pinned), reads>= $2 AND < $3with bounds the base class binds naive-UTC offcontext.TimeRangeStart(anchored, #2506 — noUtcNowanywhere in the provider or the detector), and the seam is the singleprotected override ResolveBaselineQuery— nothing of the base machinery is redeclared (pinned by reflection).D10, the retention dependency, stated and pinned: all four source tables carry 30-day service-side retention in
CollectorScheduleDefaultsand none is inTimescaleSupport.RawTierCoverage(the 4-day tier #1757 found under the SQL Server baselines), so the supply is exactly the window. What this does NOT cover is said in the provider's doc and reported below: retention is user-editable andDarlingRetention.BaselineServingRawCollectorsfloors the purge horizon only for the SQL Server raw-reading arms.The detectors
Same posture as
PgAnomalyDetector, verbatim where it could be: thepg_database_statsgate (already plumbed), each of the five detectors fenced in its owncatch … when (!AnalysisShutdown.IsExpectedAbandon(…)),CommandTimeout = AnalysisCommandTimeoutSecondson every command, the token on every store call. Three z-families throughAnomalyGate.EvaluateZScore(robust modified-z at 3.5 when the bucket carries median/MAD, classical 2σ otherwise; magnitude floor AND-ed on the trusted path; the strictly-higher absolute bar on an untrustworthy one — never AND-ed into blindness), writing key-for-key the metadata the shared ramp and extremity escape read:deviation_sigma,fire_threshold,baseline_low_quality,fallback_exceedance,baseline_samples,confidence, the tier and median/MAD. Two ratio families (deadlock rate; the Aurora wait profile onmodified_zwhen robust, ratio otherwise — the statistic the SQL Server profile fact lands on, so it is registered inIsPgRatioAnomalyKey, not as deviation-scored).The deadlock rate divides by
context.ObservedDurationMs(#3538 A7), never the nominal window, and is stamped with the database that deadlocked most — as the regular fact is — because the reconciler folds on(family, database)and a server-scoped anomaly would never meet a database-scoped parent.A first occurrence is not a ratio. An untrustworthy bucket fires on the absolute bar with
is_new = 1,fallback_exceedance = rate ÷ barand no sentinel ratio (the SQL ServerNoBaselineRatio = 100device is not reused);ScoreRatioAnomalygrades the exceedance (0.5 at the bar, 1.0 at 2×) and the advice renders "first occurrence, no baseline yet", never "100× its baseline".Known, deferred residue, copied rather than fixed (#3538 A8): the window statistic is the PEAK per-collection value and the bucket is a per-sample distribution, so "Nσ" is biased high in proportion to log k. Same on both engines; correcting it is a cross-engine change to the gate and its calibration. Stated in the detector's doc so the shape is not mistaken for a per-sample one.
The lineage shape of every bar
No PostgreSQL bar is a SQL Server constant reused by value (pinned:
PgTpsFloor ≠ BatchRequestFloor,PgSessionCountFloor ≠ SessionCountFloor, … each pair). Every number:threshold_lineage = 0stamped on every fact:PgTpsFloor 50 / PgTpsFallback 500tx/s;PgSessionCountFloor 20 / PgSessionCountFallback 200(COUNTS — a fraction ofmax_connectionsis meaningless as a constant, the detector cannot see the config fact, and the ceiling-aware arm isPG_CONNECTION_SATURATION, which this anomaly folds into and which is the never-blind arm on a young store);PgCpuFloorPct 40;PgWaitProfileFallbackMsPerSec 500(half a backend continuously waiting);PgDeadlockRateFloorPerHour 1;PgRatioAnomalyThreshold 3(the same multiple the SQL Server event-ratio detector uses, chosen on its own argument and not an alias so the two can calibrate apart); the ratio saturation at 3× the multiple; the wait profile's modified-z span (10σ past the heavy-tail cutoff, which is used by reference as the shared robust-statistic cutoff); the+0.3co-fire boost (derived from the 1.5 notify floor: one corroborator 1.3, two 1.6 — Extreme, corroborated anomaly findings can now cross the notify floor, so the baseline engine is no longer notification-inert at shipped settings (Fixes #3526) #3584's rule).PgDeadlockRateFallbackPerHour = PgTargetScorer.DeadlockWarnPerHour(p99.94 over 14 days × 43 servers, A single resolved deadlock bands a server Critical: DeadlockSeverity is count > 0 with no magnitude tier, so Critical stops discriminating on any OLTP fleet #3368) — a young store's deadlock anomaly fires exactly when the regular fact does and folds into it.CpuCapacityWarningPercent 80 / CpuCapacityCriticalPercent 95andPgCpuFallbackPct 95equalServerHealthThresholds.CpuWarningPercent / CpuCriticalPercent(this assembly references nothing;PgTargetAnomalyTestspins each pair), so the card, the High CPU alert and this pass cannot disagree about the colour of one minute — and still marked unmeasured, because that ladder is stated against a quantity, not read off a distribution.PgTargetThresholdLineageTestsscans everyPgTargetScorer*.csbar (executed locally, green); a new pin applies the same rule to everyPg*constant inAnomalyThresholds.The co-fire gating
PgTargetScorer.Anomaly.cs: the LOAD family (TPS, session spike, CPU spike) corroborate one another (+0.3 each, self-key omitted) and are confirmed by the MEASUREDPG_CPU_PERCENTat the capacity warning bar — the confirmer readscapacity_measuredfirst, so a raw percent-of-allocated reading of 100 confirms nothing (pinned). On stock PostgreSQL there is no CPU fact and the arm is inert; the advice never implies CPU was checked (D6). The wait profile takes the load arms plus "a namedPG_WAIT_*standout crossed its own threshold". The deadlock rate has no arm — its impact lives in the never-cappedPG_DEADLOCK_RATEit folds into. The ratio families never escape the cap (not graded in sigmas the multiple was calibrated for).PgTargetScorer.Cpu.cs:PG_CPU_PERCENTgrades only whencapacity_measured = 1, zero below the warning bar (the session family's self-gating shape —ApplyThresholdFormulawould read a 40% instance as 0.25 and lane 3's "CPU fired" amplifier asksBaseSeverity > 0), 0.5 at 80, 1.0 at 95. A fact with no capacity sample scores 0 whatever its raw percent — "Unknown, never Healthy", the card's #3271 rule — and the collector (PgTargetFactCollector.Cpu.cs, one read,FleetCpuProvenance.CpuBandInputPercentpicks WHICH percentage; it calls neitherServerHealthClassifier.CpuSeveritynor builds aServerHealthMetrics, so theFleetCardPostgresCpuTestsdecider census is untouched) says why.Advice
The SQL Server anomaly composer's shape, engine-neutral, with PostgreSQL lineage in every noun: "Transactions per second reached 1,234/sec this window, 6.2σ above its 280/sec baseline median for this hour-of-week (over 120 baseline samples)" + the hedge. A low-quality z-fact and an
is_newratio-fact print no sigma and no multiple. The wait profile says "the engine measured … CPU excluded", never "spent", never "estimated from sampling" (it exists only for the Aurora series). The CPU anomaly's number is "% of the configured capacity ceiling" with the raw reading beside it as "a core was pinned".PG_CPU_PERCENT's own block states peak/avg capacity, the ACUs at the peak, both levers with their counter-objectives (raise the ceiling: pays every hour and hides the query; fix the query: durable, takes finding it), and "not graded" when no capacity sample exists.What it does NOT do
pg_wait_samplingestimate is per-backend-sample, quantised atprofile_period, with a different noise distribution from a measured microsecond sum; a bucket that mixed the two, or a bar tuned on one applied to the other, would be the unit error adversarial item A names. When a sampled arm arrives it is its own metric name and its own bar — so the brief'sPgSampledWaitProfileFallbackMsPerSecis deliberately NOT declared tonight (a constant with no user is dead code, and declaring it would imply the arm exists).SetDeviationThresholdknob on the PostgreSQL detector (defaults over config).ANOMALY_WAIT_PROFILEandPgTargetFactKeys.AnomalyToFamilieshas no entry for it — both shared files, outside this lane (reported).AnomalyThresholds/BaselineMath.AbsStdDevFloorForgivespg_cpuno absolute dispersion floor (the SQL Server CPU has 5.0); out of this lane's files, reported. The 25σ display cap and the 40% magnitude floor hold a variance-collapsed bucket sane meanwhile.v1's exit criterion — is it met?
The plan's exit criterion: a 30-day planted series → z anomalies fold into lanes 2/3/5's parents;
analyze_serveron a PG target under planted load returns scored, storied, advice-bearing findings — persists, mutes, occurs, and can notify through the existing machinery. Met, and executed rather than asserted, on a throwawaytimescale/timescaledb:2.28.1-pg18store against the exact test that ships in this PR: 31 days of one-minutepg_database_statsandpg_session_statesand five-minutepg_cpu_utilizationon anaurora-postgres-stamped server, steady for 30 days, spiking in the last four hours → the REALanalyze_serverreturnedANOMALY_PG_TPSat the 25σ display cap corroborated by the session and CPU anomalies and the measured confirmer, severity 1.9 — above the 1.5 notify floor, the lone-anomaly cap released by the extremity escape;ANOMALY_PG_CPU_SPIKEsharingPG_CPU_PERCENT'sincident_id; the first-occurrenceANOMALY_PG_DEADLOCK_RATE(24 deadlocks over 4 observed hours, 6/h ≥ the measured 5/h tier) sharingPG_DEADLOCK_RATE's; advice in PostgreSQL nouns andget_pg_*next-reads. The persist/mute/occurrence/notify path is the one pass body both engines share (plumbing #3665), so a finding that reachesanalyze_serverreaches it. One caveat to state plainly:get_analysis_factsdoes not run the anomaly detector on either engine (CollectAndScoreFactsAsyncis collector + scorer only), sothreshold_lineage = 0on anomaly facts is visible in the finding, not in that read — pre-existing, reported.Which component(s) does this affect?
How was this tested?
New:
Darling/Darling.Tests/PgTargetAnomalyTests.cs(39 tests, 1 gated live). Every CTE's table and shape; the D10 supply/retention pin; the detector SQL dialect / FROM-JOIN census / observed-time divisor / per-detector fence / anchored gate / A8 statement; deviation-vs-ratio membership (everyANOMALY_PG_*in exactly one, the wait profile ratio); the ratio ramp's values (2.9→0, 3→0.5, 6→0.75, 9→1.0), the first-occurrence exceedance ramp, the wait profile's modified-z ramp; the extremity escape reachable —ANOMALY_PG_SESSION_SPIKEat 10.5σ/3.5 with two co-fires = 1.6, not capped; alone 1.0; a 7σ one with the same co-fires = 1.49; the 25σ cap bound; the low-quality path at 3× its bar; the ratio families never escaping;ScoreCpuFact's values and the threeServerHealthThresholdsequalities; the load confirmer refusing a raw 100; everyPg*constant's lineage marker and the no-reuse-by-value pairs; the advice pins; the CPU read's shape and its non-participation in the decider census; the gated e2e above.Edited:
PgTargetSharedSwitchRoutingTests— two pins moved deliberately, as their own comments said they would when lane 9 landed (the ratio-routing pin now uses a ratio the two ramps grade DIFFERENTLY, 6× → 0.75 PostgreSQL vs 0.714 SQL Server; theANOMALY_PG_WAIT_PROFILEadvice pin moves from null to "the PostgreSQL block, and not the SQL Server one").PgTargetSessionsTests— the "CPU is a stub" comment falsified by this lane is corrected, and the test now also drives a capacity-measured 95% throughScoreAllto 1.5 (the D7 shape); lane 3's saturation-chain re-pin is dev's (#3686), untouched here.Executed locally, not just compiled, via a net10.0 harness with
AssemblyName=Darling.Testsagainst a throwawaytimescale/timescaledb:2.28.1-pg18on Docker contextdesktop-linux: 654 Darling tests green acrossPgTargetAnomalyTests,PgTargetThresholdLineageTests,PgTargetSharedSwitchRoutingTests,PgTargetSessionsTests,PgTargetFactCollectorTests,PgTargetWaitTests/LiveTests,PgTargetVacuumTests/LiveTests,PgTargetTempTests,PgTargetKnobsTests,PgTargetQueriesTests,PgTargetPosture*Tests,PgTargetMcpSurfaceTests,BaselineTierJoinShapeTests,BaselineSupplyTests,SharedBaselineModelPinTests,DarlingAnomalyBaselineTests,StoreSqlClockDisciplineTests,FactSourceRegistryTests,FactCollector*Tests,AnalysisPass*Tests,AsOfWindowAnchorTests,DarlingAnalysisServiceEngineRoutingTests,RobustBaselineTests,BaselineTimeoutIsNamedTests, plusDocCommentHygieneTests,RepoFileAdoptionTests,CommentFilterAdoptionTests,LiveCleanupConversionRatchetTests,LivePostgresCollectionHygieneTestsrun in-tree (109 green). The harness caught three miscounts in my own first draft (an inclusivegenerate_seriesbound, theAnomalyPrefixconstant in a reflection sweep, the wait profile's fifth amplifier) — each was the test, not the product. CI is the first Windows execution. Rig torn down.Builds, zero new warnings (
-p:EnableWindowsTargeting=true):PerformanceMonitor.Analysis,PerformanceMonitor.Darling.Analysis,PerformanceMonitor.Darling.Service,Darling.Tests,Lite.Tests(one pre-existing xUnit2000 inLogTailOverlapThresholdPinTests, untouched),deprecated/Dashboard.Tests.Checklist
dotnet build -c Debug)PgTargetSharedSwitchRoutingTests.SqlServerAnomalies_AreUnaffected_ByThePgMembershipArm)