Skip to content

Say which transport recovers a truncated deadlock report, and pin what the other loses (#3009) - #3028

Merged
erikdarlingdata merged 8 commits into
devfrom
fix/3009-rds-truncation-doc
Sep 5, 2026
Merged

erikdarlingdata merged 8 commits into
devfrom
fix/3009-rds-truncation-doc

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

Closes #3009. (Closing keywords do not fire on a dev merge, so this is a statement rather than a trigger.)

What the comment claimed

PgDeadlockLogParser.Extract justified skipping an unparseable block with "it will be read whole on the next pass, because the window overlaps." That holds for the pg_read_file route and is false for the RDS log-API route, which is consume-once: RdsLogSource keeps a resume marker per (instance, file) and DownloadDBLogFilePortion starts the next call where the last one stopped. There is no overlap and no next pass, so a report straddling a chunk boundary is not completed for the life of that marker. The comment told the next reader that half its callers get a recovery they do not get.

What changed

Comments only. No SQL, no rung, no behaviour.

Extract's remarks now separate two claims the old text ran together: truncation at a window edge is ordinary on both transports, and recovery from it happens on one. They also name both loss shapes, because the second is the one a reader would not predict — a chunk ending before DETAIL: matches nothing and the report is lost whole, while a chunk ending mid-DETAIL still matches, since the continuation group accepts however many tab-indented lines arrived, and a row lands carrying only the participants, resources and statements that were inside the chunk.

The same recovery guarantee appeared in eleven more places, all corrected. Maintainer-facing: FromBlock's block comment; both places the dedupe hash explained itself by an overlap the managed route does not have; PgDeadlocksCollector's class doc, which named both routes in one sentence and then asserted the overlapping window for the pair; the deadlock_hash column comment in the definition both routes write through; V103's and V104's rung docs in PgMigrations.cs; the pre-existing hash test's doc; DarlingPgDeadlockReader's class doc, its times_seen column comment and DeadlockDetailSql's reason for its DISTINCT ON; and the viewer's Activity-tab read.

Consumer-facing, and the reason this went a round further than a comment fix normally would: get_pg_deadlocks' MCP tool description and the times_seen note in its payload are fed to whatever agent calls the tool. Both said the collector re-reads an overlapping tail so the same report is seen several times and times_seen reports that. On the consume-once route a report is seen once, so times_seen is 1 and stays 1 — and a consumer told the number is a read-window artifact reads that 1 as "seen once so far, expect more" when it means "this is all there will ever be". The error ran toward false reassurance, in the one place a reader cannot see the surrounding argument. Both now split the transports and say so in those words.

The grouping, the DISTINCT ON and the occurred_at windowing are all still correct on both routes; only the reasons given were transport-blind, and they are scoped rather than removed.

The claim was swept for across 2686 tracked files, by mechanism (overlap, case-insensitively) rather than by the phrasings already known, which is how the first pass missed the sites spelling it OVERLAPS. Deliberately left: server-tabs.js's times_seen column label carries no claim; and the get_pg_plans siblings, where the description warns a consumer that captures may EXCEED the executions it represents — on the consume-once route that error runs toward caution rather than toward false reassurance, and the field's own instruction (get_pg_top_queries.calls is the authority) holds on both transports.

Extract's closing paragraph carried a separate, newer staleness: it said RdsLogSource.ReadNewestAsync "advances and persists its resume marker as a side effect of the fetch, before this parser is reached," so a log_timezone refusal lost every readable sibling in its window. #3008 moved that commit after the write, and RdsDeadlockIngestor now keeps the parse ahead of the commit specifically so a refusal leaves without it — the next cycle asks for the same window and the siblings survive. That paragraph now says so, and says why the truncation is the case #3008 did not close: a refusal fails and withholds the marker, while a truncated block parses, stores and commits, so the marker advances on the strength of a cycle that succeeded by every signal the ingestor has.

PgPlanLogParser and its tests are deliberately left alone. They claim only that a bounded window truncates, which is true of both transports, and never claim the cut block comes back — the shared premise without the false conclusion. PgDeadlocksCollector.ReadAsync's in-loop comment and PgLogTimezoneUnsupportedException's remarks are already transport-scoped and correct. The viewer, reader and MCP-description copies describe why the READ dedupes, which is still necessary, and are a step removed from this.

The pins

Two, as distinct tests, because the shapes fail in opposite directions and one test covering the pre-DETAIL cut would pass a fix that left the mid-DETAIL cut in place.

AChunkCutBeforeTheDetailLineLosesTheReportWhole asserts the total loss with the uncut slab beside it, so the Assert.Empty cannot pass because the fixture never parsed. AChunkCutMidDetailStoresAnUnderReportingRow_RatherThanNothing asserts that the fragment PARSES, matches the whole report on timestamp, victim, participant count and lock modes, and under-reports on the columns carrying the evidence: one of the two locked resources, and neither participant's SQL.

That second pin also records why no consistency check on the stored row can catch this. ParticipantCount is counted from the same edges GraphText holds, so a fragment lowers both together and they never disagree. What a fragment does leave is a participant count exceeding its edge count, which a whole report — one wait edge per participant — cannot produce.

Each pin was proven red independently. (?:\t[^\n]*\n)* → + in s_deadlockBlock fails the mid-DETAIL pin on Assert.Single; counting participants from Process N: headers instead of edges fails it on Assert.Equal, a different assertion. Both left the pre-DETAIL pin green. The pre-DETAIL pin's own Assert.Empty is not reachable by a source mutation, and that is a property of the defect rather than a weak test: the fragment contains no DETAIL: and no terminating newline, so the block pattern cannot match however permissive its detail group is made, and handing FromBlock that fragment directly still returns null because there is no wait edge in it. Its discrimination is proven by moving the cut instead, which fails it on the fixture guard.

Measurement, and why the marker withhold is not here

The issue's deciding measurement was not reachable from this work: the PostgreSQL and Aurora targets live in a store it had no connection to, and both stores it could reach hold only SQL Server targets — 85 of 85 servers, with get_pg_deadlocks answering not_collected as a permanent engine capability gap on each. A SQL Server get_deadlocks call on the same store returned real rows, so the path and the query form work; there was simply no collect.pg_deadlocks row in reach to count.

#3030 has since measured the store directly, and it does not give this PR its frequency — it removes the ground the question stood on. That issue found collect.pg_deadlocks holds zero rows, ever, across 85,549 SUCCESS runs of the RDS route since its 2026-08-30 cutover, while the engines' own counters advanced six times on four targets in the same window: a fleet base rate around 1.2 deadlocks/day against a capture rate of 0%.

That is not this defect. Both truncation shapes require a report to reach the parser in order to be cut, and nothing is reaching it. So the honest position is narrower than "the frequency is now known": #3009's truncation rate is not merely unmeasured but currently unmeasurable, because a truncated row cannot be observed in a table with no rows. What these corrections fix is a comment that will mislead the next reader, and a latent loss that starts costing data the day #3030 is fixed — not an active one.

Separately, the measurement as this issue frames it would not have decided anything even with rows to count. Comparing participant_count against the row's own edge list compares a number with the thing it is derived from. The signal that survives in stored data is the cycle arithmetic above, and the pre-DETAIL loss leaves no row at all, so it is only countable against a log window this transport has already consumed.

One caveat these pins inherit, and #3030 names it independently: every fixture here is captured from a self-hosted PostgreSQL 17.11 target. Whether the managed engine's log_line_prefix produces the same block shape is untested, and #3030 lists that as a candidate cause for the 0% capture.

One note for whoever does build the withhold: RdsLogSource.LogChunk already carries MoreAvailable, which separates "the API cut this chunk on its own budget and more is available now" from "this chunk ended at the end of the readable log." That splits the wedge-guard problem where it actually divides — the first is completable immediately, the second is waiting on the server and is the shape that can wedge on a stopped instance. Note that no caller reads it today, in either ingestor, so a withhold would be its first consumer and nothing currently depends on it behaving as documented.

The other false claim in the same string

get_pg_deadlocks' description also said PostgreSQL writes a deadlock report "unconditionally - there is no setting that suppresses it". That is false. log_error_verbosity = terse drops the DETAIL field, and DETAIL is the entire graph — every wait edge and every participant's SQL. The ERROR: deadlock detected line is still written, so the log looks like it holds deadlocks while the block pattern, which requires the DETAIL group, matches nothing: zero rows from a server that is deadlocking, in the shape of a server that is not.

What was true is kept — a deadlock report needs nothing enabled on the target, no preload and no restart, which is the real contrast with auto_explain. Needing nothing enabled is not the same as being unsuppressable, and the four places that conflated the two now separate them: the parser's and the collector's class docs, the MCP description, and the viewer panel's doc.

That last one is why this was worth folding in rather than filing. Both the description and the viewer panel enumerate what to check when the result is empty, and both omitted the parameter that turned out to be the answer — log_error_verbosity and log_min_messages appeared zero times across all 2688 tracked files before this commit. #3030's cause is a logging-parameter mismatch nobody checked, and a false reassurance in agent-facing text is a plausible reason nobody thought to. Same defect class as the recovery guarantee this PR exists to fix: an absolute in consumer-facing text that steers an investigation away from the real cause.

Base

origin/dev is merged in rather than rebased, so no force-push was needed. dev had moved from 114e8cb4e to ba7835234 across 21 files, none of them this branch's — including #3017's rewrite of the RDS ingestors. That rewrite changes IngestAsync's return type but not its ordering, so this branch's claim that a parse refusal leaves without committing the marker was re-verified against the merged tree and still holds: StoreAsync runs ahead of CommitResume, and the tolerant catch still wraps only the fetch.

Review dispositions

Both findings from the automated review are addressed above: the incomplete sweep is now complete (PgDeadlocksCollector's class doc and V104's rung doc were the two it named, plus two adjacent instances of the same claim it did not), and the sweep missed them originally because it matched phrases case-sensitively while the collector spells it OVERLAPS.

Review then caught the same over-claim a third time, and it was right: the tool description and the times_seen note asserted that on RDS times_seen "is 1 and stays 1" and "no further sightings are coming", which is stronger than what this branch's own reader doc ("normally arrives ONCE") and V104 ("repeats less often and for other reasons") say about the same behaviour. The resume position lives in the collector process, so a restart re-reads a bounded tail, and since #3008 a window whose write did not land is offered again — either can push times_seen above 1 there. Both strings now hedge to match, keeping the warning that matters (a low value on that route is the ordinary state, not a partial count) and dropping the false guarantee. An absolute was worst in exactly those two strings, because they reach a consumer that cannot see the reasoning behind them.

The style nit on System.Text.RegularExpressions.Regex is declined: PgDeadlockLogParserTests.cs has no using System.Text.RegularExpressions; and its pre-existing s_blockCount uses the identical fully-qualified form, so the new s_edgeCount matches the file's own convention rather than departing from it.

The missing CHANGELOG.md edit is intentional. With several lanes appending to the same [Unreleased] block it is the queue's serialization point, so the entry text ships in this description instead.

CHANGELOG entry

Under [Unreleased] → ### Fixed:

  • Corrected PgDeadlockLogParser's account of what a truncated deadlock report costs. The parser's remarks claimed a report cut at a read's edge is recovered on the next pass; that is true of the pg_read_file route's overlapping window and false of the consume-once RDS log-API route, where the resume marker has already advanced past the fragment. The two loss shapes — a report lost whole, and a report stored under-reporting its participants and statements — are now named and pinned, and the same claim is corrected in the collector's remarks and in the two rung docs that repeated it. (#3009)

PgDeadlockLogParser.Extract justified skipping an unparseable block with
"it will be read whole on the next pass, because the window overlaps."
That holds for the pg_read_file route and not for the RDS log-API route,
which is consume-once: RdsLogSource keeps a resume marker per (instance,
file) and DownloadDBLogFilePortion starts the next call where the last
one stopped, so a report straddling a chunk boundary is never completed.

Extract's remarks now name the asymmetry and both loss shapes. A chunk
ending before DETAIL: matches nothing and the report is lost whole. A
chunk ending mid-DETAIL still matches, because the continuation group
accepts however many tab-indented lines arrived, so a row lands carrying
only the participants, resources and statements inside the chunk. The
same correction goes to FromBlock's comment, to both places the dedupe
hash explained itself by an overlap the managed route does not have, and
to V103's rung doc, which asserted the overlap for both transports.

Two pins carry the shapes as distinct cases, because they fail in
opposite directions and a test covering only the first would pass a fix
that left the second. The mid-DETAIL pin also records why a row cannot
be checked against its own graph: ParticipantCount is counted from the
same edges GraphText holds, so a fragment lowers both together. What a
fragment does leave is a participant count exceeding its edge count,
which a whole report -- one edge per participant -- cannot produce.
Comment thread Lite.Tests/PgDeadlockLogParserTests.cs
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This is a comment/test-only PR (confirmed via git diff — no SQL, no rung, no runtime behavior touched), and the new tests trace correctly against the actual parser logic (s_deadlockBlock/s_edge regexes, FromBlock's participant/resource/hash derivation) — I traced both new fixtures by hand against PgDeadlockLogParser.cs and the assertions match actual behavior.

Two things worth a look:

1. The fix is incomplete — the same corrected claim still appears twice, unfixed, elsewhere in the codebase. The PR's whole point is that "window overlaps → a report cut at the edge is whole on the next read" is false for the RDS log-API route. That claim was corrected in Extract, FromBlock, DeadlockHash's doc, HashOf's doc, and V103's rung doc — but two more instances of the identical blanket claim survive untouched:

  • Darling/PerformanceMonitor.Darling.Storage/PgMigrations.cs lines 2644-2645, V104's "Why the index earns its place" paragraph — a few lines below the now-corrected V103 doc in the same file.
  • PerformanceMonitor.Collectors/PgDeadlocksCollector.cs lines 31-34, the class-level XML doc, which explicitly says the overlap/recovery applies "by the same two routes... chosen at dispatch."

I couldn't leave inline comments on these since they're outside this PR's diff, but they're real drift given the PR's own stated goal (I posted an inline nit on the one in-diff issue I found, a style nit in the new test file's Regex usage).

2. The PR body includes a specific CHANGELOG entry to add under [Unreleased] → ### Fixed, but CHANGELOG.md isn't in the diff. Only three files changed: PgMigrations.cs, PgDeadlockLogParserTests.cs, PgDeadlockLogParser.cs. Worth confirming whether the changelog entry was intentionally dropped or just missed before merging.

No correctness, parity, security, or performance issues found — this doesn't touch T-SQL, and Lite has no counterpart file for any of the three changed files (the parser is shared collector code with one shared test project), so there's no Lite/Darling drift here beyond the two stale-comment spots above.

Three more copies of the recovery guarantee, two of them found in
review. PgDeadlocksCollector's class doc was the worst of them: it names
both routes in one sentence and then asserts the overlapping window for
the pair. V104's index rationale asserts it twenty lines from the V103
doc corrected in the previous commit, and the deadlock_hash column
comment in the definition both routes write through explained itself the
same way. The first sweep missed all three because it matched phrases
case-sensitively and the collector spells it "OVERLAPS".

Extract's closing paragraph said RdsLogSource "advances and persists its
resume marker as a side effect of the fetch, before this parser is
reached," so a timezone refusal lost the readable siblings in its
window. #3008 moved that commit after the write, and RdsDeadlockIngestor
now keeps the parse ahead of the commit specifically so a refusal leaves
without it -- the next cycle asks for the same window and the siblings
survive. That paragraph now says so, and says why the truncation above
is the case #3008 did not close: a refusal fails and withholds the
marker, while a truncated block parses, stores and commits.

"Never completed" is also softened to "not completed for the life of
that marker," which is what the in-process marker actually buys: a
restart re-reads a bounded tail.
Two statements of my own that were wrong in the same direction as the
ones this branch corrects.

The collector's remarks said the RDS marker "advances past whatever a
successful cycle stored." It advances past everything the cycle
consumed, which is more: a slab holding no deadlocks stores nothing and
still moves it. Read the narrow way, the sentence invites treating the
un-stored remainder as preserved, which is the exact false comfort being
removed.

FromBlock called a skip on that route "permanent." The resume marker
lives in the collector process, so a restart re-reads a bounded tail --
what a skip really costs is the life of that marker, which is what
Extract's remarks already say.
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This is a comments/tests-only change (confirmed via diff — no regex, no SQL, no runtime logic touched in PgDeadlockLogParser.cs, PgDeadlocksCollector.cs, or the migration SQL text itself), consistent with the PR description. I traced the two new pinned tests (AChunkCutBeforeTheDetailLineLosesTheReportWhole, AChunkCutMidDetailStoresAnUnderReportingRow_RatherThanNothing) against the actual regex (s_deadlockBlock, s_edge) and FromBlock/ParseStatements logic by hand — the assertions (resources, participant count, edge count, hash inequality, null victim statement) all match what the current code actually produces on the fixture, so these aren't tautological or accidentally-passing pins.

No Lite/Darling parity issue: get_pg_deadlocks/DarlingPgDeadlockReader are Darling-only (Lite has no PG deadlock detail reader/MCP tool), so there's no missing counterpart update.

One inline note on a residual doc inconsistency the PR's own "finish the sweep" framing didn't quite reach — see below. Non-blocking, since the PR text explicitly scopes these out, but worth a look since it's user/LLM-facing text about the exact feature this PR is about.

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Follow-up detail on the note above (couldn't inline it — neither file is touched by this diff, so GitHub won't accept a line comment on them):

  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPgDeadlockTools.cs:23 — the get_pg_deadlocks MCP tool description still says unconditionally: "the collector re-reads an overlapping tail of the log every cycle on purpose, so the same report is seen several times, and times_seen reports that rather than hiding it." That's only true on the pg_read_file route. On the consume-once RDS route (the one this PR's fix is actually about), times_seen will almost always read 1, and a reader relying on this description could misinterpret "times_seen = 1" as "seen only once so far, expect more sightings" when on that transport it usually means "this is all you'll ever get." Since this text is fed directly to an LLM/agent consuming the tool, it's arguably higher-stakes than the code comments this PR already fixed.
  • Darling/PerformanceMonitor.Darling.Storage/DarlingPgDeadlockReader.cs:15-18 and :81-83 — same blanket "the collector re-reads an overlapping tail... every cycle on purpose" / "without it the newest few rows are frequently the same report several times over" claims, unqualified by transport.

The PR description explicitly calls these out as "a step removed from this" and leaves them alone since they justify why dedup exists rather than claiming recovery from truncation. That's a reasonable line to draw, but the underlying sentence — "the collector re-reads an overlapping tail every cycle on purpose" as a universal statement — is precisely the false generalization this PR corrects everywhere else it appears. Worth a fast follow-up PR rather than blocking this one.

…ainer reads it

get_pg_deadlocks' tool description and its times_seen note are fed to
whatever agent calls the tool. Both said the collector re-reads an
overlapping tail so the same report is seen several times and times_seen
reports that. On the consume-once route a report is seen once, so
times_seen is 1 and stays 1 -- and a consumer told the number is a
read-window artifact reads that 1 as "seen once so far, expect more"
when it means "this is all there will ever be". The description now
splits the two transports and says so in those words.

The reader behind it made the same claim three times: its class doc, the
times_seen column comment, and DeadlockDetailSql's reason for its
DISTINCT ON. The grouping and the DISTINCT ON are still right on both
routes; only the reason given was transport-blind. The viewer's
Activity-tab read is the fourth, and windowing on occurred_at is
likewise correct for a reason that holds on both -- a report is always
found some minutes after the fact -- with the re-read tail now named as
the pg_read_file route's addition to that rather than as the whole of it.

Swept 2686 tracked files for the claim rather than the files pointed at.
The plan-capture siblings on get_pg_plans are deliberately left: there
the description tells a consumer captures may EXCEED the executions they
represent, so on the consume-once route the error runs toward caution
rather than toward false reassurance, and the field's own instruction --
get_pg_top_queries.calls is the authority -- holds on both.
Base hygiene ahead of review. dev has moved from 114e8cb to ba78352
and touches 21 files, none of them this branch's -- including #3017's
rewrite of the RDS ingestors, whose commit ordering this branch's
comments describe.
Comment thread Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPgDeadlockTools.cs Outdated
Comment thread Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPgDeadlockTools.cs Outdated
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Review summary

This PR is comments/docs-only as described — no SQL, no rungs, no production logic changed outside Lite.Tests. I checked the corrected claims against the actual code (RdsLogSource.cs, RdsDeadlockIngestor.cs) and they hold up: the pg_read_file route really does overlap its tail reads, the RDS log-API route really is consume-once via DownloadDBLogFilePortion, and the marker commit really does sit after the store in RdsDeadlockIngestor.IngestAsync (post-#3008), so a parse/store failure leaves the marker uncommitted while a truncated-but-parseable block does not.

I also manually traced the two new fixture-based tests (AChunkCutBeforeTheDetailLineLosesTheReportWhole, AChunkCutMidDetailStoresAnUnderReportingRow_RatherThanNothing) against the regex in PgDeadlockLogParser.cs — the assertions (empty result for a cut before DETAIL:, a single under-reporting row with ParticipantCount > edgeCount for a cut mid-DETAIL) match the actual regex/parsing behavior.

No Lite/Darling parity gap: PgDeadlockLogParser is shared collector code with a single test project (Lite.Tests), so there's no counterpart file to update.

One finding posted inline: the MCP-facing Description/note strings in DarlingMcpPgDeadlockTools.cs state unconditionally that RDS/Aurora times_seen "is 1 and stays 1" and "no further sightings are coming," which is stronger than the hedged language this same PR uses in DarlingPgDeadlockReader.cs ("normally arrives ONCE") and PgMigrations.cs ("repeats less often and for other reasons (#3008, #3009)"). Since RdsLogSource's resume marker is in-memory, a restart (or a pre-#3008 unlanded write) can cause a repeat sighting and push times_seen above 1 on RDS too — so the absolute claim, especially since it's returned verbatim to MCP/API callers, doesn't match the more careful reasoning elsewhere in this same PR.

Review caught the same over-claim a third time, in the place with the
least room for it. The tool description and the times_seen note said
times_seen "is 1 and stays 1" on RDS and that "no further sightings are
coming" -- absolutes, while this branch's own reader doc says a report
"normally arrives ONCE" and V104's says the route "repeats less often
and for other reasons." The hedged versions are the correct ones: the
resume position lives in the collector process, so a restart re-reads a
bounded tail, and since #3008 a window whose write did not land is
offered again. Either can push times_seen above 1 there.

The warning that matters survives -- a low times_seen on that route is
the ordinary state and not a partial count, so 1 must not be read as
"seen once so far, expect more" -- and the false guarantee around it is
gone. An absolute is worst here precisely because these two strings go
to a consumer that cannot see the reasoning they came from.
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Review

Verified this is what it claims to be: a comment/doc/description-only correction plus two new pinned tests, no behavioral changes anywhere in the 7 touched files.

Correctness of the corrected claims — checked against the actual code rather than taking the PR description's word for it:

  • RdsDeadlockIngestor.IngestAsync (Darling/.../Targets/RdsDeadlockIngestor.cs:98-110) confirms _logs.CommitResume(...) really does run after StoreAsync (parse + COPY), so a truncated block that parses does get committed past — matching the new Extract remarks.
  • RdsLogSource's own doc (consume-once, DownloadDBLogFilePortion never re-hands bytes, marker only moves on CommitResume) matches the new "no overlap, no next pass" language added throughout.
  • Traced both new xUnit tests (Lite.Tests/PgDeadlockLogParserTests.cs) by hand against s_deadlockBlock/s_edge in PgDeadlockLogParser.cs:
    • AChunkCutBeforeTheDetailLineLosesTheReportWhole: cutting at IndexOf("DETAIL:") removes the literal the pattern requires, so Assert.Empty is correct (no match possible), not a false-empty from an already-broken fixture (guarded by the Assert.Single(RealBlock) and Assert.Contains("ERROR:...") sanity checks beside it).
    • AChunkCutMidDetailStoresAnUnderReportingRow_RatherThanNothing: the cut text is unique in the fixture ("blocked by process 1556.\n" appears exactly once), the continuation group (?:\t[^\n]*\n)* legitimately matches zero lines so the block still matches, and the arithmetic (participant_count from distinct pids in edges vs. edge count from s_edgeCount) checks out: 2 participants over 1 edge for the fragment, both edges accounted for in the whole report.
  • The new s_edgeCount field's fully-qualified System.Text.RegularExpressions.Regex matches the file's pre-existing s_blockCount convention rather than deviating from it.

Lite/Darling parity — no drift. PgDeadlockLogParser/PgDeadlocksCollector (shared PerformanceMonitor.Collectors) are only ever consumed by Darling (DarlingWorker, RdsDeadlockIngestor); Lite has no PG-deadlock reader or MCP tool at all (grepped for pg_deadlocks/PgDeadlock under Lite/ — only an unrelated SQL Server deadlock mention in McpHealthTools.cs). That asymmetry predates this PR, so there's no counterpart file this change should have touched but didn't.

Security / performance — no input handling, SQL, or network code changed; the SQL-comment edits inside DarlingPgDeadlockReader's raw string literals are inert text sent as part of an existing parameterized query, not new SQL surface.

No findings to flag. The CHANGELOG.md omission and the two prior-review dispositions (regex identifier style, sweep completeness) are addressed in the PR description and check out against the diff.

"There is no setting that suppresses it" is false, and it sat in the
same string this branch already corrected twice, aimed at the same
consumer. log_error_verbosity = terse drops the DETAIL field, and DETAIL
is the whole graph: every wait edge and every participant's SQL. The
ERROR: deadlock detected line is still written, so the log looks like it
holds deadlocks while the block pattern -- which requires the DETAIL
group -- matches nothing. Zero rows from a server that is deadlocking,
in the shape of a server that is not.

What was true and is kept: a deadlock report needs nothing ENABLED on
the target, no preload and no restart, which is the real contrast with
auto_explain. Needing nothing enabled is not the same as being
unsuppressable, and the four places that said otherwise now say which.

#3030's cause is a logging-parameter mismatch nobody checked, and
log_error_verbosity and log_min_messages appeared ZERO times across all
2688 tracked files before this. The get_pg_deadlocks description and the
viewer panel's doc both enumerate what to check when the result is
empty, and both omitted the parameter that turned out to be the answer;
the description now names it and points at pg_stat_database's counter as
the independent test.

Swept case-insensitively by mechanism over the whole tree rather than by
phrase: four source sites, all corrected. The CHANGELOG's historical
entry is left alone.
Comment thread Darling/PerformanceMonitor.Darling.Storage/DarlingPgDeadlockReader.cs Outdated
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This is a doc/comment-accuracy PR (plus two new parser tests) with no SQL or behavior changes, as described. No T-SQL is touched (all changed files are in the Darling PostgreSQL/C# side), and there's no Lite counterpart to flag for parity — Lite doesn't do PostgreSQL deadlock collection, so PgDeadlockLogParser/PgDeadlocksCollector/the Darling MCP tool are Darling-only by design.

Verified correct:

  • The core factual claim (RDS log-API route is consume-once via RdsLogSource's per-(instance,file) resume marker + DownloadDBLogFilePortion, vs. pg_read_file's overlapping-tail re-read) matches the actual code in RdsDeadlockIngestor.cs and RdsLogSource, including the RdsLogSource advances the resume marker inside the fetch, so any failure before the write loses that chunk permanently #3008 marker-commit-after-write ordering.
  • The two new xUnit tests (AChunkCutBeforeTheDetailLineLosesTheReportWhole, AChunkCutMidDetailStoresAnUnderReportingRow_RatherThanNothing) traced through the regex (s_deadlockBlock, s_edge, s_participant) by hand — the cut points, assertions, and the "participants > edges" arithmetic all check out against PgDeadlockLogParser.cs.
  • log_error_verbosity = terse dropping DETAIL (and hence the deadlock graph) while still emitting the ERROR line is accurate Postgres behavior.

One issue posted inline: DarlingPgDeadlockReader.cs line 67 says times_seen "is 1 for good" on the RDS route, which contradicts the "normally 1, not guaranteed" position stated everywhere else in this same PR (the class doc four lines above it, and the MCP tool description in DarlingMcpPgDeadlockTools.cs, which explicitly says a restart or a write that didn't land can cause it to be offered again). Given the PR's whole point is correcting overclaimed guarantees in these comments, this one spot reintroduces the same class of error.

"1 for good" in the times_seen column comment contradicted the class doc
four lines above it ("normally arrives ONCE") and the tool description
("not guaranteed to be 1 there either"). It now points at the class
remarks instead of restating a guarantee.

The check that was supposed to catch that did not, and the reason is
worth writing down: it verified the four sites already known to carry a
hedge, which is a positive check. It never enumerated the absolutes that
REMAINED, so a fifth site could not fail it. That is the same defect as
the case-sensitive phrase-list sweep that missed the OVERLAPS spellings,
one level up -- a check over a remembered list rather than over a shape.

Swept by shape instead: 250 added lines across the eight files this
branch touches, merge-base to working tree, case-insensitively, with a
positive and a negative control. It found two more, both introduced by
the first commit and both in the pins' own doc: "the RDS log-API route
is consume-once, so it never is", and "DownloadDBLogFilePortion has
already handed those bytes out and will not again, so this deadlock is
simply gone". The resume marker is in-process, so a restart re-reads a
bounded tail; both now bound the claim to the marker's life.

Six absolutes remain in the added lines and all six are true by
construction rather than by transport behaviour: times_seen is never a
deadlock count because a recurrence carries a different hash, and a
participant count cannot exceed its edge count in a report the server
actually wrote because participants are counted FROM those edges. Those
are arithmetic, not guarantees about a marker.
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This PR is comments and tests only (confirmed via diff — no non-comment lines changed in PgDeadlockLogParser.cs or PgDeadlocksCollector.cs, and the SQL text in DarlingPgDeadlockReader.cs/PgMigrations.cs is untouched, only the surrounding <c>/<para> doc comments).

Correctness of the claims themselves — traced against the actual implementation rather than taking the doc comments on faith:

  • RdsDeadlockIngestor.IngestAsync/StoreAsync (Darling/PerformanceMonitor.Darling.Service/Targets/RdsDeadlockIngestor.cs) confirms PgDeadlockLogParser.Extract runs and StoreAsync completes before _logs.CommitResume(...) — a truncated block that still parses (mid-DETAIL cut) does get stored and does advance the marker, matching the new comments' central claim.
  • RdsLogSource keeps _markers as an in-memory Dictionary (not persisted), which backs the "not completed for the life of that marker" / "a restart re-reads a bounded tail" phrasing added in place of the earlier "never completed" — that softening is accurate, not just softer-sounding.
  • The revised times_seen guidance (DarlingMcpPgDeadlockTools.cs, DarlingPgDeadlockReader.cs) correctly avoids claiming RDS times_seen is pinned at 1 — it can exceed 1 after a restart or a RdsLogSource advances the resume marker inside the fetch, so any failure before the write loses that chunk permanently #3008 unlanded-write retry, and the final text hedges accordingly rather than asserting the stronger (false) guarantee an earlier commit in this branch briefly introduced.

New tests (Lite.Tests/PgDeadlockLogParserTests.cs) — manually traced both new fixtures against s_deadlockBlock/s_edge/FromBlock:

  • AChunkCutBeforeTheDetailLineLosesTheReportWhole: cutting before DETAIL: leaves no detail group for the regex to match, so Extract returns empty — correct, and the pin's "uncut slab beside it" guard against a vacuously-passing empty fixture works as described.
  • AChunkCutMidDetailStoresAnUnderReportingRow_RatherThanNothing: cutting after blocked by process 1556.\n (the first edge line) leaves the continuation group matching zero lines, so the block still matches with one edge instead of two. Worked through the resulting Resources/ParticipantCount/LockModes/hash values by hand against FromBlock's logic (participants counted from edge pids, not Process N: headers) — all asserted values are correct, including the partial.ParticipantCount > partialEdges (2 > 1) detection arithmetic.

Lite/Darling parity: no drift. Verified get_pg_deadlocks, DarlingPgDeadlockReader, and the PG deadlock collector reader/MCP surface are Darling-exclusive — Lite has no PostgreSQL deadlock reader, service, or MCP tool of its own (grep for PgDeadlock/pg_deadlocks under Lite/ turns up nothing outside the shared PerformanceMonitor.Collectors parser and Lite.Tests), so there's no Lite counterpart that needed the same doc correction.

Security / performance: none applicable — no SQL text, input handling, or hot-path logic changed.

No findings.

@erikdarlingdata
erikdarlingdata merged commit ac0d4c7 into dev Sep 5, 2026
8 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3009-rds-truncation-doc branch September 5, 2026 17:00
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