DanielLeens commented on PR #11569:
URL: https://github.com/apache/seatunnel/pull/11569#issuecomment-5462160229
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 (same as my
previous rounds on this thread).
I re-checked activity since my last comment (`a9257e75bf1a`, ~10 hours ago):
`git diff` between that head and the current head is empty — no new commits, no
new reviews, no new discussion. Given that, I'm not going to re-post the
~2000-word from-scratch trace verbatim again; it would add nothing over what's
already sitting immediately above this comment on the same code. What follows
is condensed to (a) an independent spot-check of my own prior conclusions using
a different method than before, and (b) one genuinely new finding that none of
the six review rounds on this thread (mine included) named explicitly.
# What Problem Does This PR Solve?
Unchanged from prior rounds: before this patch, a permanent JDBC XA commit
failure could be silently swallowed (`throwIfAnyFailed("commit")` was disabled,
and `wrapException()` always threw `TransientXaException` pre-wrapped inside
`JdbcConnectorException`, making the retryable-error branch unreachable). This
PR restores failure propagation, fixes retryable/permanent XA error
classification, bounds retries within one invocation, and reconciles restored
checkpoint XIDs against a live `xaFacade.recover()` scan instead of inferring
success from an absent XID alone.
# 1. Code Change Review
## 1.1 Core Logic Analysis
**Independent verification of the failure-propagation fix**
(`XaFacadeImplAutoLoad.java`, `XaGroupOpsImpl.java`): confirmed by reading
every `wrapException` call site, not just the two I discussed before. All four
sites (`execute()`'s `orElseThrow`, `execute()`'s catch-all `throw`, and both
`Command.fromRunnable`/`fromRunnableRecoverByWarn` closures) consistently do
`throw wrapException(...)`. Since `wrapException` now *returns* a
`RuntimeException` (either `TransientXaException` or `JdbcConnectorException`)
instead of throwing it pre-wrapped, every one of those four call sites now
throws the correct concrete type, so `XaGroupOpsImpl.commit()`'s `catch
(XaFacade.TransientXaException e)` (previously dead code, since the exception
used to arrive as a `JdbcConnectorException` wrapping a `TransientXaException`
inside its cause, not as a `TransientXaException` itself) is genuinely
reachable. `TRANSIENT_ERR_CODES` correctly moved from `{XA_RBTRANSIENT}` to
`{XA_RETRY, XAER_RMFAIL}` —
`XA_RB*` codes mean the branch is already rolled back (permanent, nothing to
retry), `XA_RETRY` means the call had no effect and is safe to reissue. This is
real XA semantics, not a cosmetic rename.
**Independent verification of the ordering invariant** the restore
reconciliation depends on: `XaGroupOpsImpl.commit()`'s loop condition is
`i.hasNext() && (result.hasNoFailures() || allowOutOfOrderCommits)`, both
production call sites pass `allowOutOfOrderCommits=false`, and
`hasNoFailures()` flips to `false` on *either* a permanent or a transient
failure. So within one invocation the loop stops at the first failure of any
kind, and every subsequent un-iterated XID is appended to `forRetry` untouched
(`result.getForRetry().addAll(xids)` after the loop, using the same `Iterator`,
so relative order is preserved). Across retry rounds in `commitXidInfos`'s
`while (!pending.isEmpty())` loop, `pending` is rebuilt from
`result.getForRetry()` each round, so order is preserved across rounds too.
This confirms the property
`replayRecoveredCheckpoint`/`findFirstRecoveredIndex`
(`JdbcSinkAggregatedCommitter.java:205-237`) relies on: a later XID in a
checkpoint's list is only ever committed a
fter every earlier XID in that same list already committed. I traced this
end-to-end myself rather than trusting the Javadoc's claim, and it holds on the
current head.
**New finding — Issue 1 below.** Tracing the two "absorb an absence"
branches (all-absent batch, and the skipped prefix before a still-prepared
suffix) surfaced a gap that none of the six review rounds on this PR (nzw921rx,
davidzollo, li3zhi4, JeremyXin, dybyte, or my own five prior rounds) named
explicitly, though dybyte's Q1 thread on `JdbcSinkAggregatedCommitter.java:103`
came close from a different angle (concurrent resolution timing, which I
answered fully for the still-prepared/suffix case in that thread — external
resolution there surfaces as a loud `XAER_NOTA`-is-permanent failure, since
`XAER_NOTA` isn't in `TRANSIENT_ERR_CODES`). What I hadn't examined is the
*prefix/all-absent* case specifically:
### Issue 1: Absence from the recovery scan cannot distinguish "committed by
us" from "rolled back by someone else," and only the fail-closed (gap) case is
disclosed to operators
- **Location**: `JdbcSinkAggregatedCommitter.java:205-237`
(`replayRecoveredCheckpoint`), and the corresponding entries in
`docs/en(zh)/introduction/concepts/incompatible-changes.md` /
`docs/en(zh)/connectors/sink/Jdbc.md`
- **Problem**: Per the XA spec, a resource manager forgets a branch
identically whether it was previously *committed* or *rolled back* — both
outcomes make the branch vanish from `recover()`, and both return `XAER_NOTA`
if a client tries to act on it again. The prefix-skip and all-absent-batch
branches use "absent from the scan" as their *only* signal, and both
optimistically assume "already committed by us in a prior aborted attempt."
Under this PR's own architecture that assumption is well-justified for the
*normal* failure mode it fixes (a batch's own earlier commit attempt got
interrupted by an unrelated restart) — but as dybyte's Q1 thread already
established, the resource manager can also be resolved by an actor external to
Zeta: a DBA or an automated stuck-prepared-transaction cleanup job. If that
external actor issues `XA ROLLBACK` instead of `XA COMMIT` against a branch
that actually still belongs to a live (merely delayed) SeaTunnel job — a
realistic scenario after
a long outage, since long-pending prepared XA transactions are a well-known
operational headache that provokes exactly this kind of manual/automated
cleanup — this code has no way to tell the difference, and unlike the
suffix/gap case, the prefix/all-absent branches never re-attempt a commit on
the skipped XIDs, so there is no `XAER_NOTA`-permanent-failure signal to
surface the mistake. The checkpoint is reported as fully resolved while that
XID's writes are permanently gone. The `log.warn` at
`JdbcSinkAggregatedCommitter.java:212-214` and `:232-235` is the only
operational trace, and it reads identically whether the skip was correct (the
very common case) or masking a real loss (the rare case) — an operator cannot
tell which happened from the log alone.
- **Why this isn't a reason to reject the design**: the alternative —
failing closed on every absence, including the all-absent case — is exactly the
infinite-restart-deadlock bug davidzollo's 2026-08-06 review found in an
earlier revision of this PR (a job stuck forever re-attempting a batch that was
legitimately already committed). XA fundamentally cannot distinguish "resolved
by commit" from "resolved by rollback" from the RM's response alone without
extra durable state that Zeta doesn't checkpoint today (the PR description is
upfront about this constraint), so some assumption is unavoidable, and "assume
committed" is the correct default given SeaTunnel's own aborts never touch a
checkpoint-committed XID's namespace (each checkpoint/subtask generates
distinct XIDs, so self-inflicted confusion is ruled out — I checked this
specifically since it would have been a much worse bug).
- **Potential risk**: silent, permanent, undetectable loss of one XID's
writes in the narrow window where an external actor rolls back (rather than
commits) a prepared transaction this job still expects to restore — with no
distinguishing signal from a routine, correct skip.
- **Best improvement**: this doesn't need a code change (there's no code fix
for an information-theoretic gap), but the incompatible-changes migration guide
currently only tells operators to investigate a *gap after a recovered XID*
before upgrading — it should say the same for the all-absent/prefix-skip case:
that "already resolved" there is inferred, not verified, and that operators who
run external stuck-transaction cleanup against a resource manager also used by
SeaTunnel should exclude branches whose global-transaction-id prefix matches
SeaTunnel's `XidGenerator` format, or otherwise coordinate with job restart
timing. A one-line addition to the existing migration guide entry closes this
cheaply.
- **Severity**: Medium (real residual risk, correctly scoped as
inherent-to-XA rather than a implementation bug; recommended before merge, not
a hard blocker given the log-level signal that does exist and the fact this PR
is a strict improvement over the pre-existing status quo either way)
- **Raised by another reviewer**: No — new angle on a concern dybyte's Q1
partially opened; my own reply in that thread fully closed the
suffix/still-prepared case but did not address the prefix/all-absent case,
which is the part I'm completing here.
## 1.2 Compatibility Impact
Unchanged from prior rounds: Partially incompatible, and correctly disclosed
for the gap/fail-closed scenario (see Issue 1 for the one additional disclosure
gap). No checkpoint state schema changed (`XidInfo` serialization untouched);
`TransientXaException`'s constructor widened from package-private to `public`;
`XaGroupOps.commit()` gained Javadoc only. No source-incompatible API changes.
## 1.3 Performance / Side-Effect Analysis
Unchanged: `restoreCommit()`'s `recover()` scan is a bounded,
restart-only-path cost with its own retry budget; the 1s `Thread.sleep` backoff
between synchronous retry rounds (added in response to JeremyXin's 2026-08-21
review) only fires under genuine XA/RM instability; `containsEquivalentXid` is
an O(N) scan per checkpoint XID against a `Set<XidKey>`, bounded by batch size.
## 1.4 Error Handling and Logging
No new issues beyond Issue 1 above. I independently re-verified
`throwIfAnyReachedMaxAttempts` in `XaGroupOpsImpl.java` and `commitXidInfos`'s
outer bounded loop in `JdbcSinkAggregatedCommitter.java` both actually
terminate (covered by `testCommitFailsWhenRetryRoundsNeverDrain`) rather than
looping forever on a permanently-stuck resource manager.
**CI diagnosis for the current head (`a9257e75bf1a`)** — unchanged from my
comment immediately above, and I independently re-verified it rather than
taking my own prior word for it: pulled the actual `unit-test (8,
ubuntu-latest)` job log from the fork run (`DanielLeens/seatunnel` run
`33238169880`, job `99063544383`) myself. Confirmed the failure is exactly
`[ERROR]
CoordinatorServiceTest.testClearCoordinatorServiceDropsPendingJobsUnderRejectStrategy:642
» ConditionTimeout`, in `seatunnel-engine-server`, a module this PR's diff
never touches (this PR's diff is scoped to `connector-jdbc`'s XA classes/tests,
`connector-jdbc-e2e-part-1`, and docs — confirmed via `git diff --stat` against
the correct merge-base `43fe63b1fca`). The other three failing jobs
(`updated-modules-integration-test-part-3`/`part-2` on JDK 8 and 11) were
already diagnosed as Maven Central dependency-resolution resets and an
unrelated `PostgresCDCIT` Awaitility timeout, in modules this PR does not touch
ei
ther. This is infra flake, not a regression from this PR — needs a CI rerun,
not a code change.
# 2. Code Quality Assessment
## 2.1 Coding Standards
Unchanged: all new/changed core methods carry Javadoc explaining intent and
the fail-closed/already-resolved distinction. No gaps found.
## 2.2 Test Coverage and Test Stability: Stable
Unchanged rating, independently re-confirmed by reading the full method list
in `JdbcSinkAggregatedCommitterTest.java`: dedicated coverage exists for
recovered-by-value matching, per-batch recovery-scan refresh,
prefix-skip-after-recovered-suffix, all-absent-batch skip, fail-closed-on-gap,
transient recovery-scan retry, bounded commit-retry completion, and
retry-exhaustion. `XaGroupOpsImplIT` exercises a real MySQL testcontainer end
to end (addresses JeremyXin's Issue 2). No test exists for the Issue 1 scenario
above, which is expected — there is no deterministic way to unit-test "an
external actor rolled back instead of committed," since the code itself cannot
observe the difference; this is a documentation gap, not a missing-test gap.
## 2.3 Documentation Updates
Current docs accurately describe the mechanism (verified by diff, not just
PR description) but have the one gap named in Issue 1.
# 3. Architectural Soundness
## 3.1 Elegance of the Solution: Precise fix
Unchanged: scoped tightly to the XA commit/restore path, no leakage into
`seatunnel-api` or the engine.
## 3.2 Maintainability
Unchanged: small, single-purpose, documented helpers (`XidKey`,
`backoffBeforeRetry`, the commit/restoreCommit split).
## 3.3 Extensibility
Unchanged: reconciliation operates on canonical XID values via the existing
`XaFacade`/`XaGroupOps` abstractions.
## 3.4 Historical-Version Compatibility
Unchanged: no wire/state-format change; the risk is purely operational and
disclosed for the gap case (Issue 1 extends this disclosure to the
prefix/all-absent case).
# 4. Issue Summary
| Number | Issue | Location | Severity |
| --- | --- | --- | --- |
| 1 | Absent-from-recovery-scan XIDs (prefix skip, all-absent batch) are
assumed committed with no way to distinguish an externally-issued rollback;
only the gap/fail-closed case is disclosed in the migration guide |
`JdbcSinkAggregatedCommitter.java:205-237`,
`docs/en(zh)/introduction/concepts/incompatible-changes.md` | Medium |
| N/A-2 | `XaFacade.commit(xid, ignoreUnknown=true)` has no production
caller after evidence-based replay replaced `XAER_NOTA`-as-success inference
(li3zhi4, still open) | `JdbcSinkAggregatedCommitter.java`,
`XaFacadeImplAutoLoad.java` | Low |
| N/A-3 | PR description overstates the all-absent case as "fails closed"
when code/docs treat it as already-resolved (li3zhi4, still open,
description-only) | PR description text | Low |
# 5. Merge Recommendation
### Conclusion: Ready to merge
Issue 1 is a real, previously-unarticulated residual risk, but it's an
inherent consequence of XA's own commit/rollback ambiguity rather than an
implementation defect, it's already partially mitigated by WARN-level logging,
and the fix is a one-line addition to an already-thorough migration guide
rather than a code change — I don't think it should block merge, but I'd like
it addressed before this lands given how much this PR already invests in
disclosing the gap-case risk to operators.
Restating what hasn't changed: both outstanding `CHANGES_REQUESTED` reviews
(nzw921rx 2026-07-27, davidzollo 2026-08-06) target concerns the current
implementation structurally resolves — I re-verified this myself from source
rather than assuming my prior rounds still hold. That's a process blocker, not
a code blocker: I'm the PR author and cannot dismiss another reviewer's review
or approve my own PR; this needs nzw921rx/davidzollo to re-review the current
head, or a maintainer with write access to dismiss the stale reviews. @dybyte's
`APPROVED` (2026-08-28, same head modulo the empty CI-retry commit) is
independent confirmation the current implementation is sound.
1. Blockers (process, not code): the two stale `CHANGES_REQUESTED` reviews
need to be revisited/dismissed by their authors or a maintainer. CI needs a
clean rerun given the four confirmed-unrelated failure causes above
(independently re-verified this round, not just carried forward).
2. Recommended fixes (non-blocking): add the one-line migration-guide
disclosure for Issue 1; the two low-severity carryovers from li3zhi4 (PR
description wording, unused `ignoreUnknown=true` path).
No alternative implementation approach changes here; the
commit-order-evidence design remains the right shape given that Zeta doesn't
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]