Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,25 @@ public static async Task<List<ObjectSizeGrowthRow>> GetObjectSizeGrowthAsync(
/// keeps it NULL. This read returns NO other timestamp, which is why converting rather than labelling
/// matters more here than elsewhere: there is nothing else in the payload for a reader to notice a
/// disagreement against.</para>
/// <para><b>The de-skew is exact only inside the current DST period, and this is the read where that

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lite/Parity: this same DST-exactness caveat applies to Lite, and Lite's description isn't updated.

Lite/Mcp/McpObjectStatsTools.cs computes last_user_access with the identical pattern: GetServerUtcOffsetMinutesAsync reads the latest utc_offset_minutes (itself DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()) at collection time — same single-current-value offset as Darling's server_properties.utc_offset_minutes), then applies it via r.LastUserAccess?.AddMinutes(-utcOffsetMinutes) against a DMV column that persists since the last instance restart. That's the exact scenario this PR documents as being 60 minutes off across a DST transition — but Lite's tool description (McpObjectStatsTools.cs:57) just says "this read de-skews them" with no scope caveat.

Since this PR is intentionally scoped to documenting rather than fixing, should the same caveat be added to Lite's description (or a follow-up issue opened) so the two apps don't drift on a documented-vs-undocumented known limitation?

/// matters most.</b> <c>server_properties.utc_offset_minutes</c> is
/// <c>DATEDIFF(MINUTE, GETUTCDATE(), GETDATE())</c> — the offset in force AT COLLECTION TIME, one
/// current value. The other de-skewed reads describe current state (a running job, an open transaction,
/// a cleaner that ran seconds ago), so their timestamps and that offset sit on the same side of any
/// transition. These four do not: <c>sys.dm_db_index_usage_stats</c> persists since the instance
/// restarted, which on a stable production box is routinely months, so a large share of values predate
/// the most recent transition and come back <b>60 minutes early</b> — silently, and in the plausible
/// direction. This is not a theoretical exposure: any target in a DST-observing zone has it, a target
/// configured to UTC does not, and on AWS RDS the instance takes its time zone from a creation-time
/// parameter — so a non-UTC zone is an ordinary configuration rather than an exotic one, and "it is
/// RDS, so it is probably UTC" is not a safe assumption. #2932 records the measured offset behind the
/// four-hour figure quoted above. <c>sqlserver_start_time</c> on the same row is the bound on how far
/// back the affected values can reach.</para>
/// <para>Fixing it properly needs a ZONE rather than an offset — <c>CURRENT_TIMEZONE_ID()</c>
/// (SQL Server 2019+) collected alongside the offset, then <c>AT TIME ZONE</c> at the read boundary,
/// which handles transitions. That is a collected-column addition and a migration rung, so what is
/// carried here is the SCOPE of the claim, in the #2993 shape: this read places a timestamp exactly
/// when it falls inside the current DST period, and within an hour otherwise.</para>
/// <para>The alias deliberately does NOT carry a <c>_utc</c> suffix, unlike the other fifteen. This one
/// is a projection alias rather than a column, and <c>ConsumedTimestampFrameDisciplineTests</c> reaches
/// the payload field through the alias — a suffix here would make the field name and the alias diverge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ namespace PerformanceMonitor.Darling.Service.Mcp;
/// exists to answer — the tool's own description sends a caller here "when ADR cleanup looks stuck" — so a
/// phantom four-hour stall is the worst place for an unmarked frame. The trend read needs nothing: it
/// returns only <c>collection_time</c>.</para>
///
/// <para><b>Exact only inside the current DST period.</b> The collected offset is the one in force at

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same parity gap as the DarlingObjectStatsReader note: Lite/Mcp/McpPvsTools.cs de-skews aborted_version_cleaner_start_time/_end_time the same way (AddMinutes(-utcOffsetMinutes) off a single collected offset, McpPvsTools.cs:77-78), off a value that "can reach back weeks on a quiet database" per this PR's own reasoning — so the same DST-crossing exactness limitation applies there too, but Lite's tool description (McpPvsTools.cs:26) doesn't carry the scope note this PR adds to Darling.

/// collection time, so a cleaner timestamp from before the most recent transition is de-skewed by an
/// offset that was not in force when it was written and lands 60 minutes early. The off-row cleaner runs
/// continuously and is unaffected in practice, but <c>aborted_version_cleaner_*</c> can reach back weeks
/// on a quiet database — a live read showed 2026-08-26 against a 2026-09-09 snapshot — so it can cross a
/// transition. Same limit and same proper fix as <c>DarlingObjectStatsReader</c> records: a collected zone
/// plus <c>AT TIME ZONE</c>, rather than one offset applied to every age of value.</para>
/// </summary>
internal static class DarlingPvsReader
{
Expand Down
Loading