Skip to content

The PostgreSQL Long-Running Query twin honours the program/login opt-out knob, so a setting an operator can see, edit and read back on a PostgreSQL target stops doing nothing there — shared predicate builder over application_name/usename, ahead of the row cap, counts by arm on the card (#3743, partial #3653) - #3770

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/3743-pg-lrq-exclusions
Sep 20, 2026

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Mechanism

The gap. #3734 gave the Long-Running Query alert its opt-out knob — longRunningQueryExcludedProgramNamePrefixes (prefix, case-insensitive) and longRunningQueryExcludedLogins (exact, case-insensitive), applied in the read ahead of the row cap and counted by arm — and #3736 gave it a Darling store home (V135) and the get_alert_settings / update_alert_settings twins. Both said the same thing in their "does not" section: the PostgreSQL LRQ twin (DarlingWorker.EvaluatePgLongRunningQueryAsync) does not consult the knob. So on a PostgreSQL target the setting was visible, editable and read back through the MCP, and did nothing: a monitored server whose longest sessions are a permanent ETL or replication worker under a service role filled the five-row page with them on every sweep, and the operator's exclusion — which worked on the SQL Server twin from the same settings row — was silently ignored.

The fix. The knob now rides INTO DarlingPgSessionStatesReader.GetCurrentLongRunningSessionsAsync through the SHARED LongRunningQueryExclusions.BuildSqlPredicates translation, over application_name (the program_name twin) and username (pg_stat_activity.usename, the login_name twin), so an entry means one thing on both engines — prefix for programs, whole name for logins, case-insensitive, % / _ escaped before ILIKE … ESCAPE '\', a NULL column matching nothing. The read mirrors the SQL Server read's shape (DarlingAlertReadAdapter.LongRunningQueriesSqlTemplate): a candidates CTE flags every over-threshold client backend once per arm; the outer query keeps the unflagged rows under LIMIT $4 — ahead of the cap, 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 — and returns excluded_by_program_prefix_count / excluded_by_login_count as two scalars over the same CTE, one statement, one capture. The login count is taken AND NOT the program flag so a session matching both arms counts once, under the prefix, and the two counts sum to the sessions removed. The operands bind as $5… after the four fixed parameters ($1 server, $2 threshold, $3 recency floor, $4 limit — kept exactly).

The evaluator normalises the knob from the SAME two DarlingAlertSettings members the SQL Server arm reads (the V135 columns, hot-reloaded by reference), and renders the receipt on the PostgreSQL card with the SQL Server twin's own item — AlertContextBuilders.BuildLongRunningQueryExclusionItem, same labels (Excluded Count / Excluded By Program Prefix / Excluded By Login, plus the entries) — under the same rule: only when the knob is SET, so an operator who cleared both lists does not read "Excluded: 0" on every card. Annotation, never suppression: the fire is decided on the rows the read returned.

Why this shape

  • The reader takes the builder's OUTPUT (two predicate texts + operands), not the knob record. PerformanceMonitor.Darling.Storage does not reference PerformanceMonitor.Alerting, and should not — that reference would pull Notifications and Analysis into the storage layer. The Service-side evaluator calls the shared builder over three reader constants (ExclusionProgramNameColumn = s.application_name, ExclusionLoginNameColumn = s.username, ExclusionFirstParameterOrdinal = 5), so the translation is still spelled once, in the builder, and the reader cannot drift from it. Rejected: adding the project reference (layering), and re-deriving the ILIKE text in the reader (the drift the shared builder exists to prevent).
  • count(*) rather than COUNT(DISTINCT session_id). The SQL Server read counts DISTINCT sessions because a MARS session can hold two request rows; here pg_stat_activity has exactly one row per backend and the capture stores one row per backend, so the row IS the session — and backend_id / pid are nullable on a redacted target, where a DISTINCT over them would drop the very rows the receipt is for.
  • One SQL shape for both the empty and the set knob. An empty arm is the builder's literal FALSE, so the two shipped field renderings (CurrentLongRunningSessionsSql / …BackupsIncluded, which the parse census checks) are byte-for-byte what the host sends on a cleared knob, and WHERE NOT (FALSE OR FALSE) returns exactly the pre-The PostgreSQL Long-Running Query twin ignores the program/login opt-out knob: the setting is visible, editable and read back, and does nothing on a PostgreSQL target #3743 rows. The pre-knob filters (non-client backends, the four maintenance tags, idle-in-transaction, the shared backups switch) all sit INSIDE the CTE, so a flagged row is one the alert would otherwise have evaluated.
  • The seeds are left alone. SQLAgent - TSQL JobStep and the two NT AUTHORITY logins name SQL Server internals and match nothing on PostgreSQL by construction — correct, and the []-excludes-nothing rule holds. Whether the PostgreSQL side wants seeds of its own (a replication role; autovacuum-style workers are already out through backend_type) is a production read for the monitor seat, not this lane's guess — stated in the README and the evaluator's doc comment.

Blast radius

DarlingPgSessionStatesReader.GetCurrentLongRunningSessionsAsync has one caller (the PostgreSQL LRQ evaluator) and gains three parameters and a result record (LongRunningSessionsReadResult); the two shipped field texts change shape (CTE) but render the same rows for an empty knob. The SQL Server read, the engine, the knob's store home, the Viewer and the MCP twins are untouched. Alert-path read shape: still one statement over the single latest capture, bounded by collection_time >= $3 and LIMIT $4; the CTE adds two boolean columns over the same rows and two scalar counts over the same CTE — the SQL Server twin's cost, and sub-second by shape.

What it does NOT do

README

Darling/README.md settings paragraph (:473): "The PostgreSQL twin does not consult the knob" → states that it does, through the same shared predicate builder, with application_name / usename as the twins of program_name / login_name, ahead of the row cap, receipt rendered the same way, seeds match nothing there. Engine-coverage table's Long-Running Query row: the PostgreSQL opt-out list gains the knob.

Tests

  • PgLongRunningQueryExclusionTests (new, 8 unit pins) — every SQL pin renders the reader's text through the SAME builder call the host makes and asserts the builder's output is in it (never a hand-typed predicate): both arms at $5/$6/$7 over s.application_name / s.username with $1..$4 kept; % / _ escaped via the shared helper; [] → both arms FALSE, no operands, and the rendering equals both shipped field texts byte-for-byte (CurrentLongRunningSessionsSql contains no ILIKE / $5); the CTE shape — filter ahead of LIMIT $4 (index compare), count(*) scalars with AND NOT, pre-knob filters inside the CTE; a blank arm text refused; the evaluator threads the knob from the shared settings through the shared builder over the reader's constants (single-line raw-file anchors — no s_lfReaders entry needed); the receipt equals the SQL Server twin's item and is absent for a cleared knob; the README lines.
  • PgLongRunningQueryExclusionLiveTests (new, [Collection("live-postgres")], DARLING_TEST_PG-gated, LiveStoreCleanup) — plants one capture: ETL worker (etl-worker / svc_etl, both arms, longest), report runner (report-runner / svc_etl, login arm only), user query (psql / analyst). Knob {etl-} / {svc_etl} → the user query alone, counts 1 / 1 (counts-once); [] → all three, 0 / 0; cap of ONE with the knob set → the user query, not the ETL worker (ahead of the cap); ETL- / SVC_ETL remove the same two (case-insensitive); login entry svc% removes nothing (no wildcard grammar); LongRunningQueryExclusions.Defaults removes nothing (the seeds match nothing here).
  • DarlingPgReadSqlParsesLiveTests.NotQueryFields gains the three new non-query string consts, with the census's own idiom (why each is a fragment and where it is verified).
  • Executed here (mactest harness inside the worktree, net10.0, AssemblyName=Darling.Tests): the 8 unit pins, the live class and the parse census against PostgreSQL 18 (timescale/timescaledb:2.28.1-pg18, Docker desktop-linux, torn down) — all green; plus DarlingPgSessionStatesReaderTests 33/33 (the existing shape pins keep reading — the outer alias stays s), LongRunningQueryExclusionsTests 34/34, DarlingPgOperationalAlertTests 30/30, RepoFileAdoptionTests, LivePostgresCollectionHygieneTests, LiveCleanupConversionRatchetTests, StoreSqlClockDisciplineTests, AlertMasterSwitchSurfaceTests, AlertReadFailureSurfaceTests, FleetIdentifierScrubTests, DocCommentHygieneTests 76/77 (the one red is the harness's own csproj in the tree — a known artifact). Red-first: the first run of the ordinals pin expected an unescaped svc_etl operand and failed against the builder's svc\_etl — the pin was wrong, the builder right; fixed the pin. Builds: Service, Storage, Darling.Tests, Lite.Tests — 0 warnings.

Closes #3743. Partial #3653 — item 6 (Q5, the PostgreSQL twin).

Which component(s) does this affect?

  • Lite
  • Darling
  • Lite Tests
  • Darling Tests
  • SQL collection scripts
  • Documentation
  • Full Dashboard (deprecated)
  • CLI Installer (deprecated)

How was this tested?

See Tests above — unit pins and the live class executed on this machine against PostgreSQL 18 in Docker; CI's Windows job is the first execution of the rest of the suite.

Checklist

  • I have read the contributing guide
  • My code builds with zero warnings (dotnet build -c Debug)
  • I have tested my changes against at least one SQL Server version (PostgreSQL 18 store; no SQL Server surface touched)
  • I have not introduced any hardcoded credentials or server names

…out knob through the shared predicate builder — application_name/usename, ahead of the row cap, with the excluded-by-arm counts on the card (#3743, partial #3653)
@erikdarlingdata
erikdarlingdata merged commit 039c9ed into dev Sep 20, 2026
6 of 7 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3743-pg-lrq-exclusions branch September 20, 2026 01:29
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