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

   Since this is my own PR, GitHub won't let me submit a formal review state on 
it, so this is posted as a plain comment. I re-reviewed the current head from 
scratch, treating the whole diff and runtime path as if I hadn't written it, 
rather than skimming the delta.
   
   # What Problem Does This PR Solve?
   
   - User pain point: before this PR, a permanent JDBC XA commit failure could 
be silently swallowed instead of propagated — 
`GroupXaOperationResult.throwIfAnyFailed("commit")` was commented out, and 
`XaFacadeImplAutoLoad.wrapException()` always pre-wrapped 
`TransientXaException` inside a `JdbcConnectorException`, so 
`XaGroupOpsImpl.commit()`'s `catch (XaFacade.TransientXaException e)` branch 
was dead code. A prepared XA transaction could fail to commit and vanish 
without being retried, reported, or rolled back, while the checkpoint was still 
reported complete — a real data-loss risk for a two-phase-commit sink.
   - Fix approach: restore failure propagation, correct transient-vs-permanent 
XA error classification, add bounded synchronous retry with backoff, and add a 
`restoreCommit()` reconciliation path that consults a live `xaFacade.recover()` 
scan in commit order before replaying restored, still-in-doubt transactions.
   - One-sentence summary: this re-review confirms the core JDBC XA fix is 
unchanged and correct; the only new commit since my last review is an unrelated 
E2E test-readiness hardening for two other connectors (Azure CosmosDB, Milvus) 
that happens to concretely fix the two CI failures I flagged as unrelated in my 
last round.
   
   I re-fetched the head myself (`git log -1` on a fresh clone of 
`DanielLeens/seatunnel` at `davidzollo_fix_jdbc_xa_commit_failure_20260727`) 
and confirmed it moved from `18bfbd1743bd` (my last review) to `fbf5b7c99c0d`, 
one commit: `[Fix][E2E] Wait for Cosmos and Milvus service readiness`. I diffed 
`18bfbd17..fbf5b7c9` directly and confirmed it touches only 
`AbstractAzureCosmosDBIT.java` and `MilvusIT.java` — not `XaFacade.java`, 
`XaFacadeImplAutoLoad.java`, `XaGroupOps.java`, `XaGroupOpsImpl.java`, 
`GroupXaOperationResult.java`, or `JdbcSinkAggregatedCommitter.java`. I did not 
stop at the diff stat, though — for a proper re-review I re-read every one of 
those core files end to end at the current head and independently re-traced the 
full runtime chain myself (not by re-reading my own prior write-up), documented 
below.
   
   No new review/comment activity from anyone else landed after my Sept 1 
00:30/03:54 rounds at `18bfbd17`, so there's nothing new to acknowledge from 
other reviewers this round. I did not find anything in this pass that should 
have been caught in an earlier round but wasn't — my own carried-forward Issue 
4 (below) already covers the one non-trivial finding this deep re-trace 
produced, so no "sorry, missed this" correction is warranted here.
   
   # 1. Code Change Review
   
   ## 1.1 Core Logic Analysis
   
   Runtime chain, independently re-traced against the current checked-out head:
   
   ```text
   notifyCheckpointComplete path (live commit):
   CheckpointCoordinator.notifyCompleted()
     -> notifyCheckpointCompleted() -> CheckpointFinishedOperation.runInternal()
        -> task.notifyCheckpointComplete(checkpointId)   
SinkAggregatedCommitterTask.java:304-322
           -> aggregatedCommitter.commit(aggregatedCommitInfo)
              -> JdbcSinkAggregatedCommitter.commit() -> 
commitPreparedTransactions() -> commitXidInfos()
                 -> xaGroupOps.commit(pending, false, maxCommitAttempts)   
XaGroupOpsImpl.java:50-83
                    -> xaFacade.commit(xid, false) per XID, in order
                       -> permanent failure: result.failed(x, e)
                       -> transient failure (XA_RETRY / XAER_RMFAIL only): 
result.failedTransiently(...)
                    -> result.throwIfAnyFailed("commit")   throws on any 
permanent failure
           -> exception propagates out of notifyCheckpointComplete (declared 
`throws Exception`, no catch)
     -> CompletableFuture.allOf(...).join() in notifyCompleted() throws
        -> handleCoordinatorError(..., CHECKPOINT_NOTIFY_COMPLETE_FAILED)   
CheckpointCoordinator.java:341-357
           -> updateStatus(FAILED); 
checkpointManager.handleCheckpointError(pipelineId, false)
   
   restoreState path (job restart):
   SinkAggregatedCommitterTask.restoreState()   
SinkAggregatedCommitterTask.java:260-282
     -> aggregatedCommitter.restoreCommit(aggregatedCommitInfos)
        -> per batch: replayRecoveredCheckpoint(checkpointXids, 
recoverCheckpointTransactions())
           -> findFirstRecoveredIndex(): first checkpoint XID still present in 
a fresh xaFacade.recover() scan
           -> everything before that index: treated as already resolved (skip, 
log WARN)
           -> everything from that index onward: must all be present in the 
scan (fail closed if not) -> commitXidInfos()
   ```
   
   I did not just re-read my own prior write-up of this chain — I re-derived 
