Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
02a1f12
issue-3653 A6 lane LC (part 1/2): freeze the legacy hourly/daily trio…
erikdarlingdata Sep 25, 2026
4beeb26
issue-3653 A6 lane LC part 2 (points 6-7): free the daily compression…
erikdarlingdata Sep 25, 2026
ed49daf
issue-3653 A6 lane LC-a2: point the raw purge and retention coverage …
erikdarlingdata Sep 25, 2026
1cc0bb2
Merge branch 'fix/3653-a6-freeze' of https://github.com/erikdarlingda…
erikdarlingdata Sep 25, 2026
eebaa27
Merge origin/dev (#4184, the daily stitch) into the freeze branch
erikdarlingdata Sep 25, 2026
1abc48b
Merge origin/dev (#4185) into the freeze branch
erikdarlingdata Sep 25, 2026
6419e4b
issue-3653 A6 lane LC-a5: re-derive the refresh grid pins for the fre…
erikdarlingdata Sep 25, 2026
8b632d0
#3653 A6 lane LC-a4: daily summary no longer throws on a frozen legac…
erikdarlingdata Sep 25, 2026
940cd7d
Merge branch 'fix/3653-a6-freeze' of https://github.com/erikdarlingda…
erikdarlingdata Sep 25, 2026
74c2555
#3653 A6 lane LC-a4: re-pin two more live tests that named a frozen r…
erikdarlingdata Sep 25, 2026
d550701
issue-3653 A6 lane LC-b: live proofs for the freeze (#3653)
erikdarlingdata Sep 25, 2026
493364e
Merge branch 'fix/3653-a6-freeze' of https://github.com/erikdarlingda…
erikdarlingdata Sep 25, 2026
2cd69d3
issue-3653 A6 lane LC-a6: re-pin the 11 live/pure tests the freeze broke
erikdarlingdata Sep 25, 2026
fafcda8
Merge branch 'fix/3653-a6-freeze' of https://github.com/erikdarlingda…
erikdarlingdata Sep 25, 2026
afa1471
Fix 7 PURE test failures for the A6 freeze constants (#3653)
erikdarlingdata Sep 25, 2026
3d2f9e0
Merge fix/4186-pins: 7 pure pin fixes (RefreshCeilingProvenance, Refr…
erikdarlingdata Sep 25, 2026
7d181c9
Fix 7 live test failures caused by #3653-A6 freeze and LC-a4 dual-con…
erikdarlingdata Sep 25, 2026
0cb2188
Fix Medium finding: ConvergeCompressionScheduleAsync resets frozen ro…
erikdarlingdata Sep 25, 2026
0174f58
Fix read regression: RollupCoverage.For() stitches legacy+successor f…
erikdarlingdata Sep 25, 2026
3cfeeea
Fix High: raw purge gate holds forever on field upgrade (#3653)
erikdarlingdata Sep 25, 2026
d534e58
Merge remote-tracking branch 'origin/fix/3653-a6-freeze' into work/36…
erikdarlingdata Sep 25, 2026
3098a9e
Fix #4186 data-loss: stitched raw purge gate must honor the outage seam
erikdarlingdata Sep 25, 2026
c14b46a
Fix #4186 follow-up: scan the outage seam even below the horizon
erikdarlingdata Sep 25, 2026
22cbb02
Fix two Low findings from the #4186 review round
erikdarlingdata Sep 25, 2026
fb26487
Merge remote-tracking branch 'origin/dev' into HEAD
erikdarlingdata Sep 25, 2026
35a0aef
Frozen rollup warning: name the create step too
erikdarlingdata Sep 25, 2026
c3291f6
Seam repair walks newest-first and stops at the first failed range (#…
erikdarlingdata Sep 25, 2026
be67f6a
Merge remote-tracking branch 'origin/dev' into fix/3653-a6-freeze
erikdarlingdata Sep 26, 2026
fd53564
Merge branch 'dev' into fix/3653-a6-freeze
erikdarlingdata Sep 26, 2026
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
35 changes: 21 additions & 14 deletions Darling/Darling.Tests/BaselineSupplyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -490,13 +490,19 @@ of the new. */
/// <c>SupersededBaselineRelations</c>); this pin names the reason.
///
/// <para><b>The grid DID move at #3653 (Q12), and not because of this pair.</b> The three interval-honest
/// HOURLY successors could not replace their legacies (the daily tier is hierarchical from those —
/// <c>SupersededHourlyRollups</c>), so they were appended to <c>HourlyAggregates</c> and the grid
/// re-derived by its own method: sixteen policies, the heaviest at :18, the watch line at 900 s. The two
/// bounded hourly successors are dealt into the bounded class ahead of the baselines, so this pair's
/// minutes moved 3→6 and 5→7 — the converge re-phases them once. What this pin still holds is the
/// baseline half: seven members, the pair in front, and the baseline pair adding NOTHING to the count the
/// grid derives from. The grid's own figures are pinned with their derivation in TimescaleSupportTests
/// HOURLY successors were appended to <c>HourlyAggregates</c> behind the legacy trio (the daily tier was
/// still hierarchical from it) and the grid re-derived by its own method: sixteen policies, the heaviest
/// at :18, the watch line at 900 s. The two bounded hourly successors were dealt into the bounded class
/// ahead of the baselines, so this pair's minutes moved 3→6 and 5→7.</para>
///
/// <para><b>And it moved BACK at #3653 (LC), again not because of this pair.</b> The freeze gave the daily
/// tier its own interval-honest successors (A6 LB) and moved the legacy trio off the grid entirely, into
/// <c>FrozenRollupAggregates</c>, with their hourly successors taking the three positions the legacy trio
/// held rather than staying appended behind it — thirteen policies again, the heaviest back at :15, the
/// watch line back at 1,050 s, and this pair's minutes back at 3 and 5. The converge re-phases every moved
/// policy once, on the first start of each build. What this pin holds throughout is the baseline half:
/// seven members, the pair in front, and the baseline pair adding NOTHING to the count the grid derives
/// from. The grid's own figures are pinned with their derivation in TimescaleSupportTests
/// (<c>CompressionPhaseGrid_ClearsEveryRefreshSlotsGuardBand_AndTheHeaviestRefreshsSlotWhole</c> and
/// <c>TheRefreshGridIsUnchanged_AndTheCompressionGridsOneInputFromItIsPinned</c>), not restated here.</para>
/// </summary>
Expand All @@ -507,14 +513,15 @@ public void Successors_TookTheLegacyPositions_SoThePhaseGridDidNotMove()
Assert.Equal(TimescaleSupport.PerfmonIntervalBaselineView, TimescaleSupport.BaselineAggregates[0].View);
Assert.Equal(TimescaleSupport.WaitStatsIntervalBaselineView, TimescaleSupport.BaselineAggregates[1].View);

/* The order's count is the hourly registry plus the seven baselines — 9 + 7 = 16 since #3653 — and
the baseline pair contributes exactly its two positions to it, no more. */
/* The order's count is the hourly registry plus the seven baselines — 6 + 7 = 13 since #3653's LC
freeze (was 9 + 7 = 16 during Q12) — and the baseline pair contributes exactly its two positions to
it, no more. */
Assert.Equal(TimescaleSupport.HourlyAggregates.Length + 7, TimescaleSupport.HourlyRefreshPhaseOrder.Count);
Assert.Equal(16, TimescaleSupport.HourlyRefreshPhaseOrder.Count);
Assert.Equal(6, TimescaleSupport.RefreshPhaseMinutesFor(TimescaleSupport.PerfmonIntervalBaselineView));
Assert.Equal(7, TimescaleSupport.RefreshPhaseMinutesFor(TimescaleSupport.WaitStatsIntervalBaselineView));
Assert.Equal(18, TimescaleSupport.HeaviestRefreshStartMinute);
Assert.Equal(900, TimescaleSupport.RefreshSlotWarningSeconds);
Assert.Equal(13, TimescaleSupport.HourlyRefreshPhaseOrder.Count);
Assert.Equal(3, TimescaleSupport.RefreshPhaseMinutesFor(TimescaleSupport.PerfmonIntervalBaselineView));
Assert.Equal(5, TimescaleSupport.RefreshPhaseMinutesFor(TimescaleSupport.WaitStatsIntervalBaselineView));
Assert.Equal(15, TimescaleSupport.HeaviestRefreshStartMinute);
Assert.Equal(1050, TimescaleSupport.RefreshSlotWarningSeconds);
}

/// <summary>
Expand Down
4 changes: 3 additions & 1 deletion Darling/Darling.Tests/CompressionPhaseAssignmentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ public void TheGridDoesNotMove_TheAssignmentIsAPermutationOfTheSameMinutes()
{
Assert.Equal(24, TimescaleSupport.CompressionPhaseBandMinutes);
Assert.Equal(24, TimescaleSupport.CompressionPhaseMinutes.Count);
Assert.Equal(18, TimescaleSupport.HeaviestRefreshWindowMinutes);
/* #3653 (LC): back to 21 minutes now that the legacy trio froze off the refresh grid and their
successors took the three positions instead of staying appended behind it (was 18 during Q12). */
Assert.Equal(21, TimescaleSupport.HeaviestRefreshWindowMinutes);
Assert.Equal(35, TimescaleSupport.AggregateCompressionBandMinute);
Assert.Equal(3, TimescaleSupport.CompressionPhaseMaxPerMinute);

Expand Down
45 changes: 32 additions & 13 deletions Darling/Darling.Tests/DailySummaryNotCarriedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ namespace Darling.Tests;
/// <para><b>The witness is the hole scan's definition, reused, not a second one.</b> The scan calls a bucket a
/// hole when the materialization holds no row for it AND the source holds an admitted row in it. The routed
/// <c>queries</c> CTE's third member asks the same two questions per server at day grain, with the source, its
/// time column and its filter read off <see cref="TimescaleSupport.MaterializationHoleTargets"/> — the repair's
/// own target list. So a server that genuinely ran nothing that day (no source row) is NOT named: the
/// time column and its filter read off <see cref="TimescaleSupport.RollupCoverageProbeTargets"/> — the repair's
/// own target list, extended (#3653 LC) so a frozen legacy rollup still has one. So a server that genuinely
/// ran nothing that day (no source row) is NOT named: the
/// disclosure cannot claim a hole where the raw table was simply empty. The live test below plants exactly
/// that control beside the hole.</para>
///
Expand Down Expand Up @@ -88,7 +89,9 @@ public void EveryRoutedForm_AsksTheHoleScansTwoProbes_PerServer_AtDayGrain()
foreach (var (tier, relation) in RoutedForms)
{
var sql = DailySummarySql.RangeSqlFor(tier, relation);
var target = TimescaleSupport.MaterializationHoleTargets.Single(t => t.View == relation);
/* #3653 LC froze query_stats_hourly/_daily out of MaterializationHoleTargets; RollupCoverageProbeTargets
still knows every relation the daily summary can route to. */
var target = TimescaleSupport.RollupCoverageProbeTargets.Single(t => t.View == relation);
var filter = TimescaleSupport.MaterializationHoleSourceFilterFor(target.CreateSql);

Assert.Contains("SELECT b.d, NULL::bigint AS c", sql, StringComparison.Ordinal);
Expand Down Expand Up @@ -138,7 +141,7 @@ the registry moves this test rather than silently moving the probe. */

private static (string Source, string TimeColumn, string Filter) SourceOf(string view)
{
var target = TimescaleSupport.MaterializationHoleTargets.Single(t => t.View == view);
var target = TimescaleSupport.RollupCoverageProbeTargets.Single(t => t.View == view);
return (target.Source, target.SourceTimeColumn, TimescaleSupport.MaterializationHoleSourceFilterFor(target.CreateSql));
}

Expand Down Expand Up @@ -174,7 +177,7 @@ public void ABucketWithZeroDistinctHashes_CannotExist_SoNullNeedsNoFlag()
public void RangeSqlFor_RefusesAnUnregisteredRelation()
{
var ex = Assert.Throws<ArgumentException>(() => DailySummarySql.RangeSqlFor(RetentionTier.Hourly, "some_future_rollup"));
Assert.Contains("MaterializationHoleTargets", ex.Message, StringComparison.Ordinal);
Assert.Contains("RollupCoverageProbeTargets", ex.Message, StringComparison.Ordinal);
Assert.Throws<ArgumentException>(() => DailySummarySql.RangeSqlFor(RetentionTier.Hourly, " "));
}

Expand Down Expand Up @@ -302,6 +305,8 @@ public async Task ASkippedDayBelowTheCeiling_ReadsNullAndIsNamed_AnEmptyDayIsAbs
var hourly = TimescaleSupport.QueryStatsHourlyView;
var successor = TimescaleSupport.QueryStatsIntervalHourlyView;
var daily = TimescaleSupport.QueryStatsDailyView;
// #3653 LC: the repair now targets the successor daily (legacy frozen); used below in the repair assertions.
var successorDaily = TimescaleSupport.QueryStatsIntervalDailyView;

/* One layout, planted twice. Five whole UTC days, D0 the oldest, window [D0, D5). D0: 3 hashes. D1:
NOTHING — the control, a day the server genuinely had no rows for. D2: 5 hashes, the day the daily
Expand Down Expand Up @@ -414,17 +419,31 @@ than a band. What this leg pins is that the routed read behind it produced the r
Assert.Equal("purged", root.GetProperty("hints").GetProperty("data_state").GetString());
}

/* THE REPAIR: the start-up pass scans the daily from its floor (the recent D0 is inside the hourly source's
90-day horizon) and finds exactly the recent D2 — its source, the hourly, holds the day and the daily
never materialized it — and closes it. The old cluster is past the scan horizon and stands (the
disclosure above did not depend on the repair). The same calendar read then prints 5 where it printed
NULL, D2 counts as a source again, and the recent server's DaysMissing at the daily tier is empty. */
/* #3653 LC: the repair now operates on query_stats_interval_daily (the successor), not the frozen
query_stats_daily. Set up a hole in the successor daily by refreshing the successor hourly for D2
(giving it a source), then materializing the successor daily for D0 and D3 only.
The old server's successor hourly is left untouched so its cluster stays past the repair's scan
horizon and oldAfter.DaysMissing remains [O(2)]. */
await RefreshAsync(connection, successor, R(2), R(3), ct);
await RefreshAsync(connection, successorDaily, R(0), R(1), ct);
await RefreshAsync(connection, successorDaily, R(3), R(4), ct);
Assert.Equal(new[] { R(0), R(3) }, await BucketDaysAsync(connection, successorDaily, RecentServerId, ct));

/* THE REPAIR: the start-up pass scans the successor daily from its floor (the recent D0 is inside the
hourly source's 90-day horizon) and finds exactly the recent D2 — its source, the successor hourly,
holds the day and the successor daily never materialized it — and closes it. The old cluster is past
the scan horizon and stands. The stitch-aware calendar read then prints 5 where it printed NULL. */
var log = new CapturingTestLogger();
var summary = await TimescaleSupport.RepairMaterializationHolesAsync(connection, log, DateTime.UtcNow, ct);
Assert.Equal(0, summary.Failures);
Assert.Contains($"{daily} had 1 bucket(s) in [{R(2):O}, {R(3):O})", log.Joined, StringComparison.Ordinal);

var repaired = await ReadCalendarAsync(connection, DailySummarySql.RangeSqlFor(RetentionTier.Daily), RecentServerId, R(0), R(5), ct);
Assert.Contains($"{successorDaily} had 1 bucket(s) in [{R(2):O}, {R(3):O})", log.Joined, StringComparison.Ordinal);

/* Read the repaired result via the stitch-aware router. The successor daily's floor is now R(0), so
StitchFloor returns null (successor covers the whole window) and RangeSqlFor picks the successor
daily for the whole window. D4 still falls through to raw (past ceiling). */
var coverageForRepair = await TimescaleSupport.DetectRollupCoverageAsync(
postgres, await TimescaleSupport.DetectRollupsAsync(postgres, ct), ct);
var repaired = await ReadCalendarAsync(connection, DailySummarySql.RangeSqlFor(RetentionTier.Daily, coverageForRepair, R(0)), RecentServerId, R(0), R(5), ct);
Assert.Equal(new[] { R(0), R(2), R(3), R(4) }, repaired.Select(r => r.Day).ToArray());
Assert.Equal(new long?[] { 3L, 5L, 7L, 2L }, repaired.Select(r => r.UniqueQueries).ToArray());
Assert.Equal(new[] { 1, 1, 1, 1 }, repaired.Select(r => r.SignalSourcesPresent).ToArray());
Expand Down
5 changes: 4 additions & 1 deletion Darling/Darling.Tests/DailySummaryReadShapeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,10 @@ GROUP BY 1
""";
}

var target = TimescaleSupport.MaterializationHoleTargets.Single(t => t.View == relation);
/* #3653 LC froze query_stats_hourly/_daily out of MaterializationHoleTargets; this oracle mirrors
DailySummarySql's own lookup (RollupCoverageProbeTargets), which still knows every relation the
product can route to. */
var target = TimescaleSupport.RollupCoverageProbeTargets.Single(t => t.View == relation);
var filter = TimescaleSupport.MaterializationHoleSourceFilterFor(target.CreateSql);
var sourceFilter = filter.Length == 0 ? string.Empty : " AND " + filter;
return $"""
Expand Down
45 changes: 45 additions & 0 deletions Darling/Darling.Tests/DailySummaryStitchedRangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,49 @@ public void RawTier_IgnoresTheStitch_ReadsExactlyAsTheTwoArgumentFormDoes()

Assert.Equal(plain, stitched);
}

/// <summary>
/// #3653 A6, lane LC-a4: the frozen legacy name this overload ITSELF ever names — <see cref="Legacy"/> on
/// the Hourly tier, <see cref="DailyLegacy"/> on the Daily tier (both hardcoded in the two-argument form
/// this overload falls back to; see its remarks) — must still build, both under
/// <see cref="RollupCoverage.Unknown"/> (nothing measured, so the legacy alone is named) and under a
/// stitched coverage whose successor floor falls INSIDE the window (the genuinely-stitched splice, which
/// names the legacy AND the successor). Both threw <see cref="ArgumentException"/> on 1abc48ba:
/// <c>QueriesCteForCagg</c>/<c>QueriesCteForStitchedCagg</c> read a frozen view's source off
/// <c>MaterializationHoleTargets</c>, which the freeze (#3653 LC) had just emptied of every frozen view.
/// </summary>
[Fact]
public void FrozenLegacy_StillBuilds_UnderUnknownCoverage_AndUnderAStitchThatCrossesTheWindow()
{
var windowStart = DaysAgo(10);

var hourlyUnknown = DailySummarySql.RangeSqlFor(RetentionTier.Hourly, RollupCoverage.Unknown, windowStart);
Assert.Contains($"FROM collect.{Legacy}", hourlyUnknown, StringComparison.Ordinal);

var dailyUnknown = DailySummarySql.RangeSqlFor(RetentionTier.Daily, RollupCoverage.Unknown, windowStart);
Assert.Contains($"FROM collect.{DailyLegacy}", dailyUnknown, StringComparison.Ordinal);

var hourlyStitch = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal) { [Legacy] = DaysAgo(80), [Successor] = DaysAgo(5) },
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.All);
var hourlyStitched = DailySummarySql.RangeSqlFor(RetentionTier.Hourly, hourlyStitch, windowStart);
Assert.Contains($"FROM collect.{Legacy}", hourlyStitched, StringComparison.Ordinal);
Assert.Contains($"FROM collect.{Successor}", hourlyStitched, StringComparison.Ordinal);

var dailyStitch = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal)
{
[DailyLegacy] = DaysAgo(200),
[DailySuccessor] = DaysAgo(5),
/* The daily stitch's own boundary is read off the successor HOURLY's floor (ceiling-of-day),
not the successor daily's floor directly — StitchedRelationSql's Daily branch. */
[TimescaleSupport.QueryStatsIntervalHourlyView] = DaysAgo(90),
},
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.All);
var dailyStitched = DailySummarySql.RangeSqlFor(RetentionTier.Daily, dailyStitch, windowStart);
Assert.Contains($"FROM collect.{DailyLegacy}", dailyStitched, StringComparison.Ordinal);
Assert.Contains($"FROM collect.{DailySuccessor}", dailyStitched, StringComparison.Ordinal);
}
}
Loading
Loading