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

ASF GitHub Bot updated FLINK-39519:
-----------------------------------
    Labels: pull-request-available  (was: )

> 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
>              Labels: pull-request-available
>
> In filtering mode, the pre-filter source buffer used by 
> {{RecoveredChannelStateHandler.getBuffer()}} is now allocated from heap 
> instead of the Network Buffer Pool. This eliminates the deadlock between 
> pre-filter and post-filter buffers competing for the same pool, which used to 
> be masked by an unbounded heap fallback in {{{}requestBufferBlocking(){}}}. 
> Memory is bounded to a single segment per task (see invariant below), so no 
> semaphore or per-gate limit is needed. The {{requestBufferBlocking()}} 
> fallback is kept unchanged in this commit and will be removed in a follow-up, 
> once FLINK-38544's OutputWriter provides the disk-spilling replacement for 
> the post-filter path.
> h2. Source Buffer One-At-A-Time Invariant
> 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 implementation is minimal — {{allocateUnpooledSegment}} + 
> {{{}FreeingBufferRecycler.INSTANCE{}}}, no semaphore, no per-gate limit, no 
> custom recycler. A test feeds a record spanning multiple source buffers and 
> asserts {{maxOutstanding == 1}} to guard the invariant against regression.



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

Reply via email to