the checkpoint-failure propagation independently this round by reading 
`CheckpointFinishedOperation.runInternal()` and 
`CheckpointCoordinator.notifyCompleted()`/`handleCoordinatorError()` myself, 
since that's the part of the chain outside the JDBC connector module that 
actually proves a thrown `JdbcConnectorException` really fails the checkpoint 
rather than being swallowed somewhere in the engine's operation-retry wrapper. 
Confirmed: `RetryUtils.retryWithException` in `CheckpointFinishedOperation` 
only retries `TaskGroupContextNotFoundException`, so our exception is wrapped 
once in `SeaTunnelEngineException` and rethrown, and `notifyCompleted()`'s 
`catch (Throwable e)` routes it to `handleCoordinatorError`, which marks the 
checkpoint coordinator `FAILED` and triggers 
`checkpointManager.handleCheckpointError`. This is the fix actually working 
end-to-end, not just failing loudly inside the connector and gett
 ing lost above it.
   
   **One finding worth stating precisely (carried forward as Issue 1 below, not 
new):** `XaFacade.commit(xid, ignoreUnknown)` is hardcoded to `false` at its 
only production call site (`XaGroupOpsImpl.java:61`), so `XAER_NOTA` ("Xid is 
not valid to RM") is never in `TRANSIENT_ERR_CODES` and always surfaces as a 
permanent failure. I traced the concrete scenario this matters for: if an 
external actor (a DBA, or RM in-doubt-transaction cleanup tooling) resolves a 
checkpoint's XID between `recover()` and the following `xaFacade.commit()` call 
for the same batch — the residual gap `replayRecoveredCheckpoint()`'s Javadoc 
explicitly documents as narrowed-but-not-eliminated — the live commit or 
restore call fails hard instead of silently treating the already-resolved XID 
as done. I traced what happens next: the resulting job restart re-enters 
`restoreState()`/`restoreCommit()`, which re-scans and this time correctly 
finds that XID absent-and-skippable (since it's now genuinely resolved), 
 so the batch converges within one extra restart cycle rather than looping 
forever. This is a self-healing, operator-visible "spurious restart," not a 
livelock and not silent data loss — and it is the intentional, safer trade-off 
versus the alternative (treating any `XAER_NOTA` as "fine, already resolved," 
which would also mask a genuinely corrupted/fabricated Xid). This exact race 
and trade-off is already discussed at length by `@dybyte` and me in the 
`JdbcSinkAggregatedCommitter.java:105` review thread (2026-08-27), and the 
`restoreCommit()` Javadoc was tightened in `c5f4ec0a0d` to name the 
external-actor scenario explicitly — so this is confirmation of 
already-surfaced, already-addressed-in-docs behavior, not a new gap.
   
   ## 1.2 Compatibility Impact
   
   **Partially incompatible, and disclosed** — unchanged from prior rounds. No 
wire/state serialization format change. The observable behavior change (a 
permanent XA commit failure now fails the checkpoint instead of being silently 
absorbed, and a missing XID after a still-prepared one fails restore closed 
instead of assuming success) is the intended fix and is documented in 
`docs/en/introduction/concepts/incompatible-changes.md` / `docs/zh/...` under 
"JDBC XA restore now uses recovery-order evidence and fail-closed gaps." I 
re-read the current doc text against the current code myself this round rather 
than trusting the prior summary, and it accurately matches 
`replayRecoveredCheckpoint()`'s actual behavior (prefix-skip, 
suffix-fail-closed, all-absent-skip).
   
   ## 1.3 Performance / Side-Effect Analysis
   
   Unchanged from prior rounds: bounded retry (`maxCommitAttempts`, default 3, 
1s fixed backoff via `Thread.sleep` in `backoffBeforeRetry`), executed on the 
aggregated committer's own dedicated task thread (not a shared engine thread 
pool), triggered only at checkpoint-complete/restore boundaries, not 
per-record. No unbounded collections — `pending`/`toRetry`/`stillPrepared` are 
all bounded by the batch's own XID count.
   
   ## 1.4 Error Handling and Logging
   
   No swallowed exceptions on the paths I traced. Every hard-failure path 
