[FLINK-40520][checkpointing] Release spilled channel state on recovery abort between fetch and drain - #29065
Conversation
31cd9d0 to
43cde36
Compare
| published = true; | ||
| return produced; | ||
| } finally { | ||
| // On abort no drainer is built to release the produced state; close it here (quietly, | ||
| // so it doesn't mask the original failure) to avoid orphaning its spill files. | ||
| if (!published) { | ||
| IOUtils.closeQuietly(stateHandler.getProducedChannelState()); | ||
| } |
There was a problem hiding this comment.
We might as well pass CloseableRegistry cancelables here instead (and skip registering state in the caller), right?
Not sure if interface change worth it though.
There was a problem hiding this comment.
Actually, is this (closeQuietly) here enough? What if an exception happens before stateHandler assigns a value to producedChannelState?
I think a proper fix would be to either:
- Expose some cleanupOnFailure method on
stateHandlerthat'd clean up any temporary files - Pass
CloseableRegistrytostateHandlerand use it there for all the resources (files/state)
(1) seems more explicit to me.
WDYT?
There was a problem hiding this comment.
Done — the spilling handler now takes the CloseableRegistry and registers its spill files itself, covering both.
9854e2d to
9796c4c
Compare
| private void deleteSpillFiles() throws IOException { | ||
| IOException firstError = null; | ||
| for (Path file : files) { |
There was a problem hiding this comment.
Can we have race conditions here? 🤔
I.e. a task in recovery is being cancelled; files is not updated yet by the recovery; or the updates are not visible to the closing thread?
Maybe just delete the directory instead (if it's exclusive)? Or can this cause some failure of the recovering thread?
There was a problem hiding this comment.
Good catch — switched to deleteDirectoryQuietly(best-effort/quiet)
9796c4c to
214b5e3
Compare
|
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
|
Spilled channel-state cleanup: ownership modelFetch and drain run on
Note: |
d65cd8f to
dd6ab14
Compare
| } catch (Throwable t) { | ||
| // The state was not handed off, so no drainer will release its spill files: delete | ||
| // them here (quietly, so the original failure is not masked). | ||
| IOUtils.closeQuietly(stateHandler.getProducedChannelState()); |
There was a problem hiding this comment.
I think we are back to this issue now:
the failure might happen before the AbstractSpillingHandler.producedChannelState is assigned.
There was a problem hiding this comment.
Right, this brought the original issue back.
Fixed with your option (1) from that thread: AbstractSpillingHandler#discardSpilledFiles() deletes the whole spill directory, and the catch in readInputData calls it, so it no longer depends on producedChannelState having been assigned.
I originally wanted to avoid adding a method on the handler for this, but after the detour it turned out to be the simplest fix anyway.
…y abort between fetch and drain Spill files produced by readInputData were only deleted once drain() ran, so any abort between fetch and drain leaked them until TaskManager shutdown. - readInputData deletes the handler's spill directory if it fails before handing the state off. This does not depend on the produced FetchedChannelState having been built, since stateHandler.close() itself may be what failed. - fetchChannelState registers the fetched state with the task's resourceCloser, so cleanUp() deletes the spill files whenever recovery aborts afterwards (drainer never built, mailbox mail rejected or dropped, drain() never scheduled). close() is idempotent, so a completed drain makes this a no-op; a fetch that finishes after cleanUp() is closed by the registry on the spot.
dd6ab14 to
64ffa4d
Compare
1996fanrui
left a comment
There was a problem hiding this comment.
Thanks for the review and suggestion, merging
What is the purpose of the change
Spilled channel-state files produced during checkpointing during recovery were released only when
drain()ran. Several fetch→drain abort paths never reach
drain(readInputDatathrows after a file was spilled; therequestPartitionsor trigger-install mail is rejected or dropped;thenRunAsync(drain)is rejected after thechannelIOExecutorshut down), so the producedFetchedChannelStatewas never closed and its spill filessurvived until TaskManager shutdown, accumulating across recovery-failure loops on a long-lived pooled TM.
The fix deletes the spill files at the two places that can still own them on an abort:
readInputDatawhenthe fetch fails before handing the state off (it deletes the handler's whole spill directory, so this holds
even when
stateHandler.close()itself failed and noFetchedChannelStatewas built), and the task'sresourceCloser(cleanUp()) for everything after the hand-off. Hooking into task cleanup rather than the recovery future chain matters because a maildropped on mailbox close never completes its future, so no future callback would ever fire for it.
Brief change log
readInputDatadeletes the spilling handler's spill directory if it fails before handing the state off; this does not depend on the producedFetchedChannelStatehaving been built, sincestateHandler.close()itself may be what failed;fetchChannelStateregisters the fetched state with the task'sresourceCloser, socleanUp()deletes the spill files whenever recovery aborts afterwards (drainer never built, mailbox mail rejected or dropped,drain()never scheduled).FetchedChannelState.close()is idempotent, so a completed drain makes this a no-op; a fetch that finishes aftercleanUp()is closed by the registry on the spot.Verifying this change
This change added a test:
SequentialChannelStateReaderImplTestasserts the spill files and their directory are deleted whenreadInputDataaborts after spilling at least one file (fails without the fix).Does this pull request potentially affect one of the following parts:
@Public(Evolving): noDocumentation
Was generative AI tooling used to co-author this PR?