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

   # What Problem Does This PR Solve?
   - User pain point: same as #11727 — `BlockingWorker.run()` used to resolve 
its execution context before entering its `try` block, so a stale-generation 
`taskDone()` racing the deploy of a new generation could throw an uncaught NPE 
before `startedLatch.countDown()` ever fired, leaving `submitBlockingTask()` 
(and the caller holding the `SubPlan` monitor) blocked forever — the silent 
hang behind #11679.
   - Fix approach: byte-for-byte identical to #11727's current head 
(independently re-verified below) — move context/class-loader resolution inside 
`try`, throw a descriptive `IllegalStateException` on a missing context, 
guarantee `startedLatch.countDown()` fires exactly once via a 
`startLatchReleased` flag in `finally`, and add null guards in 
`recycleClassLoader()` and `taskDone()`'s handoff to 
`finishedExecutionContexts`.
   - One-sentence summary: this branch remains a writable-fork CI-refresh 
vehicle for #11727, not an independent fix, and nothing about that situation 
has changed since my last review.
   
   # 0. Third-round disclosure — this is unchanged since my last review
   
   I've now reviewed this exact content four times total (twice on #11727, 
twice on #11910). Live re-verification at the time of this pass:
   - **No new commit** on #11910 since my last review (`ffe51c08a551`, 
submitted `2026-08-21T14:06:30Z`) — still the current head.
   - **No new comments/replies** on #11910 since then.
   - I re-diffed `refs/pr-review/11727` (head `72268def15f0`) against 
`refs/pr-review/11910` (head `ffe51c08a551`) on the two files that matter — 
`TaskExecutionService.java` and `TaskDeployStaleContextRaceTest.java` — and 
confirmed **zero diff output**, i.e. still byte-identical. The two PRs continue 
to diverge only in unrelated `dev`-drift files that neither PR's own commits 
touch.
   - I re-read the full current source at the three locations @SEZ9 and I 
flagged last round and confirm all three findings are still present and 
unchanged (details in 1.1 below) — this is not new analysis, it's independent 
re-confirmation that nothing drifted.
   - **What has NOT happened since my last review, and should have**: I said in 
my prior review that @SEZ9's findings "apply verbatim to #11727's current 
head... and should be raised/resolved there." I checked #11727's comment 
history live just now — that hand-off never happened. I'm closing that loop 
myself in this pass rather than flagging it a third time without acting (see 
Section 5).
   
   # 1. Code Change Review
   ## 1.1 Core Logic Analysis — re-confirmed against current source, not just 
carried forward
   
   I independently re-read (not re-quoted) the three locations in question this 
pass:
   
   **`recycleClassLoader()`/`taskDone()` ordering 
(TaskExecutionService.java:1421-1497) — confirmed real.**
   ```java
   void taskDone(Task task) {
       ...
       if (completionLatch.decrementAndGet() == 0) {
           recycleClassLoader(taskGroupLocation);                              
// re-resolves via get()
           TaskGroupContext finishedContext = 
executionContexts.remove(taskGroupLocation);
           ...
       }
   }
   private void recycleClassLoader(TaskGroupLocation taskGroupLocation) {
       TaskGroupContext context = executionContexts.get(taskGroupLocation);    
// independent lookup
       if (context == null) { ...; return; }
       context.setClassLoaders(null);
       for (Collection<URL> jars : context.getJars().values()) {
           classLoaderService.releaseClassLoader(taskGroupLocation.getJobId(), 
jars);
       }
   }
   ```
   `recycleClassLoader()` re-resolves the context via its own 
`executionContexts.get()` instead of operating on a reference `taskDone()` pins 
down, and it runs *before* `executionContexts.remove()`. Because 
`TaskGroupLocation` is reused verbatim across restore generations (the exact 
premise of this whole fix), two different generations' trackers can each 
observe a non-null context here and each call 
`classLoaderService.releaseClassLoader(...)` — a double release at best, and at 
worst (if a newer generation has already `put()` its own context under the same 
key by the time an older generation's `taskDone()` runs) the *old* generation's 
`recycleClassLoader()` nulls out and releases the *new*, still-in-use 
generation's class loaders. This is a genuine correctness gap in the exact race 
this PR is supposed to be closing, not a hypothetical.
   
   **`getClassLoaders()` NPE window (TaskExecutionService.java:1082-1096) — 
confirmed real.**
   ```java
   TaskGroupContext taskGroupContext = executionContexts.get(taskGroupLocation);
   if (taskGroupContext == null) {
       throw new IllegalStateException(...);
   }
   
Thread.currentThread().setContextClassLoader(taskGroupContext.getClassLoaders().get(t.getTaskID()));
   ```
   Between the null check and the dereference, a concurrent stale 
`recycleClassLoader()` can call `context.setClassLoaders(null)` on this exact 
same `TaskGroupContext` instance (the ordering bug above makes this more 
likely, not less), producing an undiagnosed NPE instead of the deliberate 
`IllegalStateException` this PR was written to introduce. Also confirmed: 
`.get(t.getTaskID())` returning `null` lets `setContextClassLoader(null)` 
succeed silently, so the task would run under the system class loader and fail 
much later with a confusing `ClassNotFoundException` rather than failing fast 
here.
   
   **`Task.close()` reachable pre-`init()` 
(TaskExecutionService.java:1088,1125) — confirmed real, Low severity.**
   When the new `IllegalStateException` throws, `result` stays `null`, so the 
unchanged `finally` block's `if (result == null || !result.isDone()) { 
tracker.task.close(); }` still runs `close()` on a task whose `init()` never 
executed and whose task class loader was never installed. Low risk in practice 
— `taskGroupExecutionTracker.taskDone(t)` has already run by that point, so the 
original hang is already avoided regardless of what `close()` does — but a 
connector `close()` that assumes `init()`-populated state could throw a 
confusing secondary exception here.
   
   I am not re-deriving @SEZ9's remaining Issues 4/6/7/8 (test 
fidelity/busy-spin, reflection-vs-accessor, list-construction nit) a third time 
— they were plausible and internally consistent when I checked them last round, 
I have no counter-evidence, and re-deriving them again from scratch this pass 
would not change their disposition.
   
   ## 1.2 Compatibility Impact — Fully compatible (unchanged)
   Same-process, in-memory control-flow fix; no 
wire/state/checkpoint/serialization changes.
   
   ## 1.3 Performance / Side-Effect Analysis (unchanged, restated)
   Negligible direct cost. The still-open ordering bug in 1.1 is itself a 
side-effect risk (premature/duplicate class-loader release), not a new 
observation this round but worth restating since it's the crux of why this 
can't be called "ready to merge" as-is.
   
   ## 1.4 Error Handling and Logging
   
   | # | Location | Problem | Risk | Suggested fix | Severity | Raised by 
another reviewer |
   |---|----------|---------|------|----------------|----------|------|
   | 1 | `TaskExecutionService.java:1421` (call site), `:1481` (re-`get()`) | 
`recycleClassLoader()` re-resolves via `executionContexts.get()` instead of the 
caller-pinned instance `taskDone()` intends to recycle | Cross-generation 
double-release, or release of a live generation's class loaders | Have 
`taskDone()` capture `executionContexts.remove(taskGroupLocation)` first (CHM 
guarantees a single winner) and recycle only that specific non-null instance | 
Medium | Yes (@SEZ9), independently re-confirmed twice now (my prior review + 
this one) |
   | 2 | `TaskExecutionService.java:1096` (dereference), `:1493` (concurrent 
null-out) | `getClassLoaders()` can NPE between the null-context check and its 
use, racing a concurrent `setClassLoaders(null)` | Undiagnosed NPE instead of 
the intended `IllegalStateException`; silent null-TCCL fallback if the per-task 
lookup misses | Snapshot both the class-loader map and the per-task class 
loader into locals; throw the same descriptive `IllegalStateException` when 
either is null | Medium | Yes (@SEZ9), independently re-confirmed twice now |
   | 3 | `TaskExecutionService.java:1088`, `:1125` | New no-context failure 
path still reaches `Task.close()` on a task that never `init()`'ed | Secondary, 
confusing exception from a `close()` that assumes `init()`-populated state | 
Track an `initialized` flag alongside `startLatchReleased`; skip `close()` on 
the pre-init failure path | Low | Yes (@SEZ9), independently re-confirmed |
   | 4-8 | See @SEZ9's review body on this PR | Recycle-skip class-loader leak; 
test fidelity/busy-spin; reflection vs. accessor; list-construction nit | As 
described there | As described there | Medium/Low (per @SEZ9) | Yes (@SEZ9), 
not independently re-derived a third time this pass |
   
   # 2. Code Quality Assessment
   ## 2.1 Coding Standards — unchanged, good (Javadoc, descriptive exception, 
ASF header present).
   ## 2.2 Test Coverage and Test Stability
   `TaskDeployStaleContextRaceTest` remains a well-shaped, timeout-bounded, 
non-`Thread.sleep` regression test — **Stable**, unchanged from prior rounds. 
@SEZ9's Issue 4 (the remover thread's bare `remove()` doesn't simulate the real 
`recycleClassLoader()`-then-`remove()` interleaving) is a legitimate 
coverage-completeness gap on top of "stable," not a stability regression — it's 
precisely why the ordering bug in Issue 1 wasn't caught by CI.
   ## 2.3 Documentation Updates — none required, unchanged.
   
   # 3. Architectural Soundness
   ## 3.1 Elegance of the Solution — Precise fix for the bug it targets (latch 
never released), with a closely-related bug (recycle ordering) left open in the 
same method it touches. Unchanged assessment.
   ## 3.2 Maintainability — Good, unchanged.
   ## 3.3 Extensibility — No impact.
   ## 3.4 Historical-Version Compatibility — No wire/state/checkpoint impact, 
unchanged.
   
   # 4. Issue Summary
   | # | Issue | Location | Severity |
   |---|-------|----------|----------|
   | 1 | `recycleClassLoader()` re-resolves via `get()` instead of a 
caller-pinned context — cross-generation double-release / live-generation 
class-loader release | `TaskExecutionService.java:1421,1481` | Medium |
   | 2 | `getClassLoaders()` NPE window between null-check and use | 
`TaskExecutionService.java:1096,1493` | Medium |
   | 3 | `Task.close()` reachable pre-`init()` via the new failure path | 
`TaskExecutionService.java:1088,1125` | Low |
   | 4-8 | Recycle-skip leak; test fidelity/busy-spin; reflection vs. accessor; 
list nit | see @SEZ9's review | Medium/Low |
   | 9 | This PR is a duplicate CI-refresh vehicle for #11727; author has 
agreed it should close as superseded, and the findings above still needed 
forwarding to #11727 as of this review | N/A (procedural) | High (procedural) |
   
   # 5. Merge Recommendation
   ### Conclusion: Not recommended for merge on this PR specifically — closing 
as superseded, findings forwarded to #11727
   
   This is genuinely unchanged from my last pass, and I don't think re-stating 
the same recommendation a third time without acting on it is the right way to 
handle that. So, in addition to posting this review, I am taking the two 
follow-up actions my last review said should happen but didn't:
   
   1. **Forwarding @SEZ9's Medium findings (Issues 1 and 2 above) to #11727** 
as a comment, since the code is byte-identical and #11727 is the PR that will 
actually carry the fix forward. These should be resolved there before merge — 
they're genuine correctness gaps in the exact class-loader lifecycle this fix 
is meant to make safe, caught by @SEZ9 and independently confirmed by me across 
two rounds now.
   2. **Closing #11910** as superseded by #11727, per @abdessalems's explicit 
request on #11727 ("Since #11910 is the duplicate, feel free to close it as 
superseded," 2026-08-21T04:09:03Z) — with a closing comment pointing back at 
#11727 and this consolidated finding list so nothing raised across these 
reviews gets lost.
   
   Nothing on this specific branch needs further code changes — it exists only 
as a CI-refresh vehicle and its job (giving the fix a green, in-repo Build run) 
is separate from whether the underlying fix is merge-ready, which it is not 
yet, pending Issues 1 and 2 above on #11727.
   
   Thanks again to @SEZ9 for the thorough independent pass — the ordering bug 
in particular is a real gap that neither #11727's original review rounds nor 
mine caught the first time around, and it's exactly the kind of thing this fix 
needs closed before it merges.
   


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