Repository navigation
Retry the alert pass's database-state maintenance once on a chunk-drop deadlock - #4612
Merged
Merged
Conversation
…p deadlock The alert pass's database-state seed/heal/prune/clear-recovered sequence had no handling for PostgreSQL deadlock 40P01. A drop_chunks call holding or waiting for an AccessExclusiveLock could pick this sequence's AccessShareLock as the deadlock victim, and the failure surfaced as an alert-pass error with no retry, even though every statement in the sequence is safe to repeat whole (an INSERT ... ON CONFLICT DO NOTHING seed, an idempotent heal/prune, and a clear-recovered UPDATE to a fixed NULL). This gives that sequence its own single immediate retry on 40P01, the same shape the daily retention purge already uses for its own drop_chunks call. The retry is scoped to just this sequence rather than widened inside the shared command-timeout retry seam, because that seam's other callers have no such idempotence guarantee and deliberately exclude PostgresException. A second deadlock in a row still surfaces, matching the purge's own give-up posture. The retry is logged once at Information, naming the SQLSTATE and server. Also confirmed: no product code path creates a TimescaleDB retention policy on collection_log. RetentionPolicies/RawTierCoverage/AddRetentionPolicySql never reference it; the only retention collection_log gets is the product's own drop_chunks purge in the daily retention sweep. Fixes #4606.
erikdarlingdata
marked this pull request as ready for review
September 28, 2026 17:04
This was referenced Sep 28, 2026
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.
What happened
A deadlock (SQLSTATE 40P01) between a chunk drop and a concurrent statement hit three monitoring stores over 14 days. Two of the three were the TimescaleDB retention job on
collection_loglosing to a chunk drop (self-heals on the job's own schedule). The third was the alert pass's database-state maintenance losing to adrop_chunkscall — the seed'sINSERTpicked as the deadlock victim — and that side had no retry, so it surfaced as an alert-pass error.The fix
GetDatabaseStatesCoreAsync(Darling/PerformanceMonitor.Darling.Service/DarlingAlertReadAdapter.cs) runs four statements ahead of the deviation read: seed, heal, prune, clear-recovered. All four now run inside a newExecuteDatabaseStateMaintenanceWithDeadlockRetryAsync, a delegate-seamed static helper with the same shape as the daily purge'sExecuteDropChunksWithDeadlockRetryAsync(DarlingRetention.cs): one immediate retry onPostgresErrorCodes.DeadlockDetected, logged once at Information (SQLSTATE + server id), give-up-and-surface on a second deadlock in a row, no retry for any other exception.Why a new, narrower seam instead of widening the shared
ExecuteWithOneRetryAsync: that seam serves twelve other reads across this type plusDarlingSelfAlertEvaluator, and its whole design deliberately excludesPostgresException— "an identical second attempt gets an identical answer" is true for most of those callers but not for this one sequence, whose four statements are each independently safe to repeat:INSERT ... ON CONFLICT DO NOTHING;UPDATEgated on the newest snapshot, idempotent against its own output;DELETEof rows absent from the newest snapshot, same idempotence;UPDATEto a fixedNULL.Widening the shared seam would have put a
PostgresExceptionretry in front of eleven callers that have no such guarantee. The narrower wrap keeps the existing command-timeout retry untouched — this new retry sits entirely inside the read that seam wraps, so a 40P01 retried here is never also counted as a command timeout there.The
collection_logretention job's originConfirmed: no product code path creates a TimescaleDB retention policy on
collection_log.TimescaleSupport.RetentionPoliciesand itsRawTierCoveragesource, andAddRetentionPolicySql's only caller (EnsureRetentionPoliciesAsync's loop), never namecollection_log. The only retentioncollection_loggets from the product is its owndrop_chunkspurge inside the daily retention sweep (DarlingRetention.cs~694–712), which already carries the #2143 deadlock retry. So the TimescaleDBpolicy_retentionjob that failed againstcollection_logon two stores wasn't created by current code, and a search of the history for a rung that added one found none. Where it came from on those stores isn't known from the code. That job's failures self-heal on its own schedule, so this PR changes nothing on that side; the job's origin is worth a look on the stores themselves.Pins
Darling/Darling.Tests/DarlingAlertReadAdapterTests.cs, same style asDarlingRetentionTests'DropChunksRetry_*:DatabaseStateMaintenanceRetry_OneDeadlock_RetriesOnce_AndSucceedsDatabaseStateMaintenanceRetry_TwoDeadlocks_GivesUpAndSurfacesDatabaseStateMaintenanceRetry_NonDeadlockPostgresException_DoesNotRetryDatabaseStateMaintenanceRetry_CleanRun_IsSingleShotAll four ran green on macOS via the in-process
Darling.Tests.dllrunner (no store needed — delegate-seamed like the purge's own pins). RED confirmed by mutation: changing the catch guard fromattempt == 1tofalse(removing the retry arm) failedDatabaseStateMaintenanceRetry_OneDeadlock_RetriesOnce_AndSucceedswith an unhandledPostgresException(40P01); reverted before the final build.Also ran green after the fix (post-merge, dev sha
209aca7fe):DarlingAlertReadAdapterTests(11 total, 1 skipped — the live end-to-end feed test, needsDARLING_TEST_PG),DarlingRetentionTests(47, 3 skipped),AlertReadRetrySeamTests(17),AlertReadFailureSurfaceTests(27),DocCommentHygieneTests(77),CommentFilterAdoptionTests(4),McpPayloadContractCensusTests(68),McpServiceParameterDiSeatCensusTests(1). All 0 failures.Build:
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -p:EnableWindowsTargeting=true— 0 warnings, 0 errors, both before and after the dev merge.Not run:
Lite.Tests— this change touches no Lite code (the database-state maintenance sequence and its retry live only in the Postgres-backedDarlingAlertReadAdapter; Lite's DuckDB read has its own path with no drop_chunks/deadlock exposure).Darling.Tests.dll/Lite.Tests.dllnet10.0-windows target build-only per house rule; CI decides anything platform-specific — nothing in this diff touches WPF types.Merge
Final step per house rule:
git fetch origin && git merge origin/dev— merged clean, dev sha209aca7fe("Plan analysis: rule 38 reads the server's edition and MAXDOP at every entry point (#4601)"). Rebuilt and re-ran the affected classes after the merge (listed above); all green.Refs #4606.
CHANGELOG
SECTION: Fixed
ENTRY: - A rare deadlock between a chunk drop and the alert pass's database-state check no longer fails that pass ([#4612]) - The alert pass retries its database-state statements once when PostgreSQL picks them as a deadlock victim, the way the daily purge already retries its chunk drop.
REF: [#4612]: #4612