DanielLeens commented on PR #11569:
URL: https://github.com/apache/seatunnel/pull/11569#issuecomment-5340775131
Thanks for the ping, @davidzollo, and for re-checking the restore
reconciliation.
This is a re-verification of the current head,
`17234b67e1d5574f116358f7b8f3b74a862ce496`. I want to be precise about what
actually changed since my last review,
`03f7fd4347f7870d494b8e2babb2f362e947ec74` (2026-08-19, a few hours ago),
because it matters for what this comment is and isn't:
```
git diff 03f7fd4347f7..17234b67e1d5 --stat
```
returns **empty**. The only commit on top of `03f7fd4347f7` is `17234b67e1d`
itself, `[Chore] Retrigger CI for PR #11569` — an empty commit with no file
changes. So this is not a new code version; it is the exact same diff I already
did a full from-scratch re-trace against this morning. I'm not going to pretend
to "discover" things again on identical code — I'll restate my open conclusion
precisely and respond to the new activity since then.
# 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 unreachable dead code). A checkpoint could be reported successful
while the transaction stayed prepared-but-uncommitted in the database.
- Fix approach: reclassify `XA_RBTRANSIENT` -> `XA_RETRY`, make
`wrapException` return instead of throw, re-enable `throwIfAnyFailed`, and
reconcile checkpoint-restored XIDs against a fresh `xaFacade.recover()` scan
using a commit-order (prefix/suffix) invariant rather than 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 — but the current
fail-closed branch for "no evidence either way" is itself unsound, which is
Issue 1 below.
# 1. Code Change Review
## 1.1 Core Logic Analysis — status of my own Issue 1
Re-read the code in place on the current head to confirm nothing moved:
`JdbcSinkAggregatedCommitter.java:193-215` (`replayRecoveredCheckpoint`):
```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));
}
```
Unchanged from what I traced this morning.
`testRestoreCommitFailsClosedWhenRecoveryScanHasNoCheckpointXid`
(`JdbcSinkAggregatedCommitterTest.java:144-164`) still asserts exactly this
throw, so the behavior is intentional and still covered — it's just the wrong
outcome for the case it covers.
The problem, restated once more for the record since it's the actual
blocker: `CheckpointCoordinator` sets `latestCompletedCheckpoint` as the
durable restore point *before* the phase-2 XA commit for that checkpoint even
runs (`CheckpointCoordinator.java:1330` precedes the `notifyCompleted()` call
at `:1335`), and it stays pointed at that checkpoint until the *next*
checkpoint independently completes. Any restart in that window — for a reason
having nothing to do with XA at all (an unrelated source/task failure, a node
loss) — replays a `restoreCommit()` call whose XidInfo list has, by then,
already been fully and successfully committed by a prior, healthy
`notifyCheckpointComplete()`. Every XID in that batch is legitimately absent
from the resource manager (successfully committed and forgotten, standard XA
behavior), so `findFirstRecoveredIndex` returns `-1` for the *entire* batch —
and the code now treats total absence as proof of an unrecoverable problem
rather than the si
ngle most likely, most benign explanation. Because the RM state driving this
decision doesn't change between restart attempts, the job is stuck permanently
until an operator manually intervenes.
This is a real regression relative to every earlier version of this PR I
reviewed, including the one @nzw921rx and @davidzollo raised CHANGES_REQUESTED
against: the pre-`dd143c0445c6` code tolerated every absent XID
unconditionally, which correctly handled the "whole batch already committed"
case (at the cost of the narrower data-loss risk both of you originally, and
correctly, flagged). The current code closes that data-loss risk but, in doing
so, converts the single most common healthy-restart case into a hard failure
loop. That's a worse outcome for availability than the original bug was for
correctness, and it doesn't require reverting the good parts of this rework —
see the fix direction in Issue 1 below.
## 1.2 Compatibility Impact
**Classification: Fully compatible at the API/config/serialization level**,
unchanged from my prior assessment — no public API break, no config key/default
change, no checkpoint state schema change (`XidInfo`/`JdbcAggregatedCommitInfo`
untouched). The intended behavioral tightening (restore can now fail rather
than silently guess) is documented in `incompatible-changes.md` in both
`docs/en` and `docs/zh`. That documentation is accurate for the code as
written, but the code it describes has the Issue 1 bug, so the compatibility
note currently undersells the operational risk for the all-absent case
specifically.
## 1.3 Performance / Side-Effect Analysis
Unchanged from this morning: `recoverCheckpointTransactions()` adds one
bounded-retry `xaFacade.recover()` scan on restore only (not steady-state
commit); `commitXidInfos`'s round cap has no backoff between rounds (Issue 2,
carryover, still open) — `XAER_RMFAIL`-class outages will still burn the whole
`max_commit_attempts` budget in microseconds.
## 1.4 Error Handling and Logging
Same two open items as this morning's review, both carryovers, neither new:
- **Issue 1 (Blocking):** the `firstRecoveredIndex < 0` branch (above).
- **Issue 2 (Medium):** no backoff between synchronous retry rounds
(`JdbcSinkAggregatedCommitter.java:124-142`, `:234-255`).
# 2. Code Quality Assessment
## 2.1 Coding Standards
Unchanged: Javadoc on all new/changed methods matches their bodies, ASF
headers present, no wildcard imports, no `System.out.println`, AOSP formatting
consistent with the module.
## 2.2 Test Coverage and Test Stability
Test content is byte-for-byte identical to what I reviewed this morning
(confirmed via the empty `git diff --stat`). **Stability rating: Stable** — all
tests remain Mockito-based, single-threaded, deterministic on object
identity/counts/exception message content; no `Thread.sleep`, no shared static
state, no `@DisplayName` usage.
**Gap (Issue 3, Medium, carryover, still open):** there is still no test
where the *entire* checkpoint batch is legitimately, successfully
already-committed (a prior healthy `notifyCheckpointComplete()`, followed by an
unrelated restart) — as opposed to a prior *aborted* restore attempt. That's
the exact scenario Issue 1 describes, and it should be the first regression
test added alongside the fix. `XaGroupOpsImplIT` remains `@Disabled`; there is
still no real-database exercise of any part of this PR's XA commit/restore path.
## 2.3 Documentation Updates
`docs/en/connectors/sink/Jdbc.md`, `docs/zh/connectors/sink/Jdbc.md`, and
both `incompatible-changes.md` files document the restore-order/fail-closed
behavior consistently in both languages. As in 1.2, this is accurate
documentation of code that currently has the Issue 1 bug, so it should be
revisited once that's fixed rather than treated as a separate doc task.
# 3. Architectural Soundness
## 3.1 Elegance of the Solution
The commit-order/prefix-suffix reconciliation is a genuinely better idea
than either the original strict-everywhere or tolerant-everywhere positions
from the two earlier CHANGES_REQUESTED reviews — it grounds the tolerance
decision in a real invariant of this code's own sequential commit loop instead
of an unprovable assumption about resource-manager history. The remaining
problem is narrow: the boundary condition where the model has *zero*
still-prepared evidence to anchor the prefix/suffix split against. Fixing that
doesn't require touching the rest of the design.
## 3.2 Maintainability
Good: the responsibility split (`XaFacadeImplAutoLoad` classifies driver
outcomes, `XaGroupOpsImpl` owns grouped propagation,
`JdbcSinkAggregatedCommitter` owns restore reconciliation) is clean, and the
helper methods (`replayRecoveredCheckpoint`, `findFirstRecoveredIndex`,
`recoverCheckpointTransactions`) are small and single-purpose.
## 3.3 Extensibility
Unchanged: `XidKey` is a solid reusable canonical-comparison primitive; the
`default` method on `XaGroupOps.commit(...)` lets other implementors opt in
incrementally without a forced signature change.
## 3.4 Historical-Version Compatibility
No checkpoint-state schema change; old checkpoints/savepoints deserialize
unmodified under this code. Issue 1 is a decision-logic problem, not a
serialization/format compatibility problem, and it affects every upgrade path
equally regardless of checkpoint age.
# 4. Issue Summary
| # | Issue | Location | Severity | Status |
|---|-------|----------|----------|--------|
| 1 | All-absent restore batch throws unconditionally, misclassifying
"already fully committed by a prior healthy checkpoint, unrelated restart" (the
common case) as unrecoverable data loss; recurs on every restart since the
underlying RM state never changes, wedging the job permanently |
`JdbcSinkAggregatedCommitter.java:198-205` | High (Blocking) | Open, carried
forward unchanged from my 2026-08-19T04:31 review; code is byte-identical |
| 2 | No backoff between synchronous commit/recover retry rounds;
`max_commit_attempts` is largely inert against `XAER_RMFAIL`-class outages |
`JdbcSinkAggregatedCommitter.java:124-142`, `:234-255` | Medium | Open,
carryover from 2026-08-16 |
| 3 | No end-to-end or real-database test exercises commit/restore failure
propagating through to checkpoint/job failure, and specifically nothing
exercises "whole batch already committed by a prior successful commit";
`XaGroupOpsImplIT` remains `@Disabled` | test classes; `XaGroupOpsImplIT.java`
| Medium | Open, carryover from 2026-08-16 |
| 4 | CI on the current head (`17234b67e1d`) is a plain retrigger with no
functional diff; the fork's `Build` run for this exact SHA is still `queued` as
of this comment. The prior run against identical code (`03f7fd4347f7`, fork run
`32162717118`) was green on every JDBC/XA-relevant job (unit-test,
updated-modules-integration-test parts 1-8); its only failure was an unrelated
Paimon E2E flake | fork run `32235473661` (queued); prior identical-code run
`32162717118` | Low (process, not source) | Not yet resolved (run pending) |
# 5. Merge Recommendation
### Conclusion: Not recommended for merge
1. **Blockers — must be fixed**
- **Issue 1 (High):** the all-absent branch in
`replayRecoveredCheckpoint` must stop treating "no evidence either way" as
proof of failure. Concretely: `firstRecoveredIndex < 0` should log at WARN and
return (treating the whole batch as already resolved), not throw — this is the
single most likely, most benign explanation for total absence, unlike the
partial-absence "gap after a still-prepared XID" branch at `:209-215`, which is
correctly kept as fail-closed because that pattern cannot arise from a
legitimate sequential commit. This is a one-branch fix plus one new regression
test ("all-XIDs-already-committed restore batch completes without error"), not
a redesign — the surrounding commit-order/prefix-suffix machinery is sound and
should be kept exactly as-is.
2. **Recommended fixes — non-blocking**
- Issue 2 (Medium): add bounded backoff between synchronous retry rounds.
- Issue 3 (Medium): add a committer-level test exercising the "whole
batch already committed" restore scenario, and re-enable `XaGroupOpsImplIT`
with real XA coverage.
- Issue 4 (Low): let the pending fork CI run for `17234b67e1d` complete;
no action expected beyond confirming it matches the already-green result for
the identical `03f7fd4347f7` code.
**On @davidzollo's request to re-check and clear the stale review gate:**
the restore reconciliation you asked reviewers to re-check does correctly close
both your 2026-08-06 idempotency finding and @nzw921rx's 2026-07-27
normalization finding — I independently re-verified that again on this pass
(see 3.1). But it isn't ready to clear yet, because I found a new, more severe
issue in the same reconciliation logic on 2026-08-19 (Issue 1 above), a few
hours before your comment. That one hasn't been addressed by any commit since —
the only thing that landed after your comment was the CI-retrigger no-op. So
`reviewDecision=CHANGES_REQUESTED` should stay in place, now anchored to Issue
1 rather than to either of the original two findings.
**Overall assessment:** the underlying engineering direction is sound and
has meaningfully improved across every round of review on this PR. What's
blocking merge right now is one specific branch's failure-mode choice, not the
architecture around it. Once `firstRecoveredIndex < 0` returns instead of
throwing and that's backed by a regression test, I would expect to be able to
approve this on the next pass.
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]