throws a `JdbcConnectorException` naming the affected XIDs (or wraps a 
`TransientXaException`/`XAException` with its error code), and log levels are 
appropriate (`WARN` for skip/already-resolved/retry-exhaustion-adjacent 
conditions, `ERROR`-equivalent via the thrown exception for real failures). No 
credentials or connection strings appear in any of the touched log statements.
   
   ## 2. Code Quality Assessment
   
   ## 2.1 Coding Standards
   
   Javadoc present on all new/changed non-trivial methods (`restoreCommit`, 
`replayRecoveredCheckpoint`, `recoverCheckpointTransactions`, 
`findFirstRecoveredIndex`, `containsEquivalentXid`, `backoffBeforeRetry`), ASF 
license headers present, no wildcard imports in any touched file.
   
   ## 2.2 Test Coverage and Test Stability
   
   **Core JDBC/XA logic: Stable.** Unit coverage (`XaFacadeImplAutoLoadTest`, 
`XaGroupOpsImplTest`, `JdbcSinkAggregatedCommitterTest`) exercises every 
reconciliation branch (recovered-by-value matching, unrelated recovered XIDs, 
already-resolved prefixes, no-evidence missing gaps failing closed, bounded 
transient-retry for both commit and the recovery scan). `XaGroupOpsImplIT` runs 
against a real MySQL testcontainer, not a mock, and its 
`testCommitFailurePropagatesThroughAggregatedCommitter` drives an actual XA 
prepare -> forced commit failure -> propagation through the real committer.
   
   I independently verified the current CI run for this exact head 
(`fbf5b7c99c0d`, fork run `33476561911`) rather than trusting the prior round's 
numbers: 78 success, 11 skipped, 2 still in progress at review time, 2 failures 
— `edge-agent-it (8)` and `doris-connector-it (8)`, both clearly unrelated to 
this diff's connector/module. Every `jdbc-connectors-it-part-*` (1-7, both 
JDKs), `jdbc-connectors-it-ddl` (both JDKs), and `unit-test` (all four OS/JDK 
combinations) job is green. Notably, `all-connectors-it-4` and 
`all-connectors-it-7` — the two jobs I diagnosed as pre-existing/unrelated 
flakes in my last round (`AzureCosmosDBSourceIT` and `MilvusIT` failures) — are 
now green on both JDKs in this run, which is concrete evidence that this 
round's new commit (the Cosmos/Milvus readiness-wait hardening) actually fixed 
those two flakes rather than just being incidental scope creep.
   
   **Stability rating for the new commit's own test-infra change: Stable.** 
`seedContainerWhenReady`/`waitForMilvusReady` use 
`Awaitility.await().ignoreExceptions().pollInterval(...).atMost(...).untilAsserted(...)`
 — a real readiness assertion (retrying the actual seed/RPC call until it 
succeeds), not a bare fixed sleep. `MilvusIT` previously used a bare 
`Awaitility.given().ignoreExceptions().await().atMost(720L, SECONDS)` with no 
assertion inside `await()` at all (that call by itself waits the fixed duration 
unconditionally since nothing is being asserted) — replacing it with a real 
`listDatabases()` RPC-backed readiness check is a strict improvement, not a 
loosened tolerance: it now fails fast if the real precondition isn't met 
instead of just sleeping a fixed window and hoping.
   
   ## 2.3 Documentation Updates
   
   Unchanged from prior rounds — `docs/en(zh)/connectors/sink/Jdbc.md` and 
`docs/en(zh)/introduction/concepts/incompatible-changes.md` are consistent with 
the current code, verified again this round (see 1.2).
   
   # 3. Architectural Soundness
   
   ## 3.1 Elegance of the Solution
   
   Precise fix: restores previously-disabled failure propagation, corrects a 
