Skip to content
Merged
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
83 changes: 83 additions & 0 deletions Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
*/

using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using PerformanceMonitor.Alerting;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Storage;
Expand Down Expand Up @@ -322,6 +325,86 @@ private static string EvaluatePgCpuBody()
return body;
}

/// <summary>
/// Every comment that cites this rung's NUMBER cites the right one.
///
/// <para>A rung number in prose is a frozen claim, and this branch froze the wrong one: #3315 landed
/// V117 first, this rung renumbered to V118, and five comments across both SKUs kept saying V117 —
/// three of which review found and two of which it did not. The repo leans on exactly these citations
/// to check Lite/Darling parity, so a wrong one sends the next person to the wrong rung.</para>
///
/// <para><b>The number is derived from <see cref="StorageVersion.SchemaVersion"/></b> rather than
/// compared against a literal, so a future renumber reds this instead of leaving silent copies.</para>
///
/// <para><b>And each phrase carries its own SUBJECT</b>, which is what makes a citation attributable
/// without parsing comments at all. Two earlier spellings of this pin are the reason it is shaped this
/// way. Scoped to the file, it flagged nine correct citations — those files cite V32, V40, V44, V50,
/// V60, V61, V80 and V81 for their own tables, all right. Scoped to comment BLOCKS instead, it needed a
/// hand-rolled line-prefix comment filter, which <c>CommentFilterAdoptionTests</c> correctly refuses
/// without a stated bound — and the bound I measured for it came back unreliable on its own terms. A
/// phrase that names both the table and the rung needs neither: it cannot match another rung's citation
/// because it does not describe another rung's subject.</para>
/// </summary>
[Fact]
public void EveryCommentCitingThisRungsNumber_CitesTheRealOne()
{
var rung = StorageVersion.SchemaVersion;

/* Each entry names a file and the citation in it, with {0} where the rung goes. The subject words
are part of the phrase deliberately — see the remarks. A reword reds this, which is correct: the
prose and the pin are one claim, so the pin has to be edited with it. */
var citations = new (string Path, string Phrase)[]
{
(Path.Combine("Lite", "Database", "Schema.cs"),
"config.alert_persistence_state (PgMigrations V{0})"),
(Path.Combine("Lite", "Database", "DuckDbInitializer.cs"),
"persistence-gate state, porting Darling's V{0}"),
(Path.Combine("Lite", "Services", "DuckDbAlertHistoryStore.cs"),
"the Lite twin of Darling's V{0} table"),
(Path.Combine("Lite", "Services", "LiteAlertStateStore.cs"),
"Darling's V{0} table, so the shared engine's CPU gate"),
(Path.Combine("Darling", "PerformanceMonitor.Darling.Service", "PgAlertStateStore.cs"),
"persistence-gate record from the V{0}"),
};

foreach (var (relative, phrase) in citations)
{
var source = RepoFile.ReadRepoFileLf(relative);

/* EXACTLY once. Absent means the prose was reworded and this pin went stale with it; twice
means a copy was made that the next renumber would miss. */
var expected = string.Format(CultureInfo.InvariantCulture, phrase, rung);
Assert.Equal(1, CountOf(source, expected));

/* And the same phrase carrying ANY other rung number must not appear — the renumber case. The
regex is built from the phrase itself, so it cannot drift away from the string above. */
var pattern = Regex.Escape(phrase).Replace(@"V\{0}", @"V(\d+)", StringComparison.Ordinal);
var found = Regex.Matches(source, pattern, RegexOptions.CultureInvariant)
.Select(m => m.Groups[1].Value)
.ToArray();

/* The regex has to MATCH, or the assertion below is over an empty set and passes for the wrong
reason — the failure mode of building a pattern out of an escaped literal. */
Assert.NotEmpty(found);
Assert.All(found, n => Assert.Equal(rung.ToString(CultureInfo.InvariantCulture), n));
}
}

/// <summary>Non-overlapping occurrences of <paramref name="needle"/> — <c>IndexOf</c> in a loop, because
/// there is no overload that counts and a <c>Split</c> would allocate the whole file per call.</summary>
private static int CountOf(string haystack, string needle)
{
var count = 0;
var at = haystack.IndexOf(needle, StringComparison.Ordinal);
while (at >= 0)
{
count++;
at = haystack.IndexOf(needle, at + needle.Length, StringComparison.Ordinal);
}

return count;
}

/// <summary>
/// The README's alert catalog states the sample count, so the number is pinned to the constant rather
/// than left as a prose copy of it. A doc that says "3 samples" while the code says something else is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ a missed or duplicated alert. The alert itself has already been decided by the g
}

/// <summary>
/// #3282: loads one subject's built-in persistence-gate record from the V117
/// #3282: loads one subject's built-in persistence-gate record from the V118
/// <c>config.alert_persistence_state</c> table.
///
/// <para>Returns null on failure, which the engine reads as "no memory" and arms the gate from zero —
Expand Down
2 changes: 1 addition & 1 deletion Lite/Database/DuckDbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1551,7 +1551,7 @@ positional appender and old parquet are unaffected. */

if (fromVersion < 58)
{
/* v58 (#3282): the built-in alert catalog's persistence-gate state, porting Darling's V117.
/* v58 (#3282): the built-in alert catalog's persistence-gate state, porting Darling's V118.
Before this no built-in alert required its condition to PERSIST — one sample over the bar
fired and the next sample under it resolved — so a momentary CPU spike was indistinguishable
from sustained saturation. New table only; fresh installs get it from
Expand Down
2 changes: 1 addition & 1 deletion Lite/Database/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ last_observed_at is not display data — it is what makes a row's staleness deci
fingerprint's NEXT incident would decay its already-counted mark to the new window count and report
the recurrence as nothing new. */
/* The BUILT-IN alert catalog's persistence-gate state (#3282), the twin of Darling's
config.alert_persistence_state (PgMigrations V117) — same columns, same key. "How long must this
config.alert_persistence_state (PgMigrations V118) — same columns, same key. "How long must this
condition hold before it counts" for the gauge alerts: consecutive breaching samples so far,
consecutive clearing samples so far, and whether an incident is currently open.

Expand Down
2 changes: 1 addition & 1 deletion Lite/Services/DuckDbAlertHistoryStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ gate has already decided by this point. */

/// <summary>
/// #3282: loads one subject's built-in persistence-gate record from <c>config_alert_persistence_state</c>
/// — the Lite twin of Darling's V117 table. Returns null when there is no row, which the engine reads as
/// — the Lite twin of Darling's V118 table. Returns null when there is no row, which the engine reads as
/// "no memory" and arms the gate from zero.
///
/// <para>Null on failure too, deliberately. A load failure cannot resurrect a <c>firing</c> bit it did
Expand Down
2 changes: 1 addition & 1 deletion Lite/Services/LiteAlertStateStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public Task SaveIncidentOccurrencesAsync(

/// <summary>
/// #3282: the built-in persistence-gate record, over <c>config_alert_persistence_state</c> — Lite's twin
/// of Darling's V117 table, so the shared engine's CPU gate behaves the same on both SKUs. Wrapped in
/// of Darling's V118 table, so the shared engine's CPU gate behaves the same on both SKUs. Wrapped in
/// <c>Task.Run</c> like every other method here: DuckDB.NET's I/O is synchronous under its async facade
/// and the engine runs on the WPF dispatcher, so an unwrapped call is a UI hitch (#1202).
/// </summary>
Expand Down
Loading