Repository navigation
tempdb Space fired on one collected sample and resolved on the next: the arm now holds for 3 consecutive collections behind the shared persistence gate, counted per collection rather than per sweep, with no-data freezing it instead of resolving to N/A (#3653 A5, tempdb slice) - #3692
Merged
Merged
Conversation
…the arm now sits behind the shared AlertPersistenceGate at K = 3 consecutive collections (#3653 A5, tempdb slice) The tempdb Space alert fired on the FIRST breaching read of the latest tempdb_stats row and resolved on the first sub-threshold read after it. Measured on one production store class (43 servers, 14 days), one server fired 22 times and every run was 1-4 breaching samples spanning at most 206 s: 22 pages and 22 resolves about a tempdb that was never in trouble for longer than three and a half minutes. The fire decision now sits behind the shared AlertPersistenceGate (#3282) at TempDbSpaceBreachSamples = 3 consecutive breaching COLLECTIONS, which keeps 2 of the 22 measured runs - the two an operator could still have seen as true when the page arrived. TempDbSpaceClearSamples = 1 keeps the resolve exactly where it was; the measurement covered breach runs and said nothing about mid-incident dips, so a larger clear count would be a number without evidence. Samples, not sweeps: TempDbSpaceInfo carries the row's collection_time (one projected column on a read both SKUs already ORDER BY it) so a 30 s sweep re-reading the last collected row does not advance the streak - without that, K = 3 is 90 s of one sample and the measured flap comes straight back behind the gate. A null instant degrades to counting sweeps, the same fallback the CPU gate documents. The CPU arm's fresh-sample guard, evaluate and save-on-change are lifted into ObservePersistenceAsync and the save into SavePersistenceAsync(key, metric, record), used by both arms; the CPU arm's behaviour is unchanged and every #3282 pin stays green. The in-memory _activeTempDbSpaceAlert flag is gone - the persisted Firing bit is the one "incident open" fact, seeded at startup under the new "tempdb Space" metric name in the existing (server_id, metric_name) persistence table (no rung) with the cooldown stamped on an already-open incident so a restart does not re-announce it. A null tempdb read now FREEZES the gate instead of resolving with "N/A". Blocking Wait Time and Long-Running Query are not touched: both await rulings (Q4/Q5 on #3653). Partial for #3653; does not close it.
Closed
39 tasks done
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… snapshot at 3x the bar still fires at once, anything less holds for 3 consecutive collections through the shared AlertPersistenceGate (#3653 A5, Q4) The Blocking Wait Time alert fired on the FIRST fresh blocking snapshot whose summed wait_time_ms reached the configured bar and resolved on the first one under it. Measured on one production store class (43 servers): 102 episodes, 97 of them a SINGLE snapshot at the collector's ~72 s cadence - including the p99, 1,509 s of summed blocked wait across 145 sessions, gone by the next collection - so a plain K-consecutive gate (the tempdb shape from #3692) would have dropped exactly the episodes an operator most needs to hear about. Erik ruled Q4 on #3653: fire on a single snapshot at N = 3x the configured threshold; otherwise K = 3 consecutive collections through the shared AlertPersistenceGate. Both arms ride ONE gate: the bar handed to ObservePersistenceAsync is one sample when the snapshot alone clears the multiple and BlockingWaitBreachSamples otherwise, so a 3x fire opens the incident on the same persisted record the consecutive arm would have opened it on and the clear that follows is the same clear. The fire prepends a gate item whose `Fired By` field carries the arm token (single_snapshot_3x / consecutive_k3) so the card, the history row and get_alert_history's context_json can say why. What makes this arm unlike tempdb's: dmv_blocking_snapshot writes rows only while something is blocked, so a quiet cycle writes nothing and "the latest snapshot" simply stops advancing. Two breaching snapshots with a quiet cycle between them are two episodes, so the arm restarts a partial streak when a breaching snapshot lands more than 1.5 collector cadences after the last one counted (CurrentBlockingWaitResult now carries the cadence both adapters already resolved for the freshness verdict). An OPEN incident is not restarted by a short gap. A STALE snapshot is a clear here (the #1812 rule this arm always had - it is the collector saying the blocking ended), observed per sweep; a NULL read freezes the gate (the #3692 rule); disabling the gate is not a clear (the CPU arm's rule). The in-memory _activeBlockingWaitAlert flag is gone; the persisted Firing bit under the alert's own metric name in the existing (server_id, metric_name) table is the one "incident open" fact (no rung), seeded at startup with the cooldown stamped so a restart does not re-announce. Lite and Darling both route this alert through the shared engine, so both SKUs get the gate. Long-Running Query (Q5) is a separate commit. Partial #3653 - item 6 (Q4). Does not close it.
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… snapshot at 3x the bar still fires at once, anything less holds for 3 consecutive collections through the shared AlertPersistenceGate (#3653 A5, Q4) The Blocking Wait Time alert fired on the FIRST fresh blocking snapshot whose summed wait_time_ms reached the configured bar and resolved on the first one under it. Measured on one production store class (43 servers): 102 episodes, 97 of them a SINGLE snapshot at the collector's ~72 s cadence - including the p99, 1,509 s of summed blocked wait across 145 sessions, gone by the next collection - so a plain K-consecutive gate (the tempdb shape from #3692) would have dropped exactly the episodes an operator most needs to hear about. Erik ruled Q4 on #3653: fire on a single snapshot at N = 3x the configured threshold; otherwise K = 3 consecutive collections through the shared AlertPersistenceGate. Both arms ride ONE gate: the bar handed to ObservePersistenceAsync is one sample when the snapshot alone clears the multiple and BlockingWaitBreachSamples otherwise, so a 3x fire opens the incident on the same persisted record the consecutive arm would have opened it on and the clear that follows is the same clear. The fire prepends a gate item whose `Fired By` field carries the arm token (single_snapshot_3x / consecutive_k3) so the card, the history row and get_alert_history's context_json can say why. What makes this arm unlike tempdb's: dmv_blocking_snapshot writes rows only while something is blocked, so a quiet cycle writes nothing and "the latest snapshot" simply stops advancing. Two breaching snapshots with a quiet cycle between them are two episodes, so the arm restarts a partial streak when a breaching snapshot lands more than 1.5 collector cadences after the last one counted (CurrentBlockingWaitResult now carries the cadence both adapters already resolved for the freshness verdict). An OPEN incident is not restarted by a short gap. A STALE snapshot is a clear here (the #1812 rule this arm always had - it is the collector saying the blocking ended), observed per sweep; a NULL read freezes the gate (the #3692 rule); disabling the gate is not a clear (the CPU arm's rule). The in-memory _activeBlockingWaitAlert flag is gone; the persisted Firing bit under the alert's own metric name in the existing (server_id, metric_name) table is the one "incident open" fact (no rung), seeded at startup with the cooldown stamped so a restart does not re-announce. Lite and Darling both route this alert through the shared engine, so both SKUs get the gate. Long-Running Query (Q5) is a separate commit. Partial #3653 - item 6 (Q4). Does not close it.
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…utive collections, not on every snapshot; Long-Running Query gets an opt-out knob seeded from the production read — job-step program prefix, the two NT AUTHORITY logins — applied ahead of the row cap and counted by arm (#3653 A5, Q4 + Q5) (#3734) * Blocking Wait Time fired on one snapshot and cleared on the next: one snapshot at 3x the bar still fires at once, anything less holds for 3 consecutive collections through the shared AlertPersistenceGate (#3653 A5, Q4) The Blocking Wait Time alert fired on the FIRST fresh blocking snapshot whose summed wait_time_ms reached the configured bar and resolved on the first one under it. Measured on one production store class (43 servers): 102 episodes, 97 of them a SINGLE snapshot at the collector's ~72 s cadence - including the p99, 1,509 s of summed blocked wait across 145 sessions, gone by the next collection - so a plain K-consecutive gate (the tempdb shape from #3692) would have dropped exactly the episodes an operator most needs to hear about. Erik ruled Q4 on #3653: fire on a single snapshot at N = 3x the configured threshold; otherwise K = 3 consecutive collections through the shared AlertPersistenceGate. Both arms ride ONE gate: the bar handed to ObservePersistenceAsync is one sample when the snapshot alone clears the multiple and BlockingWaitBreachSamples otherwise, so a 3x fire opens the incident on the same persisted record the consecutive arm would have opened it on and the clear that follows is the same clear. The fire prepends a gate item whose `Fired By` field carries the arm token (single_snapshot_3x / consecutive_k3) so the card, the history row and get_alert_history's context_json can say why. What makes this arm unlike tempdb's: dmv_blocking_snapshot writes rows only while something is blocked, so a quiet cycle writes nothing and "the latest snapshot" simply stops advancing. Two breaching snapshots with a quiet cycle between them are two episodes, so the arm restarts a partial streak when a breaching snapshot lands more than 1.5 collector cadences after the last one counted (CurrentBlockingWaitResult now carries the cadence both adapters already resolved for the freshness verdict). An OPEN incident is not restarted by a short gap. A STALE snapshot is a clear here (the #1812 rule this arm always had - it is the collector saying the blocking ended), observed per sweep; a NULL read freezes the gate (the #3692 rule); disabling the gate is not a clear (the CPU arm's rule). The in-memory _activeBlockingWaitAlert flag is gone; the persisted Firing bit under the alert's own metric name in the existing (server_id, metric_name) table is the one "incident open" fact (no rung), seeded at startup with the cooldown stamped so a restart does not re-announce. Lite and Darling both route this alert through the shared engine, so both SKUs get the gate. Long-Running Query (Q5) is a separate commit. Partial #3653 - item 6 (Q4). Does not close it. * Long-Running Query gets an opt-out knob by program_name / login, applied in the read ahead of the row cap, and the card counts what it removed (#3653 A5, Q5) - engine + Lite; Darling's store home is a rung away Measured on one production store class: 191 distinct sessions over the 30-minute Long-Running Query bar in 7 days, the p90 of them seen in 6,192 snapshots. These are permanent background requests - brokers, replication readers, agent loops, pollers - that are over any duration threshold forever. No persistence gate can help (they persist by definition); coverage is the fix, and which programs and logins are the background on an estate is per-operator knowledge, so it is a knob. Erik ruled Q5 on #3653: the knob is allowed; the "what are they" measurement that would seed it is the monitor seat's read. IAlertEngineSettings gains LongRunningQueryExcludedProgramNames and LongRunningQueryExcludedLogins. The rule is spelled ONCE in the new LongRunningQueryExclusions (Alerting): case-insensitive, whole-value, a trailing * matches any suffix, % and _ literal, null matches nothing, a bare * refused; From/Normalize give a Settings-window string and an MCP array one meaning. An excluded session is NOT EVALUATED - not read into the decision, not counted, not fingerprinted - which is the opposite of a mute rule, and is why excluding a paging program resolves the open incident instead of silencing it. Applied IN THE READ, before LIMIT, on both SKUs. The read is longest- elapsed-first capped at maxResults (5), and the sessions this knob is for are the longest-running on the server by construction, so a post- read drop (the excludedDatabases shape) would let five permanent requests fill the cap on every sweep and hide the real query behind them - the alert made blind by the knob meant to make it accurate. Both reads gain a candidates CTE that flags each over-threshold row with the shared ILIKE ... ESCAPE predicate (BuildSqlPredicate, operands from $5), keep the unflagged rows under the cap, and return the flagged count as an uncorrelated scalar over the same CTE: one statement, one snapshot. IAlertReadAdapter.GetLongRunningQueriesAsync returns LongRunningQueryReadResult(Sessions, ExcludedCount); login_name is now projected. The High CPU card's maintenance probe passes None: a backup run by an excluded service login is still what is burning the CPU. The card appends an "Excluded Count" item - only when the knob is set - so an operator can see the knob working; a setting whose only effect is a page NOT arriving is one nobody can verify. Lite: two settings.json arrays (documented in settings.sample.json), two boxes on the Settings window's Alerts tab, AppAlertEngineSettings forwards them. Darling: DarlingAlertSettings returns EMPTY for both, with the reason in the comment - the store row is authoritative for every alert knob (ApplyToConfig replaces config.Alerts wholesale), so a darling.json-only member would read as configured and be dead; the honest home is two text[] columns on config_alert_settings, which is a migration rung this lane may not write (cross-seat fence). The MCP twins publish the knob on neither SKU yet for the same reason (McpAlertSettingsKeyTests pins the two payloads to one shape). The Darling store plumbing follows as its own commit, gated on that rung. Partial #3653 - item 6 (Q5). Does not close it. * The Long-Running Query opt-out knob is seeded from the production read: program names match by PREFIX, logins exactly, the two lists default to the SQL Agent job-step prefix and the two NT AUTHORITY service logins, and the card counts what each arm removed in sessions (#3653 A5, Q5 addendum) The 7-day read of one large production store (monitor seat, authorised under Q5) split the long-running population into four classes: (1) SQL Agent job-step programs - program_name prefix "SQLAgent - TSQL JobStep", ~460 sessions/week across 11 single-server jobs, medians 35-62 min; (2) the NT AUTHORITY\SYSTEM / NT AUTHORITY\NETWORK SERVICE logins - the permanent multi-DAY background (~70 sessions across 42 servers, medians 4.8-8.6 days, 300K+ snapshots, CDC-capture shaped); (3) the application's admin login - carries the job wave but ALSO real ad-hoc long-runners, so excluding it would blind the alert to what it is for; (4) named humans - 3 sessions/week, the page's whole purpose. That read changes the knob's shape from the previous commit: - Program names match by PREFIX (case-insensitive), because job-step names embed the job id; logins match EXACTLY (a prefix would let svc swallow svc_owner). The trailing-* grammar is gone: an entry is the literal prefix / value, % and _ escaped before LIKE, * a character. The member is LongRunningQueryExcludedProgramNamePrefixes (settings.json alert_long_running_query_excluded_program_name_prefixes) so the name says the rule. - SEEDED DEFAULTS: LongRunningQueryExclusions.DefaultProgramNamePrefixes = ["SQLAgent - TSQL JobStep"], DefaultLogins = ["NT AUTHORITY\SYSTEM", "NT AUTHORITY\NETWORK SERVICE"]. Lite: the App statics initialise to the seeds and the settings.json reader replaces a list only when its key is PRESENT - so a fresh install / a file without the key evaluates with the seeds, and an operator who empties a box (the window always writes both keys) has cleared that arm for good; Restore Defaults puts the seeds back, not empty boxes. Darling: DarlingAlertSettings returns the seeds - not an empty knob - until the V135 rung gives the knob its store home, so both SKUs evaluate the same population from day one. - excluded_count SPLITS: both reads return excluded_by_program_prefix and excluded_by_login as two scalars over the candidates CTE, counted in SESSIONS (COUNT(DISTINCT session_id) - inside one collection of one server the (server_id, session_id, tran_start_time) identity collapses to session_id; a MARS session with two request rows is one session), the login count taken AND NOT the program flag so a session matching both arms counts once, under the prefix. The card's knob item shows Excluded Count, Excluded By Program Prefix, Excluded By Login and the entries. - Docs: IAlertEngineSettings, the Lite Settings window, settings.sample .json, README.md and Darling/README.md each state the four classes, the two seeds with their grounding, and why the admin login is absent. - Lite's Settings boxes are AlertLrqExcluded*Box so the #1840 wiring census sees them; they are gated on the master switch and reset to the seeds. The census's collection exception names the two new statics. Executed: the Darling read against a live PostgreSQL 18 store (the E2E's four-class snapshot: prefix arm 2, login arm 1 despite two rows, both arms once; mutation to COUNT(*) without AND NOT fails 3 != 1); the exact Lite DuckDB SQL text on DuckDB 1.5.5 (same fixture, plus the %/_ escaping and the literal-* cases); 347 Darling.Tests in the mactest harness. Partial #3653 - item 6 (Q5). Does not close it.
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… PRs across the #3653, #3542 and #3691 waves, #3704, #3710, #3712 and #3735, spliced once (#3751) Sixty-five PRs merged to dev since #3672 (the first splice) across two seats - the coordinator's #3653 brains-review residue lanes and the wave-2 worker's #3542 / #3691 PostgreSQL-target engine lanes - and by rule none touched this file; each returned its entry to a buffer. This is the one splice. Text-only; one file; nothing outside ## [Unreleased] and the definition block moves. [Unreleased] gains a ### Changed between ### Added and ### Fixed (Keep-a-Changelog order) for the nine entries whose subject is a designed behaviour that now behaves differently - routing (#3712 confidence chooses the channel), gating (#3692 tempdb persistence, #3734 blocking/LRQ gates, #3724 peak-and-mean), the wire shape (#3719 one error envelope), the review guard's drift arm (#3673), lineage metadata (#3723) and two grading changes (#3693, #3747). Twenty-two new capabilities append to ### Added (the sixteen PostgreSQL-target fact families and rungs, CONFIG_CHANGED, the recurrence label, the V134/V135 columns, the review traps file, the measurement census); thirty-two defects append to ### Fixed. Order within each section is ascending PR number, as #3672 did. Every entry is one line and verbatim from its buffer (trailing whitespace only). Four PR pairs fold into one entry as the buffers wrote them (#3738+#3746, #3683+#3686, #3681+#3685, #3726+#3729); #3695 carries three entries. #3673 has no issue and cites none, like the file's ninety-six other ref-less entries. Five definitions added ([#3691], [#3704], [#3710], [#3712], [#3735]) at the end of the [Unreleased] run, which stays one ascending run (3517..3735). No archive touched; the split was not run. tools/changelog/changelog_archive.py verify: all PASS (1,483 bold entries, floor 1,329; 1,363 distinct refs resolve; 1,372 definitions; CRLF throughout; 409,263 bytes under the ceiling). Client-hygiene grep over the file: zero hits.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
The tempdb Space alert fired on one collected sample and resolved on the next. It now has to hold for three consecutive collections to be an incident — the #3653 A5 persistence-gate rollout, the tempdb Space slice, on the shared
AlertPersistenceGatethat High CPU adopted in #3282. Partial for #3653 (the A5 line's tempdb arm; the Blocking Wait Time disjunction and the Long-Running Query opt-out are ruling-gated — Q4/Q5 on the issue — and untouched here). References #3653, does not close it.The lie
AlertEngine.CheckTempDbSpaceAsyncread the newesttempdb_statsrow and fired the momentReservedPercent >= TempDbSpaceThresholdPercent, then resolved on the first read under the bar. Measured on one production store class — 43 servers over 14 days — one server fired tempdb Space at or above the 80% bar 22 times, and every run was 1–4 breaching samples spanning at most 206 seconds before the percentage fell back. Twenty-two pages and twenty-two "tempdb Space Resolved" notices in two weeks about a tempdb that was never in trouble for longer than three and a half minutes. That is the exact shape #3282 measured on CPU and fixed with the persistence gate, andAlertPersistenceGate's own header says the built-in checks "adopt the same gate as #3282's own follow-up. One mechanism, not two." This is that follow-up for one arm.Mechanism
The FIRE decision sits behind the gate; everything downstream is unchanged.
CheckTempDbSpaceAsynccomputesbreachingexactly as before, hands it to the gate, and fires only while the record says an incident is open — the threshold compare, the explicit Warning grade (#3660), the cooldown-governed standing-condition reminder, the card, the mute context and theNumericCurrentValueare byte-for-byte what they were. The resolve's falling edge now comes from the gate'sResolveoutcome; with a clear count of 1 that is the same instant it always was.Samples, not sweeps — and on this arm that is the whole fix. The alert sweep runs every 30 s;
tempdb_statslands a row once a minute at the shipped cadence (every five at the light tier). A gate that counted sweeps would reach K = 3 in 90 s of wall clock on ONE collected row — the measured 1–4-sample flap intact behind a gate that looked like it fixed it, which is the trap the CPU arm's own comment names. SoTempDbSpaceInfogainsCollectionTimeUtc: the row'scollection_time, which both SKUs' reads alreadyORDER BY … DESC LIMIT 1— one projected column on a read that already exists, no new read, no new cadence, same one-row index seek (DarlingTempDbSpaceSql, LiteLocalDataService.GetLatestTempDbSpaceAsync). The engine counts an observation only when that instant has advanced past the record'sLastObservedSampleUtc; a null instant (the deprecated Dashboard's local read, every pre-existing fixture) degrades to counting sweeps — weaker, but the alert still fires, the same direction the CPU gate chose for the same null and for the same reason.One mechanism, generalised rather than duplicated. The CPU arm's fresh-sample guard,
AlertPersistenceGate.Evaluatecall and skip-the-write-when-nothing-moved rule are lifted intoObservePersistenceAsync(records, metricName, key, sampleUtc, breaching, breachSamples, clearSamples)→(Outcome, IncidentOpen), andSaveCpuPersistenceAsyncbecomesSavePersistenceAsync(key, metricName, record). Both arms call them. The CPU arm's behaviour is unchanged — every #3282 pin inAlertEngineTestsandBuiltinAlertPersistenceRungTestsexecutes green here (below). A third consumer is a third record dictionary, a third seed line and a third pair of constants, nothing else.The active flag is gone; the persisted record is the "incident open" bit.
_activeTempDbSpaceAlertleft the engine for exactly the reasons CPU's did: an in-memory flag re-announced a standing incident on the first post-restart sweep, and a bool beside the record would be a second source of truth for the same fact. The record is seeded at startup under the new metric name"tempdb Space"— a second row in the existing V118(server_id, metric_name)persistence table, so no migration rung — and an already-open incident gets its cooldown clock stamped at seed, so the reminder waits a full cooldown rather than firing seconds after a restart. Same two-part guard as the CPU seed, same rationale, stated at the site.No-data freezes the gate. The old arm fell through to its resolve branch on a null
tempDband announced "tempdb reserved space back to N/A" — a recovery nobody measured, rendered with the value it could not read. A null read is now neither a breach nor a clear; the streak and any open incident hold until a real row arrives. The"N/A"arm is deleted with it (the resolve is reached only with a non-null row by construction).Why K = 3, and why the clear count is 1
TempDbSpaceBreachSamples = 3, derived from the measured flap: every one of the 22 runs was 1–4 samples; K = 3 keeps 2 of 22 — the two runs that held for three or more consecutive collections, which are the only two an operator could still have seen as TRUE when the message arrived. K = 2 would keep the two-sample runs (most of the noise); K = 4 drops both real ones and buys nothing the measurement asked for. Three collections is ~3 min at the one-minute default and ~15 min at the five-minute tier — the constant's comment says so. A constant and not a knob, forCpuBreachSamples' stated reasons (#3314's half-plumbed number is the outcome to avoid).TempDbSpaceClearSamples = 1, and the comment says why it is NOT the CPU gate's 2:CpuClearSamplesis 2 because #3282 measured the shape it guards — 99% then 23% one minute later inside a real saturation event, a mid-incident dip. The #3653 read counted breach RUNS and said nothing about dips inside a run; there is no evidence a tempdb resolve is ever contradicted by the next sample, and reserved tempdb is a slower gauge than scheduler CPU (it moves when allocation units are freed, not per ring-buffer tick). Picking 2 anyway would be the folklore constant the lineage rule refuses. If the fleet shows resolve/re-fire pairs inside one tempdb event, that is the number that moves, with the measurement beside it. Keeping 1 also means the resolve half of this arm is behaviourally unchanged.Blast radius
TempDbSpaceInfogains a nullable property; the deprecated Dashboard's ownTempDbSpaceInfoconstruction (DatabaseService.NocHealth.cs, frozen twin, no gate) is unaffected — null by default.DarlingAlertReadAdapter.TempDbSpaceSqland Lite'sGetLatestTempDbSpaceAsyncproject one more column.DarlingAlertReadAdapterTests' dialect/table pins on the SQL still hold (nonow(, noN',FROM tempdb_stats).AlertReadFailureSurfaceTests(Under store contention the alert pass fails first and silently, so the self-alert that would report it is the first thing lost #3013 census):AlertEngine.csstays at 14 counted / 6 exempt. No catch block added or removed; the seed's newLoadAlertPersistenceAsyncawait has itsreadClock.Restart()before it, and the tempdb try body restarts the clock after the gate call. The one exemption key moved with the generalised save message ("Could not persist the {Metric} persistence gate"), so the census matches the one catch block that now serves both metrics.BuiltinAlertPersistenceRungTests' text pins on the CPU seed (if (cpuPersistence.Value.State.Firing),_lastCpuAlert[key] = _utcNow();) are untouched — the CPU seed block is byte-identical.LiteAlertForwardingTests.TempDb_ToastBody_MatchesTheOldLoop,TempDb_TheAzureCeiling_…) drove one sweep and asserted a fire; they now driveTempDbSpaceBreachSamplessweeps (their fixtures carry no instant, so they ride the pinned every-sweep-counts fallback). Assertions unchanged.TheReadmeAlertCatalogStatesTheTempDbSampleCountItActuallyUses), the same pin the High CPU row already carries.What it does NOT do
StorageVersionbump, no new knob, noCHANGELOG.mdedit (entry handed to the coordinator).IAlertReadAdapter; the sample instant rides on the existing return type.Test plan
Every touched project builds
-c Release -p:EnableWindowsTargeting=truewith 0 warnings from this change (PerformanceMonitor.Alerting,PerformanceMonitor.Darling.Service,Darling.Tests,PerformanceMonitorLite,Lite.Tests— the latter's one warning is pre-existing on dev inLogTailOverlapThresholdPinTests.cs:95, untouched).The Darling.Tests engine pins were EXECUTED on this machine, not just compiled, through a throwaway net10.0 xunit harness outside the worktree (
AssemblyName=Darling.Tests, referencing the builtPerformanceMonitor.Alerting/Common/Notifications/Darling.Storage, compilingAlertEngineTests.cs,AlertPersistenceGateTests.cs,AlertReadFailureSurfaceTests.cs,BuiltinAlertPersistenceRungTests.cs,RepoFileAdoptionTests.cs,DocCommentHygieneTests.cs+ helpers): 273 tests, 0 failed — the whole ofAlertEngineTests(145, every #3282 CPU pin included), the gate's own 12, the #3013 census's 27 with the moved exemption key, the rung file's 10 (one Viewer-reflecting test excluded, it needs WPF), the LF-reader roster, and the cref/summary hygiene sweep over the tree.Darling.Tests/AlertEngineTests, tempdb section — new (DriveTempDbAsynchelper, theDriveCpuAsynctwin):TempDb_OneOrTwoBreachingSamples_DoNotFire_TheThirdFiresExactlyOnce— K−1 → nothing; K → one fire; the cooldown reminder unchanged.TempDb_AStreakBrokenByOneClearSample_NeverFires_UntilThreeRunConsecutively— breach/clear/breach/breach → nothing; the fourth breach fires (the reset is a reset).TempDb_RepeatedCollectionTime_DoesNotAdvanceTheStreak— the samecollection_timeoffered 3K times → nothing; two fresh rows then fire. The pin that matters most on this arm.TempDb_NoCollectionTime_CountsEverySweep— the documented fallback.TempDb_AMissingRowFreezesTheGate_AndNeverAnnouncesARecovery— null reads after a fire: no resolve, no re-announce; one real clear row resolves with the measured number.TempDb_PersistsTheStreakUnderItsOwnMetric_AndResumesAcrossARestart— the record is under"tempdb Space"with the streak and the instant; a new engine over the same store completes it.TempDb_ARestartDoesNotReAnnounceAnAlreadyOpenIncident— restart withFiring = true: no re-announce; the stamped clock is a cooldown, not a silence; resolves normally.TempDb_TheGateAdvancesUnderSuppression_SoOneUnsuppressedSampleDelivers.TempDb_APersistenceSaveFailure_StillFires_AndIsNotCountedAsASwallowedRead— through the shared save path.TempDbGateDefaults_AreDerivedFromTheMeasuredFlap— 3 / 1 /"tempdb Space".TheReadmeAlertCatalogStatesTheTempDbSampleCountItActuallyUses.Re-shaped to drive the gate (assertions unchanged):
TempDb_FiresAtThreshold_AndResolutionCarriesTheCurrentPercent,TempDb_TheAzureCeiling_SuppressesTheAlert_ThatTheAllocationWouldFire,TempDb_FiresAnExplicitWarning_AndNeverGradesCritical.Lite.Tests/LiteAlertForwardingTests— the two tempdb pins drive K sweeps; compile-verified here, first executed in CI (their harness is the WPFApp's statics, which this Mac cannot host). They are the same shape asTempDb_NoCollectionTime_CountsEverySweep, which executes green.Which component(s) does this affect?
How was this tested?
See the test plan: executed here via the harness (273/0), compile-verified on every touched project, CI is the first execution of the Lite.Tests pins. No SQL Server change — the collector and its
tempdb_statsrow are untouched; the change is read-side and engine-side.Checklist
dotnet build -c Debug)