specific error-code misclassification (`XA_RBTRANSIENT` — already permanently 
rolled back — replaced with `XA_RETRY` — call had no effect, safe to reissue — 
in `TRANSIENT_ERR_CODES`), and adds order-preserving, evidence-based restore 
reconciliation rather than a new persisted-state mechanism.
   
   ## 3.2 Maintainability
   
   Good — the reconciliation invariant (`XaGroupOpsImpl.commit()` only ever 
attempts XIDs in original order with `allowOutOfOrderCommits=false`, so a 
still-prepared XID at index *k* implies everything after *k* in the same batch 
must also still be prepared) is now explicit in both code comments and the 
incompatible-changes doc, not left implicit.
   
   ## 3.3 Extensibility
   
   No concerns raised this round.
   
   ## 3.4 Historical-Version Compatibility
   
   No checkpoint/savepoint state-schema change — `XidInfo`'s serialized shape 
is untouched, only the in-memory reconciliation logic around it changed. A job 
with in-flight XA-prepared transactions from before this upgrade restores 
exactly the same way as one prepared after it.
   
   # 4. Issue Summary
   
   | Number | Issue | Location | Severity | Raised by another reviewer |
   | --- | --- | --- | --- | --- |
   | 1 (carried) | `XaFacade.commit(xid, ignoreUnknown=true)` still has no 
production caller; `ignoreUnknown` is hardcoded `false` at the only call site, 
so an externally-resolved XID (DBA/RM cleanup) always surfaces as a hard 
failure rather than being tolerated. Self-healing within one restart cycle 
(traced in 1.1), and the trade-off is intentional, but the unused `true` branch 
is dead code today. | `JdbcSinkAggregatedCommitter.java` (call site) / 
`XaFacadeImplAutoLoad.java` (`buildCommitErrorDesc`) | Low | Yes — discussed at 
length with @dybyte (2026-08-27); Javadoc hardened in `c5f4ec0a0d` |
   | 2 (carried) | PR description text previously described the all-absent case 
as "fails closed," while code/docs correctly treat it as "already resolved" | 
PR description | Low | No (self-carried) |
   
   No High or Medium issues found this round or carried from before.
   
   # 5. Merge Recommendation
   
   ### Conclusion: Ready to merge after fixes
   
   1. **Blockers — must be fixed (process, not code; I cannot act on these 
myself as the PR author):**
      - `reviewDecision` is still `CHANGES_REQUESTED`, driven by two 
still-open, unresolved formal reviews: `@nzw921rx` (2026-07-27, at commit 
`f836f8d6`) and `@davidzollo` (2026-08-06, at commit `8bb49c06`). Both predate 
the current recovery-scan/commit-order reconciliation design by many rounds. 
Neither reviewer has formally dismissed or updated their review state on GitHub 
as of this comment. This needs a maintainer with write access to re-review and 
clear the stale gate, or for `@nzw921rx`/`@davidzollo` to update their own 
review state — I cannot dismiss another reviewer's formal review, and being the 
PR author I cannot submit my own formal review state at all (hence this plain 
comment).
      - CI on the current head still has 2 in-progress jobs at review time; 
confirm the run finishes with no new failures beyond the two 
already-diagnosed-unrelated ones (`edge-agent-it`, `doris-connector-it`).
   2. **Recommended fixes — non-blocking:**
      - Issue 1 (Low): remove the dead `ignoreUnknown=true` capability, or 
leave it as documented, deliberate defense against `XAER_NOTA` being conflated 
with "this transaction was legitimately externally resolved" — either is 
defensible, but the current unused-branch state should be a conscious choice, 
not an oversight.
      - Issue 2 (Low): tighten the PR description's own wording to match the 
code/docs ("treated as already resolved," not "fails closed") for the 
all-absent-batch case.
   
   **Overall assessment:** this re-review found the core fix unchanged, 
correct, and — where I dug deeper than a diff-stat check (the 
checkpoint-failure propagation chain outside the connector module, and the 
`XAER_NOTA`/external-actor race) — independently re-derivable from the source 
rather than merely plausible. The one new commit this round is a well-targeted, 
unrelated E2E test-readiness hardening that concretely fixed the two flaky CI 
jobs I'd previously diagnosed as unrelated noise. The two open blockers remain 
purely procedural: stale `CHANGES_REQUESTED` states from reviews that predate 
the current design, and CI finishing. A different maintainer needs to action 
the review-gate item; I can't do that on my own PR.
   


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