DanielLeens commented on PR #11569:
URL: https://github.com/apache/seatunnel/pull/11569#issuecomment-5342836639

   This is a re-verification of the current head, 
`fa400efc26840cf2ae12eef437c7c6c4bd55cd2d`, one commit past what I reviewed a 
few hours ago (`17234b67e1d5`, itself a no-op CI retrigger on top of 
`03f7fd4347f7`). This new commit, `[Fix][Connector-V2] Treat fully absent XA 
restore batches as resolved`, is a direct response to Issue 1 from my last 
review. I re-traced it from scratch against the actual source rather than 
assuming the fix is correct because the commit title matches what I asked for.
   
   # What Problem Does This PR Solve?
   - User pain point: in the JDBC XA (2PC) aggregated-commit path, a permanent 
XA commit failure could be silently swallowed (`throwIfAnyFailed("commit")` was 
commented out, and `TransientXaException` was double-wrapped so its catch 
clause was dead code). A checkpoint could report success while a transaction 
stayed prepared-but-uncommitted in the database.
   - Fix approach: reclassify `XA_RETRY`/`XAER_RMFAIL` as transient (not 
`XA_RBTRANSIENT`, which is a permanent already-rolled-back outcome), stop 
swallowing permanent commit failures, and reconcile checkpoint-restored XIDs 
against a fresh `xaFacade.recover()` scan using commit order as evidence 
instead of a blanket `XAER_NOTA` tolerance.
   - One-sentence summary: this turns a silent "reports success, loses the 
commit" bug into a fail-closed commit/restore path, and this latest commit 
fixes the one case where "fail-closed" had itself become a permanent 
availability regression (a fully-committed batch surviving an unrelated 
restart).
   
   # 1. Code Change Review
   
   ## 1.1 Core Logic Analysis
   
   The changed method is 
`JdbcSinkAggregatedCommitter.replayRecoveredCheckpoint` 
(`seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/sink/JdbcSinkAggregatedCommitter.java:194-226`),
 called from `restoreCommit` once per `JdbcAggregatedCommitInfo` on job/task 
restore.
   
   Before (what I flagged as Issue 1):
   ```java
   int firstRecoveredIndex = findFirstRecoveredIndex(checkpointXids, 
recoveredXids);
   if (firstRecoveredIndex < 0) {
       throw new JdbcConnectorException(
               CommonErrorCodeDeprecated.WRITER_OPERATION_FAILED,
               String.format(
                       "none of the restored checkpoint transactions are 
present in the XA recovery scan: %s",
                       checkpointXids));
   }
   ```
   
   After:
   ```java
   int firstRecoveredIndex = findFirstRecoveredIndex(checkpointXids, 
recoveredXids);
   if (firstRecoveredIndex < 0) {
       log.warn(
               "Skipping checkpoint batch because none of its transactions 
remain in the XA recovery scan; treating it as already resolved: {}",
               checkpointXids);
       return;
   }
   ```
   
   The rest of the method — the "gap after a still-prepared XID fails closed" 
branch at `:206-217` — is untouched.
   
   Key findings:
   - Normal path reached: yes. `restoreCommit` is on Zeta's task-restore 
critical path — it runs whenever a JDBC XA sink task restarts from a 
checkpoint, which includes both real failure recovery and any unrelated restart 
(source/task failure elsewhere, node loss) that happens to fall in the window 
between a checkpoint's XA commit completing and the next checkpoint 
independently completing.
   - Scenario covered: the specific scenario I raised as Issue 1 — 
`CheckpointCoordinator` advances `latestCompletedCheckpoint` before phase-2 XA 
commit for that checkpoint even runs, and holds it there until the next 
checkpoint completes on its own. Any restart in that window replays 
`restoreCommit()` against a `XidInfo` batch that a prior, healthy 
`notifyCheckpointComplete()` already fully committed. Every XID in that batch 
is then legitimately absent from the RM (committed and forgotten — standard XA 
behavior), so the old code's unconditional throw converted the single most 
common healthy-restart case into a permanent failure loop with no operator 
remedy (the RM state doesn't change between retries).
   - Precise fix, not a workaround: it targets exactly the boundary condition I 
identified — total absence, where the model has zero still-prepared evidence to 
anchor the prefix/suffix invariant against — and leaves the rest of the 
commit-order reconciliation (which correctly stayed fail-closed for the "gap 
after a still-prepared XID" case, since that pattern cannot arise from a 
legitimate sequential commit) completely alone. One branch, one behavior change.
   - Remaining hole (not new, inherent to XA, already priced into my prior 
review): treating total absence as "already resolved" is still a heuristic. 
XAER_NOTA-equivalent absence at scan time cannot distinguish "committed and 
forgotten" (the common, targeted case) from "rolled back/expired/heuristically 
completed for an unrelated reason and then discarded by the RM." A fully 
rigorous fix needs an external durable record of "we ourselves issued a 
successful commit," independent of RM bookkeeping — out of scope for a 
one-branch fix. Given the WARN log at the point of tolerance and that this 
narrows rather than widens the pre-existing ambiguity (the pre-PR code 
tolerated absence unconditionally in every case, not just this one), I'm not 
re-opening this as a new blocker; it's the same class of residual risk every 
version of this PR has carried, not something this commit makes worse.
   - Runtime path (restore, current head):
   
   ```text
   Task restart / restore
     -> JdbcSinkAggregatedCommitter.restoreCommit(aggregatedCommitInfos) 
[L91-99]
         -> tryOpen()
         -> for each JdbcAggregatedCommitInfo:
             -> recoverCheckpointTransactions() [L234-255]
                 -> xaFacade.recover(), bounded retry on TransientXaException
             -> replayRecoveredCheckpoint(checkpointXids, recoveredXids) 
[L194-226]
                 -> findFirstRecoveredIndex() [L264-271]
                 -> branch: no checkpoint XID found in scan 
(firstRecoveredIndex < 0)
                     -> log.warn(...), return  [FIXED: was throw]
                 -> branch: some XID still prepared
                     -> verify every XID from firstRecoveredIndex onward is 
present
                         -> absent -> throw (unchanged, fail-closed, correct)
                     -> commitXidInfos(stillPrepared, ignoreUnknown=false) 
[L218]
                     -> log.warn for the already-resolved prefix, if any 
[L219-225]
   ```
   
   ## 1.2 Compatibility Impact
   **Classification: Fully compatible.** No API/config/serialization change — 
`XidInfo`/`JdbcAggregatedCommitInfo` untouched, no new/renamed config option. 
The behavioral change is a narrowing of the fail-closed surface introduced 
earlier in this same PR (not yet released), so there's no compatibility break 
relative to any shipped version. `docs/en` and `docs/zh` (`Jdbc.md` and 
`incompatible-changes.md`) were updated in the same commit and now accurately 
describe "all-absent -> treated as already resolved, skip" vs. "gap after a 
still-prepared XID -> fail closed" — I checked both language versions and they 
say the same thing.
   
   ## 1.3 Performance / Side-Effect Analysis
   No change in this commit. Carryover from my last pass, still true: 
`recoverCheckpointTransactions()` runs once per `JdbcAggregatedCommitInfo` 
inside `restoreCommit`'s loop rather than once per invocation — a pre-existing 
minor inefficiency (extra RM round-trips when a restore batch has multiple 
`JdbcAggregatedCommitInfo` entries), not a correctness issue, and not something 
this commit touches. `commitXidInfos`'s round cap still has no backoff between 
rounds (Issue 2 below).
   
   ## 1.4 Error Handling and Logging
   The fix moves this branch from "throw, opaque to a human reading the 
checkpoint failure without XA context" to "WARN with the full XID list, then 
continue" — this is strictly better for both correctness (no more permanent 
wedge) and operability (the WARN gives an operator something to grep for if 
they want to double check RM history). I verified `XaFacadeImplAutoLoad`'s 
error classification independently (not just re-reading the prior review): 
`TRANSIENT_ERR_CODES = {XA_RETRY, XAER_RMFAIL}` 
(`XaFacadeImplAutoLoad.java:75-76`) correctly excludes `XA_RBTRANSIENT` (a 
permanent already-rolled-back outcome per the XA spec, not a "no effect, retry 
me" outcome), and `buildCommitErrorDesc` (`:323-331`) only tolerates 
`XAER_NOTA` when `ignoreUnknown=true` — every other code, including the 
`XA_HEURRB`/rollback family, still propagates as a hard failure. This confirms 
the classification correctness I'd only taken on trust before.
   
   **Issue 2 (Medium, carryover, unchanged by this commit):** no backoff 
between synchronous commit/recover retry rounds — 
`JdbcSinkAggregatedCommitter.java:129-142` (`commitXidInfos`) and `:237-248` 
(`recoverCheckpointTransactions`). `XAER_RMFAIL`-class outages will burn the 
whole `max_commit_attempts` budget in microseconds with no delay between 
attempts.
   
   # 2. Code Quality Assessment
   
   ## 2.1 Coding Standards
   The updated Javadoc on `restoreCommit` (`:84-89`) matches the new behavior 
("An all-absent batch is treated as already resolved..."). No missing 
documentation on the changed method.
   
   ## 2.2 Test Coverage and Test Stability
   
`JdbcSinkAggregatedCommitterTest.testRestoreCommitSkipsAlreadyResolvedBatchWhenRecoveryScanHasNoCheckpointXid`
 (`:143-163`) was rewritten in the same commit: it now uses a 2-XID all-absent 
batch, asserts `assertDoesNotThrow`, and verifies `xaGroupOps.commit(...)` is 
`never()` invoked — which matches the implementation exactly (the method 
returns before building the `stillPrepared` list, so commit is genuinely never 
attempted, not just swallowed). This closes the exact gap I called Issue 3 last 
time ("no test where the entire batch is legitimately already-committed") at 
the unit level.
   
   **Stability rating: Stable.** Mockito-based, single-threaded, deterministic 
on object identity/counts/message content; no `Thread.sleep`, no shared static 
state, no `@DisplayName`.
   
   **Issue 3 (Medium, carryover, partially addressed):** the unit-level gap is 
now closed, but there is still no integration/real-database exercise of any 
part of this PR's XA commit/restore path — `XaGroupOpsImplIT` 
(`seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-1/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/xa/XaGroupOpsImplIT.java`)
 remains `@Disabled` and untouched by this PR (confirmed: `git diff dev...HEAD` 
for that path is empty).
   
   ## 2.3 Documentation Updates
   `docs/en/connectors/sink/Jdbc.md`, `docs/zh/connectors/sink/Jdbc.md`, and 
both `incompatible-changes.md` files were updated in this same commit to 
describe the corrected all-absent behavior. Consistent between languages; I 
read both.
   
   # 3. Architectural Soundness
   
   ## 3.1 Elegance
   Precise fix. One branch changed from throw to warn-and-return, with an 
updated regression test and updated docs in the same commit — no redesign, no 
scope creep into the (still correctly fail-closed) gap-after-recovered branch.
   
   ## 3.2 Maintainability
   Unchanged from my last assessment: the responsibility split across 
`XaFacadeImplAutoLoad` (driver-outcome classification), `XaGroupOpsImpl` 
(grouped commit propagation), and `JdbcSinkAggregatedCommitter` (restore 
reconciliation) stays clean; 
`replayRecoveredCheckpoint`/`findFirstRecoveredIndex`/`recoverCheckpointTransactions`
 remain small and single-purpose.
   
   ## 3.3 Extensibility
   Unchanged: `XidKey`'s canonical `formatId`+GTRID+BQUAL comparison is a 
solid, reusable primitive; `XaGroupOps.commit(...)`'s new `default` overload 
lets other implementors opt in without a forced signature break.
   
   ## 3.4 Historical-Version Compatibility
   No checkpoint-state schema change; old checkpoints/savepoints deserialize 
unmodified. This fix is a decision-logic change, not a serialization/format 
change, and applies uniformly regardless of checkpoint age.
   
   # 4. Issue Summary
   
   | # | Issue | Location | Severity | Status |
   |---|-------|----------|----------|--------|
   | 1 | ~~All-absent restore batch threw unconditionally, misclassifying 
"already fully committed by a prior healthy checkpoint, unrelated restart" as 
unrecoverable, wedging the job permanently~~ | 
`JdbcSinkAggregatedCommitter.java:198-205` | High | **Fixed in `fa400efc2`, 
verified** |
   | 2 | No backoff between synchronous commit/recover retry rounds; 
`max_commit_attempts` is largely inert against `XAER_RMFAIL`-class outages | 
`JdbcSinkAggregatedCommitter.java:129-142`, `:237-248` | Medium | Open, 
carryover |
   | 3 | No end-to-end/real-database test exercises commit/restore failure 
propagating to checkpoint/job failure; `XaGroupOpsImplIT` remains `@Disabled` 
and untouched by this PR | `XaGroupOpsImplIT.java` (connector-jdbc-e2e-part-1) 
| Medium | Open, carryover (unit-level gap for the specific all-absent scenario 
is now closed) |
   | 4 | Fork `Build` run for the current head (`fa400efc2`) was still in 
progress at review time: all completed jobs green, including every `unit-test` 
lane (which runs this PR's own new/changed tests), zero failures observed 
across 56 completed jobs; ~15 `updated-modules-integration-test-part-*` jobs 
still running | fork run `32248144048` (in progress) | Low (process, not 
source) | Not yet finalized |
   
   # 5. Merge Recommendation
   
   ### Conclusion: Ready to merge after fixes
   
   1. **Blockers — must be fixed**
      - None from source review. Issue 1, the sole High/blocking finding from 
my prior pass, is fixed and independently re-verified against the current head. 
The only open item before this can be called done is Issue 4: let the 
in-progress fork CI run for `fa400efc2` finish and confirm it matches the 
all-green result already observed for every completed job (especially the 
`unit-test` lanes, which already cover this exact fix on this exact SHA).
   
   2. **Recommended fixes — non-blocking**
      - Issue 2 (Medium): add bounded backoff between synchronous retry rounds.
      - Issue 3 (Medium): re-enable `XaGroupOpsImplIT` with real XA coverage, 
including the "whole batch already committed by a prior successful commit, then 
an unrelated restart" scenario at the integration level (the unit test now 
covers this at the mock level).
   
   **On the reconciliation logic overall:** I re-confirm what I found in my 
morning pass — the commit-order/prefix-suffix invariant correctly closes both 
@nzw921rx's 2026-07-27 normalization concern and @davidzollo's 2026-08-06 
idempotency concern, and this commit closes the one new problem I found on top 
of that (Issue 1). No new problem introduced by this commit; the change is 
narrowly scoped to exactly the branch I asked about, with a matching regression 
test and matching doc update in the same commit.
   
   **Overall assessment:** the engineering direction has been sound throughout 
this PR's review history, and every round has converged rather than regressed. 
This round closes the last blocking item I had open. Once the current fork CI 
run finishes green, I'd expect to be able to call this ready to merge without 
qualification.
   
   Since this is my own PR, this is posted as a comment rather than an approval.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to