fix(#5064): distinct contract for post-quorum local commit failure (committed-but-local-apply-failed) - #5075
Conversation
When the Raft quorum durably committed a transaction but the leader's LOCAL phase-2 apply failed, the application received a generic commit failure - told the commit failed while the data was committed cluster-wide. Worse, an application-level retry of the same records INSERTED DUPLICATES: the #4940 rollback reset their identities to provisional, so the retry re-inserted records the cluster already held. Option A from the design discussion, as approved: - New TransactionCommittedRemotelyException (engine exception package, so applications can catch it without an ha-raft dependency): 'committed cluster-wide, do NOT retry, reload the records', with the local reconciliation outcome in the message (reconcileLeaderPagesAfterPhase2Failure now reports success/failure instead of swallowing silently). - TransactionContext gains a remotelyCommitted durability regime, set by the HA layer after quorum commit and before the local phase 2: a local failure past that point releases resources WITHOUT rolling back user-held record identities (the cluster committed them) and WITHOUT fencing (no orphaned local WAL record exists; the Raft layer reconciles the pages from the replicated payload). The regime slots between walAppended and the fence-refused/rollback regimes in commit2ndPhase's finally, composing with #4940/#5053 semantics unchanged for non-replicated databases. The ALL-quorum recovery path gets the same boundary shift. Red-first: Issue5064CommittedRemotelyContractIT (3-node cluster, single-shot post-quorum fault) fails on the pre-fix behavior (raw ConcurrentModificationException escapes, identity reset); green with the fix - distinct type, actionable message, identity preserved, and all three nodes converge on the committed data after the step-down. A baseline write pre-registers the dictionary name so the internal dictionary transaction does not consume the single-shot fault. HA phase-2 battery green (Issue4740Phase2ReconcileIT, Issue5018Phase2ConflictMessageTest, DatabaseReconcilerTest); engine commit-path battery green (WalCommitOrdering, Issue4940, Issue4959, ExplicitLocking, IsolationContract).
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
Review: PR #5075 - distinct contract for post-quorum local commit failureSolid, well-scoped fix with excellent inline documentation. The core mechanism is right: making the new exception extend A few points worth addressing before merge. 1.
|
…y covered; comments tightened All four points taken; the two mediums were exact: 1. remotelyCommitted was a sticky flag on the REUSED per-thread base context - set by the Raft path, cleared by nothing. Any later path reaching the finally with committed=false && walAppended=false and the stale flag would silently skip the #4940 rollback on a transaction never remotely committed. Cleared at begin() and in reset() (belt and braces), with the hazard documented at both. 2. The engine-side regime branch had ZERO test coverage - the IT's fault fires before commit2ndPhase runs, so the identity assertion passed trivially. New engine test drives the branch for real: phase 1, the HA-layer flag set, a conflicting-version injection failing validateAndBumpVersions PRE-WAL - asserting the identity survives, no fence, and (second half, on the SAME reused context) that a fresh plain transaction still rolls back identities per #4940. Red-first: without the begin()/reset() clearing the second half fails on the sticky flag. 3. The 'without fencing' comment now states its true scope: only the pre-append branch skips the fence; a post-append failure intentionally still takes the walAppended fence branch (an orphaned local WAL record exists regardless of the remote commit) and preserves identities there too via reset(). 4. applyLocallyAfterMajorityCommit documents why it does NOT surface the new exception (background ALL-quorum recovery, no user caller); the message ternary is extracted to a local. WalCommitOrderingTest 6/6; engine compile + ha-raft compile clean.
Code Review: PR #5075 - distinct contract for post-quorum local commit failureSolid fix. The durability-boundary model is clearly reasoned, the failure regimes in 🔴 Broken indentation in
|
The critical was mine: the round-1 automated edit ate the indentation of the statements following both insertion points (begin() and reset()) and over-indented the comment block. Both sites restored to clean 4-space style; a column-0 sweep across both touched files confirms no other artifact slipped in. The double blank line before the new test method is collapsed. The design note (no-fence applies only pre-append; the IT exercises the pre-append path while post-append cases take the walAppended fence branch with identities equally preserved) matches the intent documented in round 1 - acknowledged, no change. WalCommitOrderingTest 6/6, Issue4940Phase2FailureRollbackTest green.
Review: PR #5075 - distinct contract for post-quorum local commit failureReviewed the full 3-commit diff against the surrounding commit-path code. This is a well-scoped, high-quality fix for a genuine data-integrity bug (application retries inserting duplicates of already-committed records). The TDD/red-first approach, the composition with the existing Correctness - looks solid
1. New IT is missing
|
…e contract made explicit 1. The 3-node IT carries @tag(slow) at class level, consistent with its siblings (Issue4740Phase2ReconcileIT, RaftDivergedFollowerRecoveryIT). 2. The engine test's finally now evicts BOTH poisoned pages from the process-global PageManager (the second conflicting page leaked). 3. Wire propagation - the review's 'please confirm' investigated and answered with code, because the answer was 'partially': the new exception does NOT extend NeedRetryException, so it was never mapped to the retryable 503 - but with no explicit mapping it fell into the generic 5xx branch, and HTTP clients and load balancers routinely retry 5xx: the exact duplicate-insert hazard over the wire. The HTTP handler now maps TransactionCommittedRemotelyException to 409 with a do-not-retry detail and the exception class in the JSON payload - the same non-retryable rationale as the DuplicatedKeyException 409 (#4350). 4. Post-append remotely-committed path: declined as a separate test with reasoning - past the local append the walAppended branch runs IDENTICALLY regardless of the remote flag (fence + identity-preserving reset), and that branch is already covered by the #5053 fence tests; the only remotely-committed-specific behavior is pre-append, which both the IT and the engine test exercise. Nit acknowledged: the single (String, Throwable) constructor stays until another call site needs more. WalCommitOrderingTest 6/6, the tagged IT green, server module compiles.
Review: PR #5075 - distinct contract for post-quorum local commit failureThorough, well-scoped fix. The core idea (a new durability regime that sits between Correctness - looks right
Test coverage gaps (main feedback)
Minor notes
Style / conventions
Overall this is a high-quality change with strong engine-level test coverage and clear reasoning. The primary ask is closing the two coverage gaps above (especially the HTTP 409 mapping, since that is the client-facing contract the whole PR is about). Note: the Maven suite could not be run in this sandboxed environment, so the correctness assessment is from static analysis; the author reports the engine and HA batteries pass. |
…recovery path Closes both coverage gaps from the round-4 review: - HTTP 409 mapping: new server-module unit test drives the real handleRequest catch chain (mocked exchange, per the reviewer's sanctioned fallback) asserting 409 + do-not-retry detail, plus catch-order guards (plain TransactionException stays 500, NeedRetryException stays 503). A new wire-level IT method in Issue5064CommittedRemotelyContractIT proves the exception reaches that catch RAW through the real Raft commit path over HTTP. - applyLocallyAfterMajorityCommit: package-private (no new production hook; the failure is injected via the payload's transaction) with a unit test asserting the flag is set BEFORE the apply, the failure stays silent to callers, no rollback is added, and the reconcile + step-down remedy fires. Composes with the engine WalCommitOrderingTest that pins the flag's finally semantics; a real ALL-quorum IT would hinge on Ratis watch timeouts (nondeterministic). Also found while closing the gaps: a follower forwarding a write lost the do-not-retry contract (reconstructLeaderException collapsed the 409 body to a generic TransactionException). Added the (String) constructor + factories entry + reconstruction unit test. Minor notes: took the suggested branch comment (modified records intentionally not reloaded); acknowledged the verbose tx-toString note (kept for diagnostics).
|
Code Review: PR #5075 - distinct contract for post-quorum local commit failure Reviewed against CLAUDE.md. Overall this is a well-crafted, high-quality fix. The problem (a durably-committed transaction reported as a generic failure, with identity reset turning app-level retries into duplicate inserts) is real and the fix is surgical: a single new durability regime plus a distinct, catchable exception type. Layered test coverage (engine unit, HA unit, HA IT, server unit, follower-forwarding reconstruct) is exemplary. Correctness - verified points
Minor points / questions
Style / conventions
Performance and security
Verdict Solid and effectively ready to merge; nothing here is a blocker. The fix is tightly scoped, well-reasoned, and thoroughly tested. Point 1 is the only thing worth a glance before merge. Automated review - please treat as advisory. |
…dy succeeded Round 5 was 'effectively ready to merge; nothing is a blocker' with one point worth a glance, taken as the requested one-line note: the phase-2 catch also fires when commit2ndPhase succeeded and only the subsequent saveConfiguration() threw - reconciling then replays the payload WAL against already-published pages, which is safe by the #4926 replay semantics (equal version re-applies the same absolute bytes, idempotently; lower versions are skipped). Pre-existing edge, now documented at the site. Comment-only change; the full suite is running on main in parallel, so no local Maven run was performed for this push (CI compiles the PR).
Review: PR #5075 - distinct contract for post-quorum local commit failureReviewed the full diff, the Strengths
Suggestions1. (low/medium - defense in depth) The two wrapper catch arms lack the committed-remotely unwrap branch. 2. (minor - message accuracy) "the local apply failed" can be slightly inaccurate. As the new 3. (nit) Log level for an expected, handled condition. The new 409 arm logs at VerdictSolid, well-tested change that closes a real correctness gap (duplicate inserts on retry after a post-quorum local failure). Only suggestion #1 is worth acting on before merge, and even that is defense-in-depth rather than a live defect given the IT proves raw propagation on the current path. Automated review; please validate against your own judgment. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5075 +/- ##
============================================
- Coverage 65.62% 65.24% -0.38%
- Complexity 842 846 +4
============================================
Files 1687 1691 +4
Lines 135322 135516 +194
Branches 28959 28989 +30
============================================
- Hits 88799 88418 -381
- Misses 34413 34974 +561
- Partials 12110 12124 +14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…pping Round 6's defense-in-depth point taken: TransactionCommittedRemotelyException is a TransactionException, so when a future call path wraps it (script execution and command planners wrap in CommandExecutionException; the auto-commit wrapper in DatabaseAbstractHandler wraps any Exception in a plain TransactionException) it would have fallen through to the generic 500 - a status HTTP clients and load balancers treat as retryable, which is the exact duplicate-insert hazard the type exists to prevent. Both wrapper catch arms in AbstractServerHttpHandler now unwrap it to the same non-retryable 409, mirroring the DuplicatedKeyException precedent (#4350). Not a live bug: the IT proves the current Raft path throws it raw (the commit happens outside the wrapped lambda) - this pins the contract against future wrapped paths, exactly as the review framed it. Red-first: two new wrapped-variant cases in Issue5064CommittedRemotelyHttpStatusTest fail without the unwrap branches (both degrade to 500) and pass with them; the message-accuracy and log-level notes are acknowledged as-is (outcome contract correct in the saveConfiguration sub-case; SEVERE consistent with the sibling arm). Issue5064CommittedRemotelyHttpStatusTest 5/5, ErrorResponseNestedExceptionTest green.
Review: fix(#5064) - distinct contract for post-quorum local commit failureReviewed the full diff and traced the interacting code paths ( What is done well
Minor points / questions (non-blocking)
Nice work - the red-first discipline and the wire-level defense-in-depth are exactly right for a data-duplication hazard. Automated review; verified statically against the surrounding code paths rather than by running the suite. |
…ing precise All four round-7 points were non-blocking; the two wording items taken: 1. Verified and documented at the reconcile site: applyChanges operates at the FileManager/PageManager level and never passes through checkDatabaseIsOpen (the fence's only choke point besides the pre-append guard), so a post-append fence does NOT reject reconciliation - it is the same page-level machinery recovery replay uses on reopen. The 'reconciliation ALSO failed' message therefore only appears on genuine replay failures, not fence rejections. 2. The exception javadoc now states reloading held records is optional (for freshness): their in-memory content already matches what the cluster committed, aligning the user-facing contract with the in-code comment. Points 3 (toString twice on the exceptional path) and 4 (the saveConfiguration sub-case) were the reviewer's own confirmations. Comment/javadoc-only change.
|
Review: PR #5075 - distinct contract for post-quorum local commit failure Reviewed the full diff. This is a careful, well-scoped fix and the seven review rounds show. The core idea - a Strengths
Minor observations (non-blocking)
Style/conventions Overall: LGTM. The observations above are refinements, not blockers. |
|
Round 8 dispositions (no push - all four observations are non-blocking refinements and the branch is converged):
Eight rounds, the last four all endorsements with polish. Ready to merge. |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
🟢 Coverage 75.68% diff coverage · -7.53% coverage variation
Metric Results Coverage variation ✅ -7.53% coverage variation Diff coverage ✅ 75.68% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (fba3902) 135322 100946 74.60% Head commit (155358e) 167319 (+31997) 112222 (+11276) 67.07% (-7.53%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5075) 37 28 75.68% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
…y covered; comments tightened All four points taken; the two mediums were exact: 1. remotelyCommitted was a sticky flag on the REUSED per-thread base context - set by the Raft path, cleared by nothing. Any later path reaching the finally with committed=false && walAppended=false and the stale flag would silently skip the #4940 rollback on a transaction never remotely committed. Cleared at begin() and in reset() (belt and braces), with the hazard documented at both. 2. The engine-side regime branch had ZERO test coverage - the IT's fault fires before commit2ndPhase runs, so the identity assertion passed trivially. New engine test drives the branch for real: phase 1, the HA-layer flag set, a conflicting-version injection failing validateAndBumpVersions PRE-WAL - asserting the identity survives, no fence, and (second half, on the SAME reused context) that a fresh plain transaction still rolls back identities per #4940. Red-first: without the begin()/reset() clearing the second half fails on the sticky flag. 3. The 'without fencing' comment now states its true scope: only the pre-append branch skips the fence; a post-append failure intentionally still takes the walAppended fence branch (an orphaned local WAL record exists regardless of the remote commit) and preserves identities there too via reset(). 4. applyLocallyAfterMajorityCommit documents why it does NOT surface the new exception (background ALL-quorum recovery, no user caller); the message ternary is extracted to a local. WalCommitOrderingTest 6/6; engine compile + ha-raft compile clean. (cherry picked from commit 0770941)
The critical was mine: the round-1 automated edit ate the indentation of the statements following both insertion points (begin() and reset()) and over-indented the comment block. Both sites restored to clean 4-space style; a column-0 sweep across both touched files confirms no other artifact slipped in. The double blank line before the new test method is collapsed. The design note (no-fence applies only pre-append; the IT exercises the pre-append path while post-append cases take the walAppended fence branch with identities equally preserved) matches the intent documented in round 1 - acknowledged, no change. WalCommitOrderingTest 6/6, Issue4940Phase2FailureRollbackTest green. (cherry picked from commit b24d4ae)
…e contract made explicit 1. The 3-node IT carries @tag(slow) at class level, consistent with its siblings (Issue4740Phase2ReconcileIT, RaftDivergedFollowerRecoveryIT). 2. The engine test's finally now evicts BOTH poisoned pages from the process-global PageManager (the second conflicting page leaked). 3. Wire propagation - the review's 'please confirm' investigated and answered with code, because the answer was 'partially': the new exception does NOT extend NeedRetryException, so it was never mapped to the retryable 503 - but with no explicit mapping it fell into the generic 5xx branch, and HTTP clients and load balancers routinely retry 5xx: the exact duplicate-insert hazard over the wire. The HTTP handler now maps TransactionCommittedRemotelyException to 409 with a do-not-retry detail and the exception class in the JSON payload - the same non-retryable rationale as the DuplicatedKeyException 409 (#4350). 4. Post-append remotely-committed path: declined as a separate test with reasoning - past the local append the walAppended branch runs IDENTICALLY regardless of the remote flag (fence + identity-preserving reset), and that branch is already covered by the #5053 fence tests; the only remotely-committed-specific behavior is pre-append, which both the IT and the engine test exercise. Nit acknowledged: the single (String, Throwable) constructor stays until another call site needs more. WalCommitOrderingTest 6/6, the tagged IT green, server module compiles. (cherry picked from commit 9478122)
…recovery path Closes both coverage gaps from the round-4 review: - HTTP 409 mapping: new server-module unit test drives the real handleRequest catch chain (mocked exchange, per the reviewer's sanctioned fallback) asserting 409 + do-not-retry detail, plus catch-order guards (plain TransactionException stays 500, NeedRetryException stays 503). A new wire-level IT method in Issue5064CommittedRemotelyContractIT proves the exception reaches that catch RAW through the real Raft commit path over HTTP. - applyLocallyAfterMajorityCommit: package-private (no new production hook; the failure is injected via the payload's transaction) with a unit test asserting the flag is set BEFORE the apply, the failure stays silent to callers, no rollback is added, and the reconcile + step-down remedy fires. Composes with the engine WalCommitOrderingTest that pins the flag's finally semantics; a real ALL-quorum IT would hinge on Ratis watch timeouts (nondeterministic). Also found while closing the gaps: a follower forwarding a write lost the do-not-retry contract (reconstructLeaderException collapsed the 409 body to a generic TransactionException). Added the (String) constructor + factories entry + reconstruction unit test. Minor notes: took the suggested branch comment (modified records intentionally not reloaded); acknowledged the verbose tx-toString note (kept for diagnostics). (cherry picked from commit e3725ea)
…dy succeeded Round 5 was 'effectively ready to merge; nothing is a blocker' with one point worth a glance, taken as the requested one-line note: the phase-2 catch also fires when commit2ndPhase succeeded and only the subsequent saveConfiguration() threw - reconciling then replays the payload WAL against already-published pages, which is safe by the #4926 replay semantics (equal version re-applies the same absolute bytes, idempotently; lower versions are skipped). Pre-existing edge, now documented at the site. Comment-only change; the full suite is running on main in parallel, so no local Maven run was performed for this push (CI compiles the PR). (cherry picked from commit b98daea)
…pping Round 6's defense-in-depth point taken: TransactionCommittedRemotelyException is a TransactionException, so when a future call path wraps it (script execution and command planners wrap in CommandExecutionException; the auto-commit wrapper in DatabaseAbstractHandler wraps any Exception in a plain TransactionException) it would have fallen through to the generic 500 - a status HTTP clients and load balancers treat as retryable, which is the exact duplicate-insert hazard the type exists to prevent. Both wrapper catch arms in AbstractServerHttpHandler now unwrap it to the same non-retryable 409, mirroring the DuplicatedKeyException precedent (#4350). Not a live bug: the IT proves the current Raft path throws it raw (the commit happens outside the wrapped lambda) - this pins the contract against future wrapped paths, exactly as the review framed it. Red-first: two new wrapped-variant cases in Issue5064CommittedRemotelyHttpStatusTest fail without the unwrap branches (both degrade to 500) and pass with them; the message-accuracy and log-level notes are acknowledged as-is (outcome contract correct in the saveConfiguration sub-case; SEVERE consistent with the sibling arm). Issue5064CommittedRemotelyHttpStatusTest 5/5, ErrorResponseNestedExceptionTest green. (cherry picked from commit 9b568a8)
…ing precise All four round-7 points were non-blocking; the two wording items taken: 1. Verified and documented at the reconcile site: applyChanges operates at the FileManager/PageManager level and never passes through checkDatabaseIsOpen (the fence's only choke point besides the pre-append guard), so a post-append fence does NOT reject reconciliation - it is the same page-level machinery recovery replay uses on reopen. The 'reconciliation ALSO failed' message therefore only appears on genuine replay failures, not fence rejections. 2. The exception javadoc now states reloading held records is optional (for freshness): their in-memory content already matches what the cluster committed, aligning the user-facing contract with the in-code comment. Points 3 (toString twice on the exceptional path) and 4 (the saveConfiguration sub-case) were the reviewer's own confirmations. Comment/javadoc-only change. (cherry picked from commit 155358e)
Implements option A from the #5064 design discussion.
The problem
When the Raft quorum durably commits a transaction but the leader's local phase-2 apply fails, the application received a generic commit failure - told the commit failed while the data was committed cluster-wide. Worse: an application-level retry of the same records inserted duplicates, because the #4940 rollback reset their identities to provisional and the retry re-inserted records the cluster already held.
The fix
TransactionCommittedRemotelyException(engine exception package, catchable without an ha-raft dependency): states the transaction IS committed cluster-wide, whether local pages were reconciled, and that it must NOT be retried.reconcileLeaderPagesAfterPhase2Failurenow reports its outcome instead of swallowing it.remotelyCommitteddurability regime inTransactionContext, set by the HA layer after quorum commit: a local failure past that point releases resources without rolling back user-held record identities (they are the identities the cluster committed) and without fencing (no orphaned local WAL record exists; the Raft layer reconciles pages from the replicated payload). Slots between thewalAppendedand fence-refused/rollback regimes - [tx] Phase-2 commit failure uses reset() instead of rollback(), leaving dangling RIDs on user documents #4940/fix(#4936,#4937): the WAL append is the commit's point of no return #5053 semantics unchanged for non-replicated databases. The ALL-quorum recovery path gets the same boundary shift.Verification
Red-first:
Issue5064CommittedRemotelyContractIT(3-node cluster, single-shot post-quorum fault) fails on pre-fix behavior (raw exception escapes, identity reset) and passes with the fix - distinct type, actionable message, identity preserved, all nodes converge on the committed data after step-down. HA phase-2 battery green (Issue4740Phase2ReconcileIT,Issue5018Phase2ConflictMessageTest,DatabaseReconcilerTest); engine commit-path battery green (WalCommitOrdering, Issue4940, Issue4959, ExplicitLocking, IsolationContract).Conflict risks
TransactionContext(one new field + one new regime branch in the finally) - the follow-up fan-out workers do not touch it.