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
337 changes: 324 additions & 13 deletions Darling/Darling.Tests/AlertEngineTests.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Darling/Darling.Tests/AlertReadFailureSurfaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ private static readonly (string Path, int Counted, int Exempt)[] s_wholeFileScop
{
["Could not load incident occurrences"] = "bookkeeping about an alert, not the condition read it is judged on",
["Could not persist incident occurrences"] = "a write",
["Could not persist the CPU persistence gate"] = "a write (#3282); the gate has already decided the observation from its in-memory record, so a dropped save costs the streak across a restart and never an alert - the seeding LOAD beside it is the read, and it is counted",
["Could not persist the {Metric} persistence gate"] = "a write (#3282; one save path for every gated built-in since #3653 A5 put tempdb Space behind the same gate as High CPU); the gate has already decided the observation from its in-memory record, so a dropped save costs the streak across a restart and never an alert - the seeding LOADs beside it are the reads, and they are counted",
["Alert resolution callback failed"] = "the delivery path",
["Connection-change self-alert delivery failed"] = "the delivery path",
["Store disk-pressure self-alert failed"] = "handed its evidence as parameters; the read is counted in DarlingWorker",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,10 @@ public async Task<List<PvsPressureInfo>> GetPvsPressureAsync(

/* ---------------- tempdb ---------------- */

/// <summary>Lite's latest-tempdb-snapshot read verbatim (tempdb_stats table).</summary>
/// <summary>Lite's latest-tempdb-snapshot read verbatim (tempdb_stats table). <c>collection_time</c> rides
/// as the last column since #3653 (A5): it is the row the read already orders by, projected so the engine's
/// persistence gate can tell a fresh collection from a re-read of the last one — see
/// <see cref="TempDbSpaceInfo.CollectionTimeUtc"/>. Same shape as before: one row, one index seek.</summary>
public const string TempDbSpaceSql = @"
SELECT
total_reserved_mb,
Expand All @@ -769,7 +772,8 @@ public async Task<List<PvsPressureInfo>> GetPvsPressureAsync(
version_store_reserved_mb,
top_session_tempdb_mb,
top_session_id,
max_size_mb
max_size_mb,
collection_time
FROM tempdb_stats
WHERE server_id = $1
ORDER BY collection_time DESC
Expand Down Expand Up @@ -799,7 +803,12 @@ ORDER BY collection_time DESC
/* NULL on every row collected before the V81 rung, and 0 is what "no ceiling measured"
is spelled as — so history keeps reporting the percentage it always did rather than
dividing by a zero cap. */
MaxSizeMb = reader.IsDBNull(7) ? 0 : ToDouble(reader.GetValue(7))
MaxSizeMb = reader.IsDBNull(7) ? 0 : ToDouble(reader.GetValue(7)),
/* #3653 (A5): naive UTC off the `timestamp` column, read back Kind Unspecified and stamped Utc
because that is what it IS and what the property's name says — the #3636 file-growth read's
idiom. The engine only ever compares one server's stamps with each other, so the Kind is
honesty rather than arithmetic. */
CollectionTimeUtc = reader.IsDBNull(8) ? null : DateTime.SpecifyKind(reader.GetDateTime(8), DateTimeKind.Utc)
};
}

Expand Down
2 changes: 1 addition & 1 deletion Darling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ One alert name means one condition on both engines wherever both engines can hon
| **Poison Wait** | THREADPOOL / RESOURCE_SEMAPHORE / RESOURCE_SEMAPHORE_QUERY_COMPILE | IPC:BtreePage / IPC:BufferIo (Aurora) | Both, **one shape** since #3593: accumulated wait over a rolling ten-minute window, graded WARNING/CRITICAL from the shared `PoisonWaitEvaluator` bars on both engines. The earlier divergence (always-CRITICAL on SQL Server, graded on PostgreSQL, under one mute key) is closed. Both hosts clear a standing alert only on an **observed** window (#3593 SQL Server, #3653 PostgreSQL): a collector that stops delivering holds the alert open rather than announcing a recovery nobody measured. On PostgreSQL the witness is the `pg_wait_stats` collector's own `collection_log` runs inside the window, because since #2694 a quiet poison event writes no row and the rows alone cannot tell a finished storm from a dead collector. |
| **High CPU** | Ring buffer, 3-sample persistence gate | Performance Insights, banded on ACU headroom, same gate | Both. Both fires are graded since #3653: WARNING at the configured threshold, CRITICAL at the CPU health band's 95% bar — the card's ladder, not a new number. On PostgreSQL the grade is the band's own classifier on the same two figures the card is handed, so the 95% is of the configured ACU ceiling (the figure the gate thresholded), never of the currently allocated capacity. |
| **Database File Growth** | Per-file growth over the lookback window vs `FileGrowthRiseMb` | — | SQL-only. It is a SQL Server *file-model* condition: data/log files with explicit growth increments and ceilings, where a rise means a file autogrew. PostgreSQL has no file growth to watch — relations grow a page at a time and the disk-level answer is Volume Free Space. The PostgreSQL relative is *bloat growth* (`pg_table_bloat_stats` hourly and `pg_index_bloat` daily are collected); a "bloat growth" alert is a future item and would not share this name, because it answers a different question. |
| **TempDB Space**, **Volume Free Space**, **Long-Running / Failed Agent Job** | Yes | — | SQL-only by construction (tempdb, host volumes via `sys.dm_os_volume_stats`, SQL Agent). TempDB Space fires with an explicit WARNING tier and no CRITICAL tier (#3653): the product has no measured "tempdb nearly full" bar to cite, and the percent's denominator is the growth ceiling where one exists and the allocation where not, so a single reserved-percent tier would mean two distances from failure. |
| **TempDB Space**, **Volume Free Space**, **Long-Running / Failed Agent Job** | Yes (TempDB Space: 3-sample persistence gate) | — | SQL-only by construction (tempdb, host volumes via `sys.dm_os_volume_stats`, SQL Agent). TempDB Space sits behind the same shared persistence gate as High CPU since #3653 (A5): 3 consecutive collected `tempdb_stats` samples at or above the threshold to fire, the first collected sample below it to resolve, counted per collection rather than per 30 s sweep, and a reading that stops arriving freezes the gate rather than resolving it — measured on a 43-server production fleet, every one of one server's 22 crossings in 14 days was 1–4 samples long. TempDB Space fires with an explicit WARNING tier and no CRITICAL tier (#3653): the product has no measured "tempdb nearly full" bar to cite, and the percent's denominator is the growth ceiling where one exists and the allocation where not, so a single reserved-percent tier would mean two distances from failure. |
| **PostgreSQL Wraparound Risk**, **PostgreSQL Vacuum Horizon Blocked** | — | Yes | PostgreSQL-only by construction: MVCC transaction-id wraparound and the xmin horizon have no SQL Server counterpart. |
| **PostgreSQL Replication Slot Retention** | — | `pg_replication_slots` retained WAL, `wal_status` | The SQL Server analogue is a database whose log cannot truncate — `sys.databases.log_reuse_wait_desc` reading REPLICATION / AVAILABILITY_REPLICA / ACTIVE_TRANSACTION / LOG_BACKUP for consecutive collections. That column IS collected, but only as the load-time `database_config` snapshot (frequency 0), not as a time series, so a persistence-gated "Log Reuse Blocked" condition cannot be evaluated honestly today; it needs the column on a cadenced per-database collector (`database_states` is the natural home), which is a collector change with its own census and pins. Named here so the pair is on record: slot retention on PostgreSQL, log-reuse wait on SQL Server. |
| **Server unreachable**, **Collection stopped**, **Retention Held**, collector-health bands | Yes | Yes | Both — these are about the monitor, not the engine. |
Expand Down
30 changes: 25 additions & 5 deletions Lite.Tests/LiteAlertForwardingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,15 @@ public async Task TempDb_ToastBody_MatchesTheOldLoop()
var h = new Harness();
h.Adapter.TempDb = new TempDbSpaceInfo { TotalReservedMb = 910, UnallocatedMb = 90 }; /* 91% used */

await h.Build().EvaluateServerAsync(Harness.Snapshot());
/* #3653 (A5): the fire is behind the shared persistence gate, so it takes TempDbSpaceBreachSamples
observations to earn. The fixture carries no CollectionTimeUtc, which is the documented every-sweep-
counts fallback (pinned in AlertEngineTests.TempDb_NoCollectionTime_CountsEverySweep); the strings
below are what this pin is about and are unchanged. */
var engine = h.Build();
for (var i = 0; i < AlertEngine.TempDbSpaceBreachSamples; i++)
{
await engine.EvaluateServerAsync(Harness.Snapshot());
}

var fired = Assert.Single(h.Deliverer.Outcomes);
Assert.Equal("91% reserved (910 MB)", fired.CurrentValue); /* :448 */
Expand All @@ -879,15 +887,27 @@ public async Task TempDb_TheAzureCeiling_SuppressesTheAlert_ThatTheAllocationWou
Assert.Equal(80, App.AlertTempDbSpaceThresholdPercent);

var h = new Harness();
/* GP_S_Gen5_2 with one ~57 MB #temp table: 62.44 MB allocated, 65,536 MB of headroom behind it. */
/* GP_S_Gen5_2 with one ~57 MB #temp table: 62.44 MB allocated, 65,536 MB of headroom behind it. Held
for a full gate's worth of sweeps (#3653 A5), so the silence is the ceiling's and not the gate's. */
h.Adapter.TempDb = new TempDbSpaceInfo { TotalReservedMb = 59.75, UnallocatedMb = 2.69, MaxSizeMb = 65_536 };
await h.Build().EvaluateServerAsync(Harness.Snapshot());
var withCeiling = h.Build();
for (var i = 0; i < AlertEngine.TempDbSpaceBreachSamples; i++)
{
await withCeiling.EvaluateServerAsync(Harness.Snapshot());
}

Assert.Empty(h.Deliverer.Outcomes);

/* The identical snapshot with the ceiling unmeasured is the pre-#2515 reading, and it pages. */
/* The identical snapshot with the ceiling unmeasured is the pre-#2515 reading, and it pages — after the
same number of sweeps. */
var withoutCeiling = new Harness();
withoutCeiling.Adapter.TempDb = new TempDbSpaceInfo { TotalReservedMb = 59.75, UnallocatedMb = 2.69 };
await withoutCeiling.Build().EvaluateServerAsync(Harness.Snapshot());
var engine = withoutCeiling.Build();
for (var i = 0; i < AlertEngine.TempDbSpaceBreachSamples; i++)
{
await engine.EvaluateServerAsync(Harness.Snapshot());
}

var fired = Assert.Single(withoutCeiling.Deliverer.Outcomes);
Assert.Equal("96% reserved (60 MB)", fired.CurrentValue);
Assert.Equal("tempdb 96% reserved", fired.ShortMessage);
Expand Down
14 changes: 11 additions & 3 deletions Lite/Services/LocalDataService.TempDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ FROM v_tempdb_stats
}

/// <summary>
/// Gets the latest TempDB space snapshot for alert checking.
/// Gets the latest TempDB space snapshot for alert checking. <c>collection_time</c> rides as the last column
/// since #3653 (A5): the row the read already orders by, projected so the shared engine's tempdb persistence
/// gate can tell a fresh collection from a re-read of the last one — see
/// <see cref="TempDbSpaceInfo.CollectionTimeUtc"/>. Darling's <c>TempDbSpaceSql</c> twin projects the same.
/// </summary>
public async Task<TempDbSpaceInfo?> GetLatestTempDbSpaceAsync(int serverId)
{
Expand All @@ -84,7 +87,8 @@ FROM v_tempdb_stats
version_store_reserved_mb,
top_session_tempdb_mb,
top_session_id,
max_size_mb
max_size_mb,
collection_time
FROM v_tempdb_stats
WHERE server_id = $1
ORDER BY collection_time DESC
Expand All @@ -107,7 +111,11 @@ ORDER BY collection_time DESC
/* NULL on every row collected before the v56 migration, and 0 is what "no ceiling
measured" is spelled as — so history keeps reporting the percentage it always did
rather than dividing by a zero cap. Darling's twin reads the same column. */
MaxSizeMb = reader.IsDBNull(7) ? 0 : ToDouble(reader.GetValue(7))
MaxSizeMb = reader.IsDBNull(7) ? 0 : ToDouble(reader.GetValue(7)),
/* #3653 (A5): the collector stamps UTC into DuckDB's tz-less TIMESTAMP, so the value comes back
Kind Unspecified and is stamped Utc because that is what it is — the AG-topology reads' idiom.
The engine only ever compares one server's stamps with each other. */
CollectionTimeUtc = reader.IsDBNull(8) ? null : DateTime.SpecifyKind(reader.GetDateTime(8), DateTimeKind.Utc)
};
}

Expand Down
Loading
Loading