Repository navigation
PostgreSQL deadlock reports store and show their SQL normalized, and no read returns a hash of the raw report (#4005) - #4013
Merged
Conversation
…eturns a hash of the raw report (#4005) The deadlock parser reads each candidate report through PgLogEntryAssembler, so the label is the line's own and the HINT proves the DETAIL whole, and puts every query through PgLogTextRedactor.RedactDetail before anything is read out of it. The identity is over the timestamp and the normalized graph. Every read normalizes a row stored before this, and names it by its timestamp and victim pid instead of its raw hash, which it never looks a row up by. The detail read without an identity now returns the newest reports. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…and a report is named by its time and pid (#4005) Findings persist their drill-down and prose for 30 days, so one written before the deadlock SQL was normalized kept its exemplar's statement, fingerprint and graph raw, and a hash that may be over the raw graph. PgFindingStore's read puts them through the same normalization and names the report by its timestamp and victim pid, which the detail read now finds whichever build stored the row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…4005) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
… follow the one-column candidate read (#4005) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ng this build wrote as written (#4005) A cut first can land inside a literal, and the lexer then withholds the whole statement. The read now takes what normalizing needs and the caps apply after. A section this build writes carries sql_normalized, so the stored-finding read rewrites only sections written before the SQL was normalized. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
erikdarlingdata
enabled auto-merge (squash)
September 23, 2026 09:43
This was referenced Sep 23, 2026
…up (#4005) LiveCleanupConversionRatchetTests failed CI: the new class's teardown deleted its rows on the body's own connection, which a failed body can leave unusable (#1902). It now runs through LiveStoreCleanup.RunAsync with bodySucceeded set as the body's last statement, like every other live class. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
This was referenced Sep 23, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…ntries in their sections (#4080) Adds 42 entries and 42 link refs (#3992, #3995, #3996, #3998, #4001, #4002, #4003, #4007, #4010, #4011, #4013, #4015, #4020, #4022, #4025, #4029, #4030, #4031, #4036, #4038, #4039, #4040, #4044, #4047, #4048, #4049, #4050, #4051, #4055, #4061, #4063, #4064, #4065, #4066, #4067, #4068, #4069, #4070, #4071, #4073, #4074, #4078). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran. #3989 left 26 entries under bare 'Changed' and 'Fixed' lines above '### Added'. They move into '### Changed' and '### Fixed', below the new entries, and one blank line stays under [Unreleased]. Claude-Session: https://claude.ai/code/session_01Ua31ugERL5DmhFVRtf6keQ Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Closes #4005.
Why
collect.pg_deadlocksstored each deadlock report's DETAIL block verbatim (tabs stripped) and the victim's statement verbatim. Every read returned them as stored:get_pg_deadlocks(victim statement),get_pg_deadlock_detail(graph), the web dispatch on both, the desktop viewer's grid, the deadlock alert (email, webhook{{incidents_json}}, Teams and Slack cards, the persisted alert context), and the analysis drill-down (pg_deadlock_exemplars), whose finding persists the statement, its fingerprint in the advice prose, and the graph for 30 days. That is the exposure #3944 ruled on, through a different table.deadlock_hashwas SHA-256 over the raw graph. Readers got it inget_pg_deadlocks,get_pg_deadlock_detail, the drill-down, and the alert's dedup key (rendered as "Dedup Key" on cards and persisted in the context JSON).get_pg_deadlock_detailalso looked rows up by it. Once the graph is shown normalized, that hash is the #4004 guessing oracle: rebuild the graph, try values for each?, and compare, offline or by lookup.The block pattern also found
ERROR: deadlock detectedandDETAIL:anywhere on their lines, so a logged statement carrying those words was stored as a deadlock. Measured: the old pattern matches aLOG: statement: SELECT 'x ERROR: deadlock detectedline followed by a forged DETAIL, as a report from pid 1600.What changes
Write: one reader, one lexer.
PgDeadlockLogParserno longer parses a report with its own pattern.pg_read_fileroute) now only finds candidate text. It returns the ERROR line, the DETAIL block, and the line after it, which is where DeadLockReport always writes its HINT.FromReportreads each candidate withPgLogEntryAssembler, the reader Log messages show as PostgreSQL wrote them, the SQL in them stays normalized, and one statement's text never decides how another's is read (#3944) #3996 hardened. The label has to be the line's own, the DETAIL has to come from the same backend, and each continuation line loses exactly the tab PostgreSQL added. A query's own tabs stay, so the lexer reads the tokens PostgreSQL ran. The HINT setsDetailComplete.PgLogTextRedactor.RedactDetail(detail, DetailComplete)normalizes the queries before anything is read out of the block. It is Log messages show as PostgreSQL wrote them, the SQL in them stays normalized, and one statement's text never decides how another's is read (#3944) #3996's own code (per-waiter heads, fail-closed without the HINT proof), not a second masker. The wait-for lines stay as written.deadlock_hashisIdentityOf= SHA-256 over (timestamp, normalized graph). It covers only what a reader sees. The timestamp is there because normalizing can make two different reports' graphs match. It also means a new hash can never equal the oldHashOf(graph), which is how a pre-pg_deadlocks keeps each deadlock query and the victim statement verbatim, literals included, and returns them to readers and alerts #4005 row is told apart.Read: every path normalizes, idempotently, and no raw hash leaves.
DarlingPgDeadlockReader(MCP, web dispatch, WPF viewer, deadlock alert) normalizes the victim statement and graph of every row. A row this build wrote comes back unchanged.PgDeadlockLogParser.RawGraphHashSql: its hash equals SHA-256 of its graph). Its hash is never returned; it is namedat-<occurred_at>-<victim_pid>instead, from values every read already shows.get_pg_deadlock_detailfinds a report by either kind of identity. A hash lookup never matches a row whose hash is over its raw graph, so a guessed raw hash finds nothing.PgTargetDrillDownCollector.Deadlocks) now reads as much text as normalizing needs, normalizes it, then cuts to its 2000/4000-character caps in memory. Cutting first could land inside a literal, and the lexer would then withhold the whole statement. The exemplar's identity follows the same rule as the reader.PgFindingStore.ReadFindingnormalizes apg_deadlock_exemplarssection written before this change. It replaces the fingerprint in the stored advice prose and names each exemplar's report by time and pid. Every stored-finding read (get_analysis_findings, the viewers) gets that. A section this build writes carriessql_normalized: trueand is left as written, because a statement already normalized and cut to its cap can end inside a'?'.Found while here, fixed in lane. Without an identity,
get_pg_deadlock_detailreturned the reports whose hashes sort first, not the newest. Its LIMIT sat on theDISTINCT ONsort, which leads with the hash. It now sorts newest first in an outer query.Wording. Tool descriptions, the detail note, the web panel note, the README collector row and the runbook now say the SQL is normalized.
No store migration. No parity item: Lite and the deprecated Dashboard do not monitor PostgreSQL.
Test plan
Lite.TestsPgDeadlockLogParserTests(54): three new regression tests.Darling.TestsPgDeadlockNormalizationTests(6, live PG):at-..., and its raw hash is neither returned nor finds it.{{incidents_json}}.PgTargetDeadlockDrillDownTests(9, live):get_analysis_findingsis normalized.RdsDeadlockIngestorTests,PgDeadlockLogTimezoneTests: identity and zone pins updated deliberately (the zone is now read out of the returned report text).pg_read_filepattern on PostgreSQL 18.6, over a 4.78 MB synthetic tail with 8 reports: old 20-26 ms, new 20 ms, same 8 matches. The HINT line is taken, and a report with no HINT does not take the next report's first line.Darling.TestsandLite.Tests: see below.Full runs, on the rig (PostgreSQL 18.6 + TimescaleDB 2.30.1),
DARLING_TEST_PGset:Darling.Tests: 13011 total, 3 failed, 31 skipped. All three are accounted for:PgLogEventsPipelineTests.TheTailerExtraction_LeftBothSiblingsSqlByteIdentical: the deadlock sibling's SQL pin. Updated deliberately, with the reason in the test.LivePostgresCollectionHygieneTests: the new class had not joined thelive-postgrescollection yet. Fixed.TrendPayloadBudgetLiveTests:get_file_io_trendhit a transientException while reading from streamunder full-suite load. This class is untouched here; it passes alone (1/1).PgLogEventsPipelineTests38,LivePostgresCollectionHygieneTests11,PgTargetDeadlockDrillDownTests9,PgDeadlockNormalizationTests6,RdsDeadlockIngestorTests11,PgDeadlockLogTimezoneTests2,DocCommentHygiene77,McpPayloadContractCensusTests68,DarlingPgOperationalAlertTests30.Lite.Tests: 5171 total, 1 failed:PgLoggingCollectorGateTests.AnOrdinaryDeadlockRowStillParses, the collector seam's old four-column shape, since updated. The affected classes re-ran green (73).For the reviewer
pg_read_fileroute, a report still inside the 4 MB tail at upgrade is stored again under its new identity. It shows as two rows (oneat-..., one hash) until it ages out of the window. The alert's dedup key for such an in-window report changes once.collect.pg_deadlocksuntil the 90-day retention. Every product read normalizes them, but a role with a directSELECTon the table (raw_line_hash and statement_fingerprint are unkeyed hashes a store reader can use to guess the literals they hide #4004 names themcpgrant) reads them as stored. Closing that needs a one-time rewrite of those rows, which is out of scope under no-migration.LIMIT\t5stored asLIMIT5) lexes as an identifier and is not masked. New rows keep the query's own tabs.🤖 Generated with Claude Code
https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv