Skip to content

Blocking Wait Time fires on one snapshot at 3× the bar or on 3 consecutive 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

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/3653-blocking-lrq-gates
Sep 19, 2026

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Mechanism

Blocking Wait Time (Q4). The alert fired on the FIRST fresh dmv_blocking_snapshots snapshot whose summed wait_time_ms reached BlockingWaitSecondsThreshold and resolved on the first one under it. It now sits behind the shared AlertPersistenceGate as a disjunction: one fresh snapshot at or above 3× the bar fires at once; anything between the bar and that multiple must hold for 3 consecutive collections. Both arms ride ONE gate — the bar handed to ObservePersistenceAsync is one sample when the snapshot clears the multiple and BlockingWaitBreachSamples otherwise — so a 3× fire opens the incident on the same persisted record the consecutive arm would, and the clear that follows is the same clear. The fire prepends a gate item whose Fired By field carries single_snapshot_3x / consecutive_k3.

What makes this arm unlike tempdb's: the collector writes rows only while something is blocked, so a quiet cycle writes nothing. Two breaching snapshots with a quiet cycle between them are two episodes; the arm restarts a partial streak when a breaching snapshot lands more than 1.5 collector cadences after the last one counted (CurrentBlockingWaitResult.CadenceMinutes, the number both adapters already resolved for freshness). An OPEN incident is not restarted by a short gap. A STALE snapshot is a clear (the arm's #1812 rule — it is the collector saying the blocking ended), observed per sweep; a NULL read freezes the gate; disabling the gate is not a clear. The in-memory active 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.

Long-Running Query opt-out knob (Q5). New IAlertEngineSettings members LongRunningQueryExcludedProgramNamePrefixes / LongRunningQueryExcludedLogins. The rule is spelled once in LongRunningQueryExclusions: a program name matches by prefix (case-insensitive — job-step names embed the job id, so only a prefix can name the class), a login matches exactly (case-insensitive — a prefix would let svc swallow svc_owner); no wildcard grammar (* is a character, %/_ are escaped before LIKE), a null session value matches nothing, blanks are dropped on the way in. An excluded session is NOT EVALUATED — not read into the decision, not counted, not fingerprinted — the opposite of a mute, so excluding a paging program resolves the open incident. Applied IN THE READ ahead of LIMIT on both SKUs (a candidates CTE flags each over-threshold row once per arm with the shared ILIKE … ESCAPE '\' predicates, the outer query keeps the unflagged rows under the cap and returns the two counts as scalars over the same CTE), because the sessions the knob names are the longest-running by construction and a post-read filter would let them fill the cap and blind the alert.

Seeded from the production read. The 7-day Long-Running Query population on one large production store fell into four classes:

  1. SQL Agent job-step programs (program_name prefix SQLAgent - TSQL JobStep): ~460 sessions a week across 11 single-server jobs, medians 35–62 min → default-excluded by program-name prefix. Anything of theirs that should still page is Name the Agent job on the Long-Running Query card: nightly maintenance should not read as a mystery twelve times #3497's job-naming annotation, not this knob.
  2. 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) → default-excluded by exact login.
  3. The application's admin login: deliberately not excluded — it carries the job wave, but it is also the login real ad-hoc long-runners arrive under; blinding it would hide the second to silence the first, and the job-step prefix already removes its share.
  4. Named humans: 3 sessions a week. Never excluded — they are what the page is for.

So the two lists ship as ["SQLAgent - TSQL JobStep"] and ["NT AUTHORITY\SYSTEM", "NT AUTHORITY\NETWORK SERVICE"] (LongRunningQueryExclusions.DefaultProgramNamePrefixes / DefaultLogins). They are defaults, not constants: Lite's App statics initialise to the seeds and the settings.json reader replaces a list only when its key is PRESENT — a fresh install or a file without the key evaluates with the seeds; an operator who empties a box (the Settings window always writes both keys) has cleared that arm for good; "Restore Defaults" restores the seeds. Darling's DarlingAlertSettings returns the seeds — not an empty knob — until the knob's store home lands, so both SKUs evaluate the same population from day one.

The card's receipt, split by arm. The Long-Running Query card gains a knob item — Excluded Count, Excluded By Program Prefix, Excluded By Login, and the entries — so an operator can see the knob working and which default did the work. Counts are sessions (COUNT(DISTINCT session_id) inside the one snapshot the read looks at; the production read's (server_id, session_id, tran_start_time) identity collapses to session_id inside one collection of one server, and a MARS session with two request rows is one session), and a session matching both arms counts once, under the program prefix (the login count is taken AND NOT the program flag), so the two counts sum to the sessions removed.

Evidence (one production store class, 43 servers; one large production store for the knob's defaults)

  • Blocking Wait Time: 102 episodes, 97 single-snapshot at ~72 s cadence, including the p99 — 1,509 s across 145 sessions — gone by the next collection. A plain K=3 gate drops exactly those.
  • Long-Running Query: 191 distinct sessions ≥ 30 min in 7 days, p90 seen in 6,192 snapshots — permanent background requests no gate can separate from a runaway query. The four-class split above is the 7-day read of that population on one large production store.
  • The knob evaluates against the snapshot population (query_snapshots / v_query_snapshots, the rows the arm has always read). collect.long_query_completions is not consulted anywhere in the LRQ alert's path, and no LRQ alert description cites it.

Why this shape

  • One gate for both blocking arms (a per-observation bar, not a second gate rule) — AlertPersistenceGate's header asks for one mechanism.
  • Cadence-based episode detection rather than counting snapshots inside the freshness window — otherwise three separate one-snapshot flaps inside ten minutes would fire as "consecutive".
  • Pre-cap SQL exclusion rather than the excludedDatabases post-read shape — the cap problem above.
  • Prefix for programs, exact for logins — the shape of the values themselves; the read found no class a wildcard grammar was needed for.
  • Two scalars over one CTE rather than a second read — one statement, one snapshot; Npgsql positional parameters do not survive a batch.
  • Seeds live on the hosts, not in the engine: the engine never re-seeds an empty list, because present-and-empty is the operator's decision and the absent/present distinction is a settings-store fact.

What it does NOT do

  • Does not gate Blocking Detected (the count gate) — untouched.
  • Does not give the knob a Darling store home, Viewer boxes or an MCP surface — that is two text[] columns on config_alert_settings, a migration rung (V135), and one un-landed rung rides at a time (cpu_utilization stores each sample's UTC instant beside its server-local stamp and server_properties stores the engine's time-zone id beside its offset (Darling V134 / Lite v63), so UTC-window readers stop deriving an offset that is an hour wrong across DST (#3653 item 13, Q7 + Q8) #3730's V134 is in flight). PR-B carries the rung, StoreConfigProvider seed/read, the Viewer's SettingsWindow boxes and ViewerDataService.AlertSettings, DarlingAlertReader, and the get_alert_settings / update_alert_settings twins on both SKUs — neither SKU publishes the knob over MCP in this PR because McpAlertSettingsKeyTests derives Lite's payload shape from Darling's source, so the two must move together. Darling evaluates with the seeded defaults meanwhile.
  • Does not apply the knob to the PostgreSQL LRQ twin (DarlingWorker.EvaluatePgLongRunningQueryAsync) — stated in the Darling README.
  • Does not apply the knob to the High CPU card's maintenance probe (deliberate; a backup under an excluded login is still what burns the CPU).
  • Does not touch the long_query_completions collector (default-off by CollectorScheduleDefaults); the fleet-wide empty table is reported separately.
  • Does not change the K/N/clear numbers into knobs.

Tests

  • AlertEngineTests: 19 blocking-wait pins (3× fires at once and says so; 1.5× silent for two, fires on the third as consecutive_k3; a streak one short that triples fires as single_snapshot_3x; same collection_time ×9 = one observation; quiet-collection gap restarts; 72 s adjacent stays consecutive; short gap holds an open incident; no cadence degrades to freshness-window counting; stale resolves once and writes nothing more; stale from cold writes nothing; stale resets a partial streak; null freezes an open incident; disable is not a clear; suppression advances the gate; restart resumes / does not re-announce; constants pinned to the ruling; Darling README row pinned to the constants). 8 LRQ pins (knob travels normalised; the seeded defaults remove the four-class fixture's background and keep the admin login's ad-hoc query and the human, counts 2/2 with the job-step-as-SYSTEM session counted once; excluded session not evaluated and the card counts it by arm; login exact vs program prefix, * literal; excluding everything resolves; empty knob = no item; knob that removed nothing says 0/0/0; CPU probe ignores the knob).
  • LongRunningQueryExclusionsTests (rewritten): prefix / exact truth tables, Classify counts-once rule, the seeds pinned to the four classes (and to NOT containing an admin login), normaliser, prefix vs exact LIKE operands, both-arm SQL with FALSE for an empty arm and operand ordinals, both reads' source pins (two flags, COUNT(DISTINCT session_id), AND NOT, no trace of the single-flag shape), both hosts' seeding and Lite's absent-vs-present rule.
  • DarlingAlertReadAdapterTests live E2E (Darling PostgreSQL tests): a four-class snapshot — job step under the admin login, job step running as SYSTEM, NETWORK SERVICE with two request rows, the human — read with None (all rows, 0/0), with Defaults (human only, 2/1), and with the login default cleared (NETWORK SERVICE evaluated again, 2/0).
  • DarlingAlertTuningKnobsTests: Darling's seam returns the seeds. LiteAlertForwardingTests: Lite forwards both lists. AlertSettingsControlWiringTests: the two new AlertLrqExcluded*Boxes are loaded, gated, reset and persisted (collection exception named).
  • Executed here: 347 Darling.Tests in a mactest harness (including the live E2E against PostgreSQL 18 in Docker); the exact Lite DuckDB SQL text on DuckDB 1.5.5 with the same fixture plus the %/_ escaping and literal-* cases (10/10). Mutation: COUNT(*) without AND NOT fails the live E2E 3 ≠ 1. Red-first: 15 blocking pins red against the pre-gate arm; 3 LRQ pins red with the knob unwired. Lite.Tests: CI is first execution (WPF-hosted).

Partial #3653 — item 6 (Q4, Q5 — Darling store home + MCP twins follow as V135)

… 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.
…ied 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.
…d: 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
erikdarlingdata merged commit 1d25100 into dev Sep 19, 2026
6 of 7 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3653-blocking-lrq-gates branch September 19, 2026 12:52
erikdarlingdata added a commit that referenced this pull request Sep 19, 2026
… adds two text[] columns to config_alert_settings whose DEFAULT is the production read's seeds, wired through the service seed/read, the Viewer's Settings window and select/upsert, the viewer probe's top arm, and both SKUs' get_alert_settings / update_alert_settings twins (#3653 A5, Q5) (#3736)

#3734 gave both SKUs the knob - program_name PREFIXES and exact logins
whose sessions the Long-Running Query alert does not evaluate, seeded
from the 7-day production read (SQLAgent - TSQL JobStep; NT AUTHORITY\
SYSTEM and NT AUTHORITY\NETWORK SERVICE) - editable on Lite, and on
Darling the seeds and nothing else, because a Darling alert knob has no
rung-free home: the store row is authoritative (ApplyToConfig replaces
config.Alerts wholesale), so a darling.json-only member is dead, and
McpAlertSettingsKeyTests pins both SKUs' get_alert_settings to one
shape, so neither could publish it until Darling could store it. This
is that store home, one rung after #3730's V134.

V135: long_running_query_excluded_program_name_prefixes and
long_running_query_excluded_logins, text[] NOT NULL, DEFAULT the two
seeded arrays - the one knob rung on this row that ships a non-empty
default, so a pre-rung row reads as exactly what DarlingAlertSettings
returned before the rung and what Lite ships, and the two SKUs and the
two sides of the rung evaluate one population. An operator's cleared
list is stored as an explicit '{}' and honoured: present-and-empty is
a decision, and nothing re-seeds it. AlertsConfig and the Viewer's
AlertSettingsRow initialise to the same seeds (pinned equal to the
rung's DEFAULT and to LongRunningQueryExclusions.Default*). No reload
beacon (V17's trigger), no GRANT (table-level), IF NOT EXISTS.

Plumbing, all at the appended ordinals: StoreConfigProvider seeds them
as $69/$70 and reads them at 67-68 (a column selected but not read
resets the knob on every worker start); the Viewer's select/upsert bind
them as $68/$69 and read 67-68, its Settings window gets two boxes
(AlertLrqExcluded*Box, so the #1840 wiring census sees them: gated on
the master switch, Restore Defaults puts the SEEDS back); the viewer
probe gains the V135 sentinel as the top arm (V134's top-rung claims
move here, as V134 moved V133's); DarlingAlertReader reads 67-68 for
get_alert_settings; update_alert_settings accepts
long_running_query.excluded_program_name_prefixes / .excluded_logins
through the shared normaliser (empty array = cleared arm); Lite's
get_alert_settings publishes the same two keys. Every description
states the two rules, the four classes, the seeds' grounding and why
the admin login is not a default.

TimeHonestyRungLivePostgresTests' simulated climb drops V135's columns
and stamp too and expects two rungs: the applier skips anything at or
below MAX(version), so a "one rung short" store must be short of the
TOP, which this rung now is.

Executed on this Mac: the real Darling.Tests.dll (WindowsDesktop stripped)
against PostgreSQL 18 in Docker - 402/402 across the rung, probe, seed/
read, viewer bind, MCP and engine classes (the seed -> read scratch
round-trip carries a cleared login list back empty and a padded prefix
list back normalised; the store's column DEFAULTs read back from the
catalogue as the seeds); full suite 11,451 with 191 platform reds
(PresentationFramework / SqlClient ConfigurationManager / Windows
paths), the same baseline as before this change.

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.
erikdarlingdata added a commit that referenced this pull request Sep 20, 2026
…n both SQL Server SKUs: the shared list becomes the candidates CTE's third flag through the knob's predicate builder, its count rides the card beside the knob's, and the two adapters' post-read Where blocks go (partial #3742) (#3772)

Every SQL Server Long-Running Query read was ORDER BY total_elapsed_time_ms
DESC LIMIT maxResults and THEN, in C#, dropped the rows whose database is on
excludedDatabases — the page cut before the exclusion, so an excluded
database's sessions consumed the cap and the alert came back short or empty
while un-excluded long-running sessions existed. #3734 refused exactly this
shape for the opt-out knob and named the database list as the contrast; that
contrast described the defect.

The list now rides LongRunningQueryExclusions.BuildSqlPredicates as a third
arm (exact, case-insensitive, one ILIKE … ESCAPE term per entry — the login
arm's rule, the text both stores already execute for the knob), operands
appended after the logins so no ordinal the knob bound moves. Both reads
project it as excluded_by_database, drop it in the outer WHERE with the
knob's two, and count it AND NOT both knob flags — the database arm is last,
so a session the knob would have removed anyway is the knob's and the three
counts sum to the sessions removed. LongRunningQueryReadResult carries
ExcludedByDatabase; the card gets its own "Excluded By Database" item,
rendered only when the list is set (it is empty by default). The
three-argument overload is unchanged, so #3770's PostgreSQL-target call
sites are byte-identical; that read still filters after its cap and is
#3743's follow-through.

Executed here: 36 LongRunningQueryExclusionsTests, 176 AlertEngineTests, the
live DarlingAlertReadAdapterTests E2E against PostgreSQL 18 (six excluded-
database sessions ahead of one real one at cap 5 → a page of five matches;
mutation with the old WHERE fails on [86, 85, 84, 83, 82]), the exact Lite
DuckDB text on DuckDB 1.5.5 with the same fixture (same mutation, same red),
and the census pins. Lite.Tests twin pins are CI-first (WPF-hosted).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant