rkhachatryan commented on PR #29065:
URL: https://github.com/apache/flink/pull/29065#issuecomment-5588018359

   Review generated with Claude Code (posted by @rkhachatryan; findings 
reviewed but the analysis below is Claude's, so please double-check the 
reasoning).
   
   Four observations, all stemming from the cleanup hook being a one-shot 
whole-directory delete rather than the refcount-aware close the PR description 
implies.
   
   ### 1. The hook bypasses `FetchedChannelState`'s refcount contract (medium)
   
   `FetchedChannelState` documents that "files are deleted only when the last 
lifecycle grant is released", but the hook deletes `baseDir` unconditionally. 
`cancelables` is registered on `resourceCloser` *after* 
`channelIOExecutor::shutdown` and `this::shutdownAsyncThreads` 
(`StreamTask:457/491/492`), and `AutoCloseableRegistry` closes in reverse order 
— so `cancelables.close()` runs before the channel-IO executor is drained. 
`cancel()` also calls `cancelables.close()` directly (`StreamTask:1395`), by 
design, to interrupt blocking I/O.
   
   Scenario: an operator fails shortly after recovery while `drain()` is still 
running on `channelIOExecutor`. The directory is deleted; 
`FetchedChannelStateReaderImpl.openFileAndSeek()` opens files lazily one at a 
time, so the next `Files.newByteChannel` throws `NoSuchFileException`, 
`drain()` reports it via `asyncExceptionHandler.handleAsyncException` → 
`failExternally`, adding a spurious failure that can mask the real root cause. 
Same for an async recovery-checkpoint snapshot reader still writing spill 
segments to checkpoint storage.
   
   Reordering the registration doesn't help (cancel closes the registry 
explicitly regardless). Suggestion: hand ownership to 
`FetchedChannelState::close` so the `closed` flag is at least set, and gate the 
reporting in `drain()` (`StreamTask:1102`) and the `exceptionally` handler on 
the existing `canceled` / `!isRunning` state — log at debug when the task is 
already going down — or throw a typed abort exception that `drain()` treats as 
expected.
   
   ### 2. One-shot registration lets post-abort spill files leak permanently 
(medium)
   
   Registration is guarded by `spillCleanupRegistered`, but file creation is 
not: every `ensureFileOpen()` re-runs `Files.createDirectories(baseDir)`.
   
   Scenario: the task is cancelled while `readInputData` is still fetching a 
large channel state (>64 MB, i.e. after at least one rotation). `cancel()` → 
`cancelables.close()` fires and *removes* the hook, deleting `baseDir`. The 
fetch loop on `channelIOExecutor` is not interrupted and never touches the 
registry again (the flag is already `true`, so no further `registerCloseable` — 
hence no `IOException` to abort it either), so it recreates `baseDir` and keeps 
writing `spill-segment-N.bin`. Those files are never deleted — the exact leak 
this PR fixes, now permanent until TM shutdown.
   
   Suggested fix — make the hook stateful instead of one-shot:
   
   ```java
   private final class SpillCleanup implements Closeable {
       private volatile boolean aborted;
       @Override public void close() {
           aborted = true;                                  // set before 
deleting
           FileUtils.deleteDirectoryQuietly(baseDir.toFile());
       }
   }
   ```
   
   and in `ensureFileOpen()`, after `files.add(filePath)`, bail out if 
`aborted`: close the stream, delete what was just written, throw `IOException`. 
That terminates the fetch loop and closes the write-after-delete window (files 
created before the hook fires are removed by it; files created after see the 
flag). Re-registering per file would also work — `registerCloseable` on a 
closed registry throws `IOException` and closes the argument 
(`AbstractAutoCloseableRegistry:89`) — but it accumulates entries.
   
   ### 3. The `finally`-close described in the PR text isn't in the diff (low)
   
   The description says the produced `FetchedChannelState` is force-closed in a 
`finally` in `readInputData`, but that code isn't there. On abort, 
`closeInternal()` has already built the state and called `acquire()`, and 
nothing releases it, so cleanup is deferred to whole-task teardown rather than 
happening at the abort point. Conversely, on the happy path the hook is never 
`unregisterCloseable`d after ownership transfers, so `cancelables` keeps a 
stale entry (and a strong reference to `baseDir`) for the task's whole lifetime 
and re-deletes the directory at teardown.
   
   Suggestion: in `closeInternal()`, after `producedChannelState = new 
FetchedChannelState(files); acquire();`, unregister the hook and register 
`producedChannelState` itself (it is `Closeable` and refcount-aware), then add 
the described `finally`-close.
   
   ### 4. Javadoc overstates what is registered (low)
   
   `SequentialChannelStateReader:38` — the `@param cancelables` javadoc says 
the registry is what "the spilling handler registers its spill files with"; it 
actually receives a single whole-directory delete hook, and only in the 
spilling modes. The parameter is also mandatory/non-null on the 
`NoSpillingHandler` path where it is entirely unused. Same wording appears in 
the two new inline comments in `SequentialChannelStateReaderImpl` and 
`StreamTask.fetchChannelState`.
   
   ### Checked and looks fine
   
   - No other implementors/callers of `readInputData` beyond `NO_OP`, 
`SequentialChannelStateReaderImpl`, `StreamTask`, and the two updated tests.
   - `ChannelStateFilteringHandler.createFromContext` returns `null` for zero 
gates, so the new test's comment about hitting `SpillingNoFilteringHandler` is 
accurate; `RecordFilterContext` arg order matches the constructor and 
`bufferSize` (10/20) satisfies `memorySegmentSize > 0`.
   - `assumeTrue(stateParLevel > 0 && parLevel > 0)` still leaves 3 of the 5 
parameter combinations running, so the new test isn't silently skipped.
   - Registration happens before `Files.createDirectories(baseDir)` and only 
when `currentStream == null`, so no fd leak if `registerCloseable` throws on an 
already-closed registry.
   


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