Repository navigation
PostgreSQL targets key their hour-of-week buckets on their own TimeZone setting, so a New York target's Tue 09:00 bucket is 09:00 in New York in every season — the clock read becomes the provider's second seam, UTC becomes the fallback instead of the rule (partial #3691, lane 20) - #3757
Merged
Merged
Conversation
…ne setting, so a New York target's Tuesday-09:00 bucket is 09:00 in New York in every season (partial #3691, lane 20) The clock read in PgBaselineProvider becomes a protected virtual seam (SQL Server body unchanged, byte for byte); PgTargetBaselineProvider overrides it once to read the TimeZone row of the latest pg_server_config snapshot at or before the window end, session-scoped sources excluded (the config facts' rule, copied), and hands the shared resolver the zone name. UTC is now the fallback for a target with no server-scoped snapshot, not the rule for the engine. Doc riders: the root doc names the $4..$6 bind the base has made since #3749 and states the setting-then-UTC rule; the TpsTrendKey prose in PgTargetScorer.Database.cs and PgTargetFactCollector.Database.cs no longer claims a co-fire reads the trend (nothing has since #3747).
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.
Partial #3691 — lane 20. Checklist line completed, verbatim: PostgreSQL-target buckets stay UTC-keyed after Q6 (#3749) (and the root-doc line beneath it).
What a PostgreSQL operator now learns
That the "Tue 09:00" bucket a finding names on a New York PostgreSQL target is 09:00 in New York, in February and in July alike. Before this PR it was 09:00 UTC — 04:00 on the server in winter and 05:00 in summer — and, worse, one bucket pooled two different local hours across a DST change: the exact smear #3749 (Q6) fixed for SQL Server targets, still live on the other engine.
The gap this closes (#3749's own out-of-lane note)
Q6 re-keyed every hour-of-week bucket to the target's local clock through one shared expression (
BaselineLocalClock.LocalCollectionTimeSql) and three bound parameters ($4..$6: the one offset transition inside the window and the offset minutes either side of it). The clock those parameters come from was read from the newestserver_propertiesrow — a table only the SQL Server collectors write. A PostgreSQL target has no such row, so the read answered(NULL, NULL),BaselineLocalClock.Resolvefell to its UTC arm, and everyPgTargetBaselineProviderbucket stayed UTC hour-of-week by construction. #3749's body says so in its "does NOT do" list and reports it as the finding this PR takes.A PostgreSQL target's clock is collected:
pg_server_configis the hourly snapshot of itspg_settings, andTimeZoneis one of its rows — the IANA name the server keeps its clock in (UTC,America/New_York, …), withsourcesaying where it came from. That name is the input the resolver already prefers over a fixed offset, because a zone id knows when the offset changes, which is the whole of the DST fix.The mechanism
One new seam, one override, nothing else redeclared.
PgBaselineProvider.ReadServerClockAsyncwas aprivate static; it is nowprotected virtual, with the SQL Server body byte-identical (the sameServerClockSql, the same reader —LocalClockBucketKeyTests.TheClockRead_IsTheStoresIdiom…still pins the text and Lite's twin of it). Its signature gains the window end (AsNaive(analysisTime)) so an override can anchor on the window an anchored pass (#2506) asked for; the base ignores it, because the SQL Server clock is a property of the host, not of the window.PgTargetBaselineProvider(new partialPgTargetBaselineProvider.Clock.cs) overrides it withPgTargetClockSql: theTimeZonerow of the latestpg_server_configsnapshot at or before the window end, session-scoped sources excluded —PgTargetFactCollector.PgTargetConfigSnapshotSql's rule, copied token for token (the test pins the shared anchor and exclusion against the collector's text), not re-derived. It returns(null, setting);Resolveturns the zone into the step function, the base binds$4..$6, caches theLocalClockWindowbeside the buckets, andGetBaselineAsynclooks the analysis time up through the same three numbers. The two engines therefore share how a clock becomes a bucket key and differ only in where the clock is read.UTCresolves to offset 0 — byte-for-byte the pre-PR keying — so a target that keeps UTC changes nothing, which is most of the measured population.Why the session-scope exclusion matters more here than for the config facts.
TimeZoneis a setting a client can and routinely does set for its own session (libpq'sPGTZ, a driver's connection-string option), so a snapshot whoseTimeZonerow issource IN ('client','session','override')describes the collector's connection, not the server. Such a snapshot yields no row and that compute keys on UTC — honest, not wrong: the server's clock is unknown from that snapshot, and an older snapshot's value is deliberately not taken in its place, because the rule is "the snapshot that applied", not "any snapshot that had an answer". No snapshot at all (config collector off; a store older than V102) is the same(NULL, NULL)the base gives a row-less SQL Server target, and the same UTC keying.UTC was the RULE for this engine; it is now the FALLBACK. The root doc of
PgTargetBaselineProvidersays so in those words: "PostgreSQL targets resolve their clock from theTimeZonesetting in the latestpg_server_configsnapshot at or before the window end; UTC only when no snapshot carries a server-scoped one." The base's own doc no longer describes the row-less arm as the PostgreSQL target's fate.Why this shape, and what was rejected
TimeZonerather than an offset.pg_settingshas no offset column; a zone name is what the server has, and it is the resolver's preferred input. IANA names resolve natively on Linux/macOS and through ICU on Windows (Hour-of-week baselines key on the target's local clock, not UTC: one shared step-function expression re-keys every arm through the scaffold, three bound clock parameters from server_properties (zone id, offset fallback, UTC when there is no row), the lookup through the same numbers — nothing keyed is stored, so re-bucketing is the next compute (#3653 item 12, Q6) #3749 measured both, and its fixed-offset fallback with the one-per-zone Information note is what an unresolvable name would get — there is no fixed offset to fall back to here, so it would be UTC with the note).PgTargetClockTests.ThePostgresProvider_RedeclaresExactlyTheTwoSeams_AndNoStatepins exactly two overrides (ResolveBaselineQuery,ReadServerClockAsync), no instance fields, and that the Clock partial never namesBaselineLocalClockorLocalClockWindow.server_propertiesfor PostgreSQL targets. That table describes SQL Server hosts; each engine's read names only its own collector's table (pinned).BaselineLocalClock.cs(the other seat's file, read-only for this lane) —Resolvealready takes exactly what the override hands it.Blast radius
LocalClockBucketKeyTests(13 + live) and Lite's 8 are unaffected — the census reads statements, and this PR adds none.CacheTtlon a target whoseTimeZoneis notUTCre-buckets to local time — the "re-bucket once" the Q6 ruling asks for, automatic because nothing keyed is stored. No migration rung, no aggregate change (ladder stays V132 landed).idx_pg_server_config_time) beside the 30-day scan, on the compute's own connection, inside its one classified catch — where the base puts its own clock read.AnalysisShutdownResidueTestsstill pins exactly one catch in the provider.What it does NOT do
baseline_hour/baseline_dowmetadata is the bucket's key, andBaselineContextFormatterprints it as the target-local hour with no edit — exactly as Hour-of-week baselines key on the target's local clock, not UTC: one shared step-function expression re-keys every arm through the scaffold, three bound clock parameters from server_properties (zone id, offset fallback, UTC when there is no row), the lookup through the same numbers — nothing keyed is stored, so re-bucketing is the next compute (#3653 item 12, Q6) #3749 left the SQL Server side. NoBaselineBucketcarries a zone and the resolvedLocalClockWindowlives in the provider's cache, so there is nothing for a detector to read without a shared-file change this lane does not make. Stated in the Clock partial's doc.TimeZone(argued above).BaselineLocalClock.cs,BaselineMath.cs,BaselineBucket.cs, any detector, or any arm's SQL.Doc riders (three, docs only)
PgTargetBaselineProvider.csroot: the base has bound$1..$3and$4..$6since Hour-of-week baselines key on the target's local clock, not UTC: one shared step-function expression re-keys every arm through the scaffold, three bound clock parameters from server_properties (zone id, offset fallback, UTC when there is no row), the lookup through the same numbers — nothing keyed is stored, so re-bucketing is the next compute (#3653 item 12, Q6) #3749 (new paragraph; theGetPgTargetBaselineQuerysummary says an arm's own text references only$1..$3and the scaffold appended to it reads the rest). The "two things swapped / two seams" framing replaces "one thing swapped".PerformanceMonitor.Analysis/PgTargetScorer.Database.cs(TpsTrendKey) andPgTargetFactCollector.Database.cs(three sites): the prose said lane 3's offered-vs-delivered co-fire readstps_trend. Nothing has since PostgreSQL sessions family: an operator now learns when a pool near its ceiling is accepting more connections than this hour usually carries while completing no more work — the offered-vs-delivered co-fire reads the anomaly pair, and the raw TPS-trend hook is retired on the calibration's evidence (partial #3691, lane 18) #3747 retired the raw-trend hook on the calibration's evidence; the trend is a reader's figure inPG_TPS's metadata. Reworded to that.PgTargetAnomalyTestscomment "one protected override" → points at the second seam's pin.Tests
New
Darling/Darling.Tests/PgTargetClockTests.cs— five pins plus one gated live class:TheClockRead_IsAProtectedVirtualSeam_OverriddenOnceByThePostgresProvider— reflection:protected virtual, the exact signature, the override'sGetBaseDefinition()is the seam; source: the compute calls the seam exactly once withAsNaive(analysisTime); the Clock partial never namesDateTime.UtcNow.EachEnginesClockRead_NamesOnlyItsOwnCollectorsTable— base body still executesServerClockSqloverserver_propertiesand neverpg_server_config; override executesPgTargetClockSqloverpg_server_configand neverserver_properties.TheOverridesRead_IsTheConfigFactsRule_…—FROM pg_server_config,c.name = 'TimeZone',NOT IN (+DarlingPgServerConfigReader.SessionScopedSources+),MAX(collection_time),collection_time <= $2,LIMIT 1, nonow()/CURRENT_TIMESTAMP, nov_view, no$3; and the anchor subquery + exclusion line are token-identical toPgTargetConfigSnapshotSql's.ThePostgresProvider_RedeclaresExactlyTheTwoSeams_AndNoState— exactly two overrides, zero instance fields, no resolver/window type in the partial,CommandTimeout = DarlingAnalysisService.AnalysisCommandTimeoutSeconds.TheRootDoc_StatesTheSettingAsTheRuleAndUtcAsTheFallback— the rider's sentence is present; the base doc no longer says "(a PostgreSQL target)" of the row-less arm.DARLING_TEST_PG,[Collection("live-postgres")],LiveStoreCleanup): two twins with identical 32-day one-minutepg_database_statsseries — 100 tps in the 09:00 hour New York time every weekday, 10 tps otherwise, across the 2026-03-08 spring-forward, the local hour computed in SQL with the same one-step function the provider binds. New York twin (snapshotTimeZone = America/New_York,source = configuration file; plus a later-than-window-end Tokyo snapshot that must be ignored): the realPgTargetBaselineProvider'spg_tpsbucket for Tue 2026-03-10 13:30Z (09:30 EDT) is (9, Tue), 270 samples over 5 local days, median = mean = 100, MAD 0. UTC twin (no snapshot): (13, Tue), 270 samples, median 10, mean 20 — the four EST Tuesdays' 08:xx idle rows pooled with the EDT Tuesday's 09:xx load rows. Then a newer in-window snapshot withTimeZone = Asia/Tokyo,source = session→ClearCache→ the New York twin keys (13, Tue), median 10: session scope excluded, and no fallback to the older snapshot. The UTC twin is the red-first half of the pair: it is exactly what the New York twin read before this PR.Executed here: the five pins and the live class green against a throwaway
timescale/timescaledb:2.28.1-pg18(Docker contextdesktop-linux, torn down), through a net10.0 harness inside the worktree (AssemblyName=Darling.Tests) — 244 tests acrossPgTargetClockTests,LocalClockBucketKeyTests(+ its live class, green on the same rig),PgTargetAnomalyTests,PgTargetFactCollectorTests,PgTargetKnobsTests,BaselineSupplyTests,RepoFileAdoptionTests,CommentFilterAdoptionTests,StoreSqlClockDisciplineTests,DocCommentHygieneTests,LiveCleanupConversionRatchetTests,LivePostgresCollectionHygieneTests, thenAnalysisPassCommandTimeoutTests,AnalysisPassTokenThreadingTests,AnalysisShutdownResidueTests,MeasurementContractCensusTests,TsqlConventionGuardTests,FactCollectorCommandTimeoutTests,FactCollectorFailureReportingTests(79): all green exceptDocCommentHygieneTests.TheDerivedProjectListCoversEveryProjectInTheTree, which found the harness's own csproj in the tree (artifact; the harness is not committed). Builds with 0 warnings:PerformanceMonitor.Darling.Analysis,PerformanceMonitor.Analysis,Darling.Tests,deprecated/Dashboard.Tests;Lite.Testscarries its one pre-existingxUnit2000onLogTailOverlapThresholdPinTests, untouched. CI's Windows job is the first execution of everything not listed.Which component(s) does this affect?
How was this tested?
See Tests above: pins executed locally through the in-worktree harness; live class executed against a throwaway TimescaleDB 2.28.1 / PostgreSQL 18 container; CI's
Darling PostgreSQL testsis the second execution.Checklist
dotnet build -c Debug)