Skip to content

No guard scans a payload or render projection for a server-local column handed to a UTC-assuming consumer #3208

Description

@erikdarlingdata

Three clock-frame defects of one shape have now been found by hand — #2991 (query_stats.creation_time in analysis SQL), #3198 (default_trace_events.event_time in an MCP payload), and the sibling sweep for #3202 that found 16 more MCP fields plus 8 desktop render sites. Nothing in either test suite scans a payload or render projection for a server-local column being handed to a consumer that assumes UTC. A guard over that boundary would have caught all three at once, and it is the more valuable half of the work.

What the three existing guards do and do not reach

Guard Roots Hunts Why it cannot see this
CreationTimeClockFrameDisciplineTests Darling/PerformanceMonitor.Darling.Analysis, Lite/Analysis bare creation_time compared against a $n window bound right discriminator shape, wrong roots — neither Service/Mcp/ nor either viewer is in its globs
StoreSqlClockDisciplineTests store source incl. Service/Mcp/ naive timestamp column compared against a bare clock function right roots, wrong hazard, and it explicitly disclaims projections at :43-47: "a bare clock in a SET, a VALUES row or a projection is a different defect shape … deliberately out of scope"
CollectorTimestampFrameTests PerformanceMonitor.Collectors the frame a collector writes right per-column reasoning, but it reads collector source only — it says what the frame IS, never who consumes it
ServerLocalReadFrameDisciplineTests (#3202) Darling, Lite production .cs de-skew on reads of one table the right boundary, scoped to default_trace_events by design

The gap is exactly the diagonal: a column whose frame CollectorTimestampFrameTests knows, reaching a consumer none of the four inspects.

Why the frame cannot be keyed on the column name

The repo has already established this twice and both notes are worth keeping:

  • StoreSqlClockDisciplineTests maintains AmbiguousFrameColumns = ["sample_time", "event_time", "last_execution_time"] — three names that are two frames across several tables each, with the evidence per table.
  • CollectorTimestampFrameTests' own remarks record that its first cut was a store-wide "all naive timestamps are UTC" rule that would have forbidden the CPU collector's intentional local clock.

So a guard here must be scoped per (table, column), and the census has to be derivable rather than hand-declared or it rots.

The technique that already works

#3202's EveryAnnotationSourcesDeclaredFrame_MatchesItsOwnCollectorsQueryText derives the frame from the collector's own query text instead of trusting a declaration: this codebase writes alias = expression, so the right-hand side is the provenance. A column assigned from the XE @timestamp is UTC; one assigned from fn_trace_gettable's StartTime or a dm_exec_* / dm_tran_* DMV value is server-local. Critically, it fails when the provenance cannot be read rather than defaulting to UTC — "a frame nobody can read is the state the de-skew defects shipped in."

Run over the whole collector corpus that classifier already resolves the population: 66 CollectorColumnType.Timestamp columns, with 17 server-local columns reaching a consumer. It also finds its own blind spot honestly — BlockedProcessReportCollector's six and DmvBlockingSnapshotCollector.event_time have no T-SQL assignment at all (they are parsed from XML in C#, or stamped from context.CollectionTime), so those need a C#-side arm or an explicit per-column declaration with its evidence. That is the honest shape of the coverage, not a reason to skip them.

What to decide before writing it

  1. One guard or two. The MCP payload boundary (ToString("o") into an anonymous object) and the WPF render boundary (ForDisplay / FormatServerClock / FormatServerTime getters) are different discriminators over different roots. Two focused scans probably beat one that has to understand both.
  2. How the census is anchored. A floor and a ceiling per file, the way ServerLocalReadFrameDisciplineTests.KnownReaders does it — a bare total lets a Darling site vanish and a Lite one appear and still add up, which is the one-sided-port regression De-skew query_stats.creation_time before comparing it to the analysis window (Part of #2991) #2992 found nothing guarding against.
  3. Sequencing. The guard must be red before the fixes and green after, per site. If it is written after the fixes land and passes on the first run, it has converted an open question into false confidence. That means it lands with or after MCP reads return 16 server-local timestamps unmarked, beside naive-UTC fields in the same payload (#3198's siblings) #3206 and Desktop clock frames are wrong in opposite directions in both SKUs, and each SKU is right where the other is wrong #3207, with the red run recorded.

Floors to pin so a broken glob cannot report a clean bill of health, measured on dev at e63a367e7: 131 ToString("o") sites across 27 files in Darling/PerformanceMonitor.Darling.Service/Mcp/; 66 collector timestamp columns; ServerLocalReadFrameDisciplineTests measured 713 production .cs files across Darling/ and Lite/.

Blocks on, or lands with, #3206 (MCP reads) and #3207 (desktop renders). Related: #3198, #3202, #2991/#2992, #2932.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions