[ 
https://issues.apache.org/jira/browse/FLINK-39519?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Rui Fan updated FLINK-39519:
----------------------------
    Description: 
In filtering mode, the pre-filter source buffer used by 
`RecoveredChannelStateHandler.getBuffer()` is now allocated from heap instead 
of the Network Buffer Pool, preventing deadlock between source and filtered 
buffers. Also adds a non-blocking `RecoveredInputChannel.requestBuffer()` used 
by OutputWriter, and removes the unbounded heap fallback from 
`requestBufferBlocking()` in filtering mode.

In filtering-mode channel state recovery, {*}at most one pre-filter (source) 
heap buffer is in flight per task at any moment{*}. Worst-case source-buffer 
memory = {{1 × memorySegmentSize}} (≈ 32 KB). No OOM risk, no semaphore or 
per-gate limit needed.

*Why it holds:*
 # {{ChannelStateChunkReader.readChunk()}} is a single-threaded while-loop: 
{{{}getBuffer → fill → recover(finally recycle){}}}. The next {{getBuffer()}} 
can only start after {{recover()}} returns. Structural concurrency = 1.

 # {{SpillingAdaptiveSpanningRecordDeserializer.getNextRecord()}} recycles 
{{currentBuffer}} the instant {{{}isBufferConsumed=true{}}}, which happens on 
both {{PARTIAL_RECORD}} (record spans buffers) and {{LAST_RECORD_FROM_BUFFER}} 
(buffer exhausted). {{filterAndRewrite()}} breaks its loop on the same 
condition.

 # Cross-buffer bytes are always *copied out* before recycle: 
{{SpanningWrapper.transferFrom}} / {{addNextChunkFromMemorySegment}} copy 
partial bytes into an internal {{byte[]}} (or into {{{}SpanningWrapper{}}}'s 
own spill file for records ≥ 5 MB). The only retained segment pointer 
({{{}leftOverData{}}} / {{{}NonSpanningWrapper.segment{}}}) is read via 
{{segment.get(...)}} — copies into the target — and only while 
{{{}isBufferConsumed=false{}}}.

 # Exception path converges through {{{}ChannelStateFilteringHandler.close() → 
VirtualChannel.clear() → deserializer.clear(){}}}, which recycles any buffer 
the deserializer still holds.

*Implication:* the heap-isolation change needs only {{allocateUnpooledSegment}} 
+ {{{}FreeingBufferRecycler.INSTANCE{}}}. Drop the semaphore, the 
{{MAX_HEAP_BUFFERS_PER_GATE}} constant, and the custom recycler. Add one test 
that feeds a record spanning multiple source buffers and asserts 
{{{}maxOutstanding == 1{}}}.

  was:In filtering mode, the pre-filter source buffer used by 
`RecoveredChannelStateHandler.getBuffer()` is now allocated from heap (bounded 
to 5 per gate) instead of the Network Buffer Pool, preventing deadlock between 
source and filtered buffers. Also adds a non-blocking 
`RecoveredInputChannel.requestBuffer()` used by OutputWriter, and removes the 
unbounded heap fallback from `requestBufferBlocking()` in filtering mode.


> Allocate pre-filter source buffers from heap in filtering mode
> --------------------------------------------------------------
>
>                 Key: FLINK-39519
>                 URL: https://issues.apache.org/jira/browse/FLINK-39519
>             Project: Flink
>          Issue Type: Sub-task
>          Components: Runtime / Checkpointing
>            Reporter: Rui Fan
>            Assignee: Rui Fan
>            Priority: Major
>
> In filtering mode, the pre-filter source buffer used by 
> `RecoveredChannelStateHandler.getBuffer()` is now allocated from heap instead 
> of the Network Buffer Pool, preventing deadlock between source and filtered 
> buffers. Also adds a non-blocking `RecoveredInputChannel.requestBuffer()` 
> used by OutputWriter, and removes the unbounded heap fallback from 
> `requestBufferBlocking()` in filtering mode.
> In filtering-mode channel state recovery, {*}at most one pre-filter (source) 
> heap buffer is in flight per task at any moment{*}. Worst-case source-buffer 
> memory = {{1 × memorySegmentSize}} (≈ 32 KB). No OOM risk, no semaphore or 
> per-gate limit needed.
> *Why it holds:*
>  # {{ChannelStateChunkReader.readChunk()}} is a single-threaded while-loop: 
> {{{}getBuffer → fill → recover(finally recycle){}}}. The next {{getBuffer()}} 
> can only start after {{recover()}} returns. Structural concurrency = 1.
>  # {{SpillingAdaptiveSpanningRecordDeserializer.getNextRecord()}} recycles 
> {{currentBuffer}} the instant {{{}isBufferConsumed=true{}}}, which happens on 
> both {{PARTIAL_RECORD}} (record spans buffers) and 
> {{LAST_RECORD_FROM_BUFFER}} (buffer exhausted). {{filterAndRewrite()}} breaks 
> its loop on the same condition.
>  # Cross-buffer bytes are always *copied out* before recycle: 
> {{SpanningWrapper.transferFrom}} / {{addNextChunkFromMemorySegment}} copy 
> partial bytes into an internal {{byte[]}} (or into {{{}SpanningWrapper{}}}'s 
> own spill file for records ≥ 5 MB). The only retained segment pointer 
> ({{{}leftOverData{}}} / {{{}NonSpanningWrapper.segment{}}}) is read via 
> {{segment.get(...)}} — copies into the target — and only while 
> {{{}isBufferConsumed=false{}}}.
>  # Exception path converges through {{{}ChannelStateFilteringHandler.close() 
> → VirtualChannel.clear() → deserializer.clear(){}}}, which recycles any 
> buffer the deserializer still holds.
> *Implication:* the heap-isolation change needs only 
> {{allocateUnpooledSegment}} + {{{}FreeingBufferRecycler.INSTANCE{}}}. Drop 
> the semaphore, the {{MAX_HEAP_BUFFERS_PER_GATE}} constant, and the custom 
> recycler. Add one test that feeds a record spanning multiple source buffers 
> and asserts {{{}maxOutstanding == 1{}}}.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to