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

Dmitry Werner updated IGNITE-29206:
-----------------------------------
    Issue Type: Bug  (was: Improvement)

> DirectMessageWriter retains oversized temporary serialization buffer and 
> reuses it after a failure
> --------------------------------------------------------------------------------------------------
>
>                 Key: IGNITE-29206
>                 URL: https://issues.apache.org/jira/browse/IGNITE-29206
>             Project: Ignite
>          Issue Type: Bug
>            Reporter: Dmitry Werner
>            Assignee: Dmitry Werner
>            Priority: Major
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> *Problem*
> DirectMessageWriter serializes @Compress fields and pending raw fields into a 
> reusable heap scratch buffer (tmpBuf) with a nested tmpWriter. The data is 
> then compressed or copied out of it. A writer is long-lived:
> - communication writers are stored in the NIO session meta and live as long 
> as the session;
> - the discovery ServerImpl.cliMsgSer lives as long as the node.
> This causes one problem and one robustness gap.
> 1. The oversized buffer is retained per writer.
> - The buffer starts at 10 KB and doubles until the whole field fits.
> - It is never shrunk.
> - The nested tmpWriter streams keep a reference to it too.
> So every writer ends up holding a buffer as large as the biggest field it has 
> ever serialized, roughly up to twice that field's raw size.
> The exchange coordinator is hit hardest. It sends 
> GridDhtPartitionsFullMessage to every node, and on large clusters compressed 
> fields such as locParts can reach tens of megabytes of raw data. After a PME, 
> each of the N communication sessions keeps such a buffer. On large clusters 
> this can add up to hundreds of megabytes of heap that is never released. 
> Other nodes are affected the same way by GridDhtPartitionsSingleMessage, and 
> discovery by DiscoveryDataPacket.
> 2. A half-written tmpWriter would be reused after a failure.
> If serialization into the temporary buffer throws, tmpWriter keeps stale 
> nested state: map/collection iterators, keyDone flags and per-item state. 
> reset() only resets the top state item, not the stream fields. A subsequent 
> compressed field written through the same writer would continue from that 
> stale state and produce corrupted output, and the buffer would stay pinned as 
> well.
> Current callers tear the writer down after such an error: NIO closes the 
> session, and discovery closes the session or stops the worker. So this is 
> hardening of the writer's own invariant, not a fix for a reproducible failure.
> The read side (DirectMessageReader) has neither issue. The decompressed 
> payload is allocated per message, releasePayload() resets the stream 
> references after reading, and tmpReader is dropped on failure.
> *Fix*
> - Once the temporary buffer has been used, release it if its capacity is 
> above 320 KB (a doubling step of the initial 10 KB capacity). "Used" means 
> deflated into the CompressedMessage chunks or copied into a RawField. For 
> compressed fields the release also happens if compression fails.
> - On release, reset the nested tmpWriter and all its state-item streams to a 
> shared empty buffer, so nothing keeps the large array reachable. This mirrors 
> DirectMessageReader#releasePayload().
> - If serialization into the temporary buffer fails, drop both tmpWriter and 
> tmpBuf, so a writer with half-written state is never reused. This mirrors how 
> DirectMessageReader drops tmpReader.
> - Buffers up to 320 KB are still reused, so small and medium fields keep 
> serializing with no per-message allocations.
> - An oversized field now grows a fresh buffer each time. That copying is 
> small compared to serializing and compressing such a field.
> - The wire format is unchanged.
> *Tests*
> - CompressedMessageTest#testLargeTemporaryBufferReleased writes a small 
> message, then a large GridDhtPartitionsFullMessage twice, through the same 
> writer. After each write it checks that neither the writer nor its tmpWriter 
> streams hold a buffer above the limit, and that the message reads back 
> correctly.
> - CompressedMessageTest#testWriterReusableAfterFailedCompressedSerialization 
> makes serialization of a compressed field fail in the middle of a nested map. 
> It checks that tmpWriter and tmpBuf are dropped and that the next message 
> written through the same writer reads back correctly.
> - Both new tests fail without the fix.
> - Existing serialization tests pass: CompressedMessageTest, 
> DirectMarshallingMessagesTest, RollingUpgradeMessageSerializationTest, 
> *MessageSerializationTest.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to