Repository navigation
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
Merged
Conversation
This was referenced Sep 20, 2026
Closed
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).
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.
Mechanism
The gap. #3734 gave the Long-Running Query alert its opt-out knob —
longRunningQueryExcludedProgramNamePrefixes(prefix, case-insensitive) andlongRunningQueryExcludedLogins(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 theget_alert_settings/update_alert_settingstwins. 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.GetCurrentLongRunningSessionsAsyncthrough the SHAREDLongRunningQueryExclusions.BuildSqlPredicatestranslation, overapplication_name(theprogram_nametwin) andusername(pg_stat_activity.usename, thelogin_nametwin), so an entry means one thing on both engines — prefix for programs, whole name for logins, case-insensitive,%/_escaped beforeILIKE … ESCAPE '\', a NULL column matching nothing. The read mirrors the SQL Server read's shape (DarlingAlertReadAdapter.LongRunningQueriesSqlTemplate): acandidatesCTE flags every over-threshold client backend once per arm; the outer query keeps the unflagged rows underLIMIT $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 returnsexcluded_by_program_prefix_count/excluded_by_login_countas two scalars over the same CTE, one statement, one capture. The login count is takenAND NOTthe 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 ($1server,$2threshold,$3recency floor,$4limit — kept exactly).The evaluator normalises the knob from the SAME two
DarlingAlertSettingsmembers 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
PerformanceMonitor.Darling.Storagedoes not referencePerformanceMonitor.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 theILIKEtext in the reader (the drift the shared builder exists to prevent).count(*)rather thanCOUNT(DISTINCT session_id). The SQL Server read counts DISTINCT sessions because a MARS session can hold two request rows; herepg_stat_activityhas exactly one row per backend and the capture stores one row per backend, so the row IS the session — andbackend_id/pidare nullable on a redacted target, where a DISTINCT over them would drop the very rows the receipt is for.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, andWHERE 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.SQLAgent - TSQL JobStepand the twoNT AUTHORITYlogins 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 throughbackend_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.GetCurrentLongRunningSessionsAsynchas 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 bycollection_time >= $3andLIMIT $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.mdsettings paragraph (:473): "The PostgreSQL twin does not consult the knob" → states that it does, through the same shared predicate builder, withapplication_name/usenameas the twins ofprogram_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/$7overs.application_name/s.usernamewith$1..$4kept;%/_escaped via the shared helper;[]→ both armsFALSE, no operands, and the rendering equals both shipped field texts byte-for-byte (CurrentLongRunningSessionsSqlcontains noILIKE/$5); the CTE shape — filter ahead ofLIMIT $4(index compare),count(*)scalars withAND 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 — nos_lfReadersentry 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_ETLremove the same two (case-insensitive); login entrysvc%removes nothing (no wildcard grammar);LongRunningQueryExclusions.Defaultsremoves nothing (the seeds match nothing here).DarlingPgReadSqlParsesLiveTests.NotQueryFieldsgains the three new non-query string consts, with the census's own idiom (why each is a fragment and where it is verified).AssemblyName=Darling.Tests): the 8 unit pins, the live class and the parse census against PostgreSQL 18 (timescale/timescaledb:2.28.1-pg18, Dockerdesktop-linux, torn down) — all green; plusDarlingPgSessionStatesReaderTests33/33 (the existing shape pins keep reading — the outer alias stayss),LongRunningQueryExclusionsTests34/34,DarlingPgOperationalAlertTests30/30,RepoFileAdoptionTests,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests,StoreSqlClockDisciplineTests,AlertMasterSwitchSurfaceTests,AlertReadFailureSurfaceTests,FleetIdentifierScrubTests,DocCommentHygieneTests76/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 unescapedsvc_etloperand and failed against the builder'ssvc\_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?
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
dotnet build -c Debug)