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

   Note: this PR is authored by me (DanielLeens), so GitHub blocks a 
self-review submission. Posting this as a plain issue comment instead of a 
formal PR review, per project convention for self-authored PRs. This is a full 
re-review from scratch against the current head, not a diff against my prior 
rounds.
   
   # What Problem Does This PR Solve?
   
   Before this patch, a permanent JDBC XA commit failure could be silently 
swallowed: `XaGroupOpsImpl.commit()` recorded the failure but never surfaced it 
(`throwIfAnyFailed("commit")` was disabled), and `wrapException()` always 
wrapped `TransientXaException` inside `JdbcConnectorException`, making the 
`catch (TransientXaException)` branch in the group-commit loop unreachable. The 
net effect was that every XA commit failure, transient or permanent, could let 
a checkpoint be reported successful while the transaction was left 
prepared-but-uncommitted in the database. This PR restores failure propagation, 
fixes the retryable/permanent error classification, bounds retries within a 
single invocation (since Zeta does not persist attempt counters across a 
checkpoint restart), and reconciles restored checkpoint XIDs against a live 
resource-manager recovery scan instead of inferring success from an absent XID 
alone.
   
   # 1. Code Change Review
   
   ## 1.1 Core Logic Analysis
   
   Reviewed at head `ca15d97a9ba` (13 commits ahead of `dev` at `43fe63b1fca`, 
merge-base is the current `dev` tip). The single commit since my last round 
(`403a0b09f88` -> `ca15d97a9ba`, "[Fix][Connector-V2] Clarify XA restore 
recovery evidence") is comment-only: it rewords the Javadoc/inline comments on 
`restoreCommit()` to state that the per-batch recovery-scan refresh is 
intentional (a DBA or RM cleanup process can resolve an in-doubt transaction 
between restored batches), addressing li3zhi4's non-blocking Suggestion 3 from 
2026-08-21. I confirmed via `git diff 403a0b09f88..ca15d97a9ba` that no 
executable code changed.
   
   Because this is a full re-review, I retraced the whole runtime path myself 
rather than relying on my own or others' prior conclusions:
   
   **Failure classification and propagation** 
(`XaFacadeImplAutoLoad.wrapException`, `XaGroupOpsImpl.commit`): 
`TRANSIENT_ERR_CODES` is now `{XA_RETRY, XAER_RMFAIL}` (moved off 
`XA_RBTRANSIENT`, which means the branch was already rolled back — a permanent 
outcome, not a retryable one). `wrapException` now returns 
`TransientXaException` directly instead of throwing it wrapped inside 
`JdbcConnectorException`, so `XaGroupOpsImpl.commit()`'s `catch 
(XaFacade.TransientXaException e)` branch is reachable, and 
`result.throwIfAnyFailed("commit")` (re-enabled) now actually fails the 
checkpoint on a permanent/unknown outcome instead of returning a false success.
   
   **Ordering invariant behind the restore design** (this is the load-bearing 
correctness property, and I verified it directly rather than taking the PR 
description's word for it): `XaGroupOpsImpl.commit()` iterates `xids` with the 
guard `i.hasNext() && (result.hasNoFailures() || allowOutOfOrderCommits)`, and 
`GroupXaOperationResult.hasNoFailures()` returns false once *either* a 
permanent failure *or* a transient failure has been recorded. Both production 
call sites pass `allowOutOfOrderCommits = false`. So within one `commit()` 
invocation, the loop stops at the *first* failure of any kind, and every 
subsequent un-iterated XID is appended to `forRetry` untouched, preserving its 
position. Across `commitXidInfos()`'s own retry rounds, `pending` is rebuilt 
from `getForRetry()` each round, preserving relative order. This means: a later 
XID in the list is committed only if every earlier XID already committed 
successfully. That is exactly the invariant `restoreCommit()` -> 
`replayRecovered
 Checkpoint()` depends on when it treats "everything before the first 
still-prepared XID" as already resolved. I traced this end to end and it holds 
on the current head.
   
   **Restore reconciliation** 
(`JdbcSinkAggregatedCommitter.replayRecoveredCheckpoint`): finds the first 
checkpoint XID still present in a fresh `xaFacade.recover()` scan 
(`findFirstRecoveredIndex`); everything before it is logged and skipped as 
already resolved (justified by the ordering invariant above); everything from 
that index onward must *all* still be present in the scan or the batch fails 
closed with a `JdbcConnectorException` before anything in that suffix is 
committed (no partial commit on the fail-closed path); the still-prepared 
suffix is then replayed through the same `commitXidInfos` path used by normal 
commit. An all-absent batch (no checkpoint XID at all in the scan) is treated 
as already resolved and skipped without ever calling `xaFacade.commit`, which 
also means restore no longer needs `XAER_NOTA`-as-success inference at all for 
that case.
   
   This directly resolves the two outstanding `CHANGES_REQUESTED` reviews on 
this PR:
   
   - **nzw921rx (2026-07-27)** asked for restore to reconcile against 
`xaFacade.recover()` instead of trusting `XAER_NOTA` alone, with an explicit, 
durable commit-decision rule. That review predates the recovery-scan 
reconciliation entirely (it was posted the day after the PR opened, before this 
mechanism existed). The current head implements exactly the reconciliation 
shape requested: checkpoint XIDs are intersected with the recovery scan, XIDs 
not owned by the current batch are never touched (`XidKey`-based canonical 
comparison, not driver object identity), and unknown/missing XIDs no longer 
fall back to `XAER_NOTA`-is-success.
   - **davidzollo (2026-08-06)** found a concrete non-idempotency bug: if 
`xidA` commits and `xidB` then permanently fails in the same batch, the success 
of `xidA` was previously not recorded anywhere durable, so a restart's 
`restoreCommit()` would re-attempt `xidA`, get `XAER_NOTA` (forgotten, since it 
was already committed), and treat that as a **new** permanent failure — an 
infinite restart loop that never even reaches `xidB`. I reconstructed this 
exact scenario against the current code: `findFirstRecoveredIndex` finds `xidB` 
(still prepared) at index 1, treats `xidA` (index 0, absent from the scan) as 
an already-resolved prefix, and only replays `xidB`. `xidA` is never 
recommitted and never reported as a failure. This is precisely the 
"commit-order evidence" mechanism the PR description names, and it holds under 
the ordering invariant traced above.
   
   **li3zhi4 (2026-08-21, COMMENTED, not blocking)** independently re-derived 
both of the above from source at a slightly earlier head (`61df8fd43`, 
functionally identical to the current head except for the comment-only commit) 
and reached the same conclusion: no correctness blocker, three non-blocking 
suggestions. I re-verified all three against the current head:
   - Suggestion 1 (PR description overstates the all-absent case as "fails 
closed" when the code/docs treat it as already-resolved): still an unaddressed 
**PR-description-only** discrepancy; code and docs are internally consistent 
and match what li3zhi4 confirmed. Non-blocking, cosmetic.
   - Suggestion 2 (`XaFacade.commit(xid, ignoreUnknown)`'s `ignoreUnknown = 
true` path has no production caller — both `commitPreparedTransactions` and 
`replayRecoveredCheckpoint` call it with `false`): confirmed still true on the 
current head; the `true` path is exercised only by unit tests. Non-blocking, 
but worth a follow-up: either document the intended production use or narrow 
the API now that evidence-based replay has replaced `XAER_NOTA`-as-success 
inference.
   - Suggestion 3 (clarify why the recovery scan is refreshed per batch instead 
of hoisted out of the loop): **addressed** by the one new commit on this head.
   
   ## 1.2 Compatibility Impact
   
   Partially incompatible, and correctly disclosed. This is a deliberate, 
documented behavior change to the XA exactly-once restore path: jobs that 
previously relied on restore silently treating a missing-with-a-gap XID as 
success will now fail closed and require operator investigation. 
`docs/en/introduction/concepts/incompatible-changes.md` and 
`docs/zh/introduction/concepts/incompatible-changes.md` both carry a new entry 
describing the affected component, the behavior change, the operational impact, 
and migration guidance (inspect `XA RECOVER` / `pg_prepared_xacts` for dangling 
prepared transactions before upgrading). No public API signatures changed in a 
way that breaks source compatibility; `TransientXaException`'s constructor 
visibility widened from package-private to `public`, and `XaGroupOps.commit()` 
gained Javadoc only.
   
   ## 1.3 Performance / Side-Effect Analysis
   
   - `restoreCommit()` now performs one `xaFacade.recover()` scan per restored 
batch instead of zero; this is a bounded, restart-only-path cost, not a 
steady-state one, and is retried with the same backoff budget as commit 
(`recoverCheckpointTransactions`) so a transient RM outage during restore does 
not fail restore immediately.
   - A 1-second `Thread.sleep` backoff was added between synchronous retry 
rounds in both the commit and recovery-scan retry loops. This only fires during 
genuine XA/RM instability (a real net improvement over the pre-PR behavior, 
which had effectively zero retries — any non-empty result cancelled the 
pipeline on the first transient blip). Two of the new unit tests exercise this 
real sleep (bounded to a couple of seconds each), which is an acceptable, 
deterministic cost, not a source of flakiness.
   - `containsEquivalentXid` is an O(N) linear scan per checkpoint XID against 
a `Set<XidKey>`, so the reconciliation as a whole is effectively O(N) per batch 
(the recovered side is already a `Set`). This is restore-path-only and bounded 
by batch size; not a concern.
   
   ## 1.4 Error Handling and Logging
   
   No new formal issues found on this head. All previously raised concerns 
(nzw921rx, davidzollo) are, on inspection of the current code, structurally 
resolved rather than merely asserted to be resolved.
   
   # 2. Code Quality Assessment
   
   ## 2.1 Coding Standards
   
   New/changed core methods (`restoreCommit`, `replayRecoveredCheckpoint`, 
`commitXidInfos`, `recoverCheckpointTransactions`, `findFirstRecoveredIndex`, 
`containsEquivalentXid`, `backoffBeforeRetry`, `wrapException`, `XidKey`) all 
carry Javadoc explaining intent, constraints, and (where relevant) the 
fail-closed/already-resolved distinction — this is exactly the kind of "why" 
documentation this project's `CLAUDE.md` conventions ask for on complex 
conditional/retry logic, and it is genuinely useful here given how easy this 
reconciliation logic is to get subtly wrong. No missing-comment gaps found on 
this pass.
   
   ## 2.2 Test Coverage and Test Stability: Stable
   
   - `XaFacadeImplAutoLoadTest` and `XaGroupOpsImplTest` genuinely simulate XA 
failures by `doThrow`-ing real `XAException` error codes (`XAER_RMFAIL`, 
`XA_RETRY`, `XA_RBTRANSIENT`, `XAER_NOTA`, `XA_HEURCOM`) from a mocked 
`XAResource`/`XaFacade`, not just mocking a success path — this was a specific 
concern going into this round and it checks out.
   - `JdbcSinkAggregatedCommitterTest` (8 methods) exercises the reconciliation 
logic directly: prefix-skip after a still-prepared suffix commits, 
all-absent-batch skip, fail-closed on a gap after the first recovered XID, 
transient recovery-scan retry, and bounded-retry exhaustion 
(`testCommitFailsWhenRetryRoundsNeverDrain` deliberately uses a non-draining 
`XaGroupOps` stub to prove the outer loop still terminates and fails rather 
than looping forever — a good defense-in-depth test for a retry loop).
   - Notably, this PR also fixes a real, unrelated pre-existing 
test-infrastructure bug as a side effect: `XaGroupOpsImplIT` 
(`seatunnel-e2e/.../connector-jdbc-e2e-part-1`) was `@Disabled` with reason 
"JdbcDatabaseContainer: ClassNotFoundException: com.mysql.jdbc.Driver". This PR 
removes that `@Disabled` and adds 
`testCommitFailurePropagatesThroughAggregatedCommitter`, which prepares a real 
XA transaction against a live MySQL Testcontainer, stops the container to force 
a genuine RM-unavailable commit failure, and asserts the failure propagates as 
a `JdbcConnectorException` through the actual 
`JdbcSinkAggregatedCommitter.commit()` path (not a mock). This is real, 
non-mocked evidence that the fix works end to end against a real resource 
manager, which is a meaningfully stronger signal than the unit tests alone.
   - All tests are deterministic (mock-driven or single-container-stop), no 
timing-sensitive assertions beyond the bounded, intentional retry-backoff 
sleeps noted above.
   
   ## 2.3 Documentation Updates
   
   `docs/en/connectors/sink/Jdbc.md` and `docs/zh/connectors/sink/Jdbc.md` both 
updated to describe the new restore semantics under 
`max_retries`/`max_commit_attempts`; 
`docs/en/introduction/concepts/incompatible-changes.md` and the `zh` 
counterpart both carry the breaking-change entry with migration guidance. 
Content is consistent with the actual code behavior (verified directly against 
`replayRecoveredCheckpoint`), not just with the PR description (which, per 
li3zhi4's Suggestion 1 above, has one stale sentence that overstates the 
all-absent case — worth a quick edit to the PR description text, no doc or code 
change needed).
   
   # 3. Architectural Soundness
   
   ## 3.1 Elegance of the Solution: Precise fix
   
   The fix is scoped tightly to the XA commit/restore path and does not leak 
connector-specific logic into `seatunnel-api` or the engine. The 
commit-order-evidence design is a reasonable way to distinguish "already 
committed in a prior aborted attempt" from "genuinely lost/unknown" without 
persisting additional state across checkpoints, which the PR description is 
upfront about being a constraint (Zeta does not persist attempt 
counters/partial-success state across a restart today).
   
   ## 3.2 Maintainability
   
   Good. The `XidKey` canonical-comparison type, the `commit`/`restoreCommit` 
split, and the retry/backoff helpers are all small, single-purpose, and 
documented. The one loose end (li3zhi4 Suggestion 2, `ignoreUnknown=true` with 
no production caller) is a minor API-clarity nit, not a maintainability risk.
   
   ## 3.3 Extensibility
   
   The reconciliation logic operates purely on canonical XID values and the 
existing `XaFacade`/`XaGroupOps` abstractions, so it should extend cleanly to 
other XA-capable JDBC dialects without connector-specific branching.
   
   ## 3.4 Historical-Version Compatibility
   
   This is the one place where "compatible" needs to be read carefully: this is 
a **behavior change**, not a wire/state-format change. No checkpoint state 
schema changed (`XidInfo` serialization is untouched), so upgrading a job with 
existing checkpointed XA state and then restoring is safe to attempt; the risk 
is purely operational — a job that was silently succeeding through a masked 
failure before this fix may now fail closed on restore and require manual 
investigation, exactly as documented in the incompatible-changes entry. That is 
the correct trade-off for a data-correctness fix (a checkpoint that silently 
reports success while data is missing is a worse historical-compatibility 
problem than a restore that now surfaces the inconsistency), and it is 
disclosed rather than silent.
   
   # 4. Issue Summary
   
   No formal (numbered) issues found on this head. All prior concerns from 
other reviewers are addressed in the current code; the two remaining items 
below are non-blocking carryovers already surfaced by li3zhi4 and not yet 
actioned:
   
   | Number | Issue | Location | Severity |
   | --- | --- | --- | --- |
   | N/A-1 | PR description says restore "fails closed" for an all-absent 
batch; code and docs actually treat it as already-resolved | PR description 
text only (not code) | Low |
   | N/A-2 | `XaFacade.commit(xid, ignoreUnknown=true)` has no production 
caller after the evidence-based replay design replaced `XAER_NOTA`-as-success 
inference | `JdbcSinkAggregatedCommitter.java`, `XaFacadeImplAutoLoad.java` | 
Low |
   
   # 5. Merge Recommendation
   
   ### Conclusion: Ready to merge
   
   From a source-correctness standpoint, my independent re-trace of the 
ordering invariant, the restore reconciliation logic, and the test evidence 
(including the previously-disabled real-MySQL IT test that now genuinely 
exercises a live commit failure) finds no blocking issues on the current head 
(`ca15d97a9ba`). Both outstanding `CHANGES_REQUESTED` reviews were filed 
against earlier heads and target concerns that the current implementation 
structurally resolves — nzw921rx's request for recovery-scan-based 
reconciliation instead of `XAER_NOTA`-as-success inference, and davidzollo's 
non-idempotent-restart scenario. I want to be precise about what I can and 
cannot do here: I am not nzw921rx or davidzollo, I cannot dismiss their reviews 
on their behalf, and `reviewDecision` will correctly keep showing 
`CHANGES_REQUESTED` until one of them re-reviews the current head or a 
maintainer with write access revisits and dismisses a stale review. That is a 
process blocker, not a code blocker,
  and it is the only thing standing between this PR and merge as of this 
comment.
   
   Separately, `mergeStateStatus` is `BLOCKED` and the `Build` check is 
currently red. I pulled the actual failing job log rather than trusting the 
status pointer: `updated-modules-integration-test-part-3` failed after 1 hour 
with `Could not resolve dependencies for project connector-jdbc-e2e-part-2: ... 
database-commons:1.20.1 ... Connection timed out` — a Maven Central network 
timeout unrelated to this PR's code, in a module bucket this PR does not touch 
logically. This is the same class of transient infra failure I flagged and 
confirmed resolved by rerun in my previous round (that time it was a 
`jetty-http` 502 from Maven Central); it needs a rerun, not a code change.
   
   1. Blockers (process, not code): the two outstanding `CHANGES_REQUESTED` 
reviews from nzw921rx and davidzollo need to be revisited against the current 
head and either re-affirmed with a concrete remaining objection or 
dismissed/updated — I cannot do this myself as the PR author. CI needs a clean 
rerun given the confirmed-transient failure cause.
   2. Recommended non-blocking fixes: correct the PR description's 
all-absent-batch wording (li3zhi4 Suggestion 1); consider documenting or 
removing the unused `ignoreUnknown=true` production path (li3zhi4 Suggestion 2).
   
   No alternative implementation approach looks better here; the 
commit-order-evidence design is the right shape for this problem given the 
constraint that Zeta does not persist partial-commit state across a checkpoint 
restart today.
   


-- 
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