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

Andy Huang reassigned HDDS-17370:
---------------------------------

    Assignee: Andy Huang

> MappedBufferManager hands out the cached mapped buffer itself, so a consumer 
> that changes its position and limit corrupts the cached entry
> ------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17370
>                 URL: https://issues.apache.org/jira/browse/HDDS-17370
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 2.0.0, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
>            Reporter: Siyao Meng
>            Assignee: Andy Huang
>            Priority: Major
>         Attachments: CR-1-duplicate-cached-mapped-buffer.patch
>
>
> h3. Mechanism
> {{MappedBufferManager.computeIfAbsent}} returns the cached {{ByteBuffer}} 
> object itself, both when it finds the range in its cache and when it has just 
> mapped and cached it. {{ChunkUtils.readData}} wraps that object into the 
> {{ChunkBuffer}} it returns, so every read of the same cached range holds the 
> same position and limit as the cache entry.
> {{ChunkBufferToByteString.toByteString}} and {{toByteStringList}} document 
> that the conversion function must preserve the position and limit of the 
> input buffer. {{ByteStringConversion.safeWrap}}, used when 
> {{ozone.UnsafeByteOperations.enabled=false}}, preserves them only when 
> nothing else uses the buffer: it copies with {{ByteString.copyFrom}}, which 
> moves the position to the limit, and then calls {{flip()}}. Because the 
> buffer is the cache entry, each conversion moves the entry's position and 
> limit. When two conversions of the same cached range interleave so that the 
> second copies after the first copied but before the first flipped, the second 
> copies 0 bytes, the first flip restores the full range and the second flip 
> sets the limit to 0. The cache entry is left at limit 0.
> {{ChunkUtils.readData}} then gets a buffer with 0 remaining for that range. 
> Its mapped read loop counts the read length by {{mapped.remaining()}}, so it 
> makes no progress, and each pass through the cache calls {{releaseQuota(1)}}.
> h3. Trigger
> All of these on one datanode:
> * Mapped reads are on ({{ozone.chunk.read.mapped.buffer.max.count}} > 0, 
> default 0).
> * {{ozone.UnsafeByteOperations.enabled=false}} (default true).
> * A read is served from the mapped path: it is longer than 
> {{ozone.chunk.read.mapped.buffer.threshold}} (32KB) and its buffer capacity 
> is above that threshold, and for gRPC reads 
> {{ozone.chunk.read.netty.ChunkedNioFile}} is false (the default).
> * Two conversions of the same cached range overlap in time.
> No fault is needed.
> h3. Impact
> * Reads of that range return an empty payload or one that does not match the 
> stored data, or fail with a runtime exception, for example 
> {{IllegalStateException}} from 
> {{ChunkBufferToByteString.applyAndAssertFunction}} ({{Unexpected limit: 
> expected = 65536 but computed = 0}}), {{BufferUnderflowException}} or 
> {{IndexOutOfBoundsException}}.
> * Later reads of that range on the mapped path do not complete.
> * The permit count of {{MappedBufferManager}} grows past the configured 
> {{ozone.chunk.read.mapped.buffer.max.count}} on each pass of such a read, so 
> the limit on mapped buffers no longer holds.
> * This lasts until the cached entry is replaced or the datanode restarts, and 
> the extra permits stay until the datanode restarts (argued, not run).
> h3. Reproduction
> No standalone reproduction is attached. The regression test in the patch 
> covers it: 
> {{TestMappedBufferManager.testComputeIfAbsentReturnsIndependentPositionAndLimit}}
>  ({{hadoop-hdds/container-service}}) moves the position of the buffer 
> returned for a new mapping and checks that the next cache hit still covers 
> the full range, then sets the limit of that hit to 0 and checks that a third 
> call still gets position 0 and the full limit, and that the cached buffer 
> itself is unchanged. Its supplier fails the test if the cache is not reused. 
> On unmodified source it fails with {{expected: 1024 but was: 0}} at the first 
> check. It also fails if only one of the two paths is changed.
> h3. Patch
> [^CR-1-duplicate-cached-mapped-buffer.patch], against 
> 5fed99dab3c3204cd7bf1516074969a38d5eb00d. It also applies to master at 
> 76b5c6947a2.
> {{MappedBufferManager.computeIfAbsent}} returns {{duplicate()}} of the buffer 
> on both paths, so each caller has its own position and limit and the cached 
> buffer itself never leaves the manager. This covers every consumer of the 
> returned buffers, not only {{safeWrap}}: any code that reads a returned 
> buffer moves only its own copy. Content, read only mode, byte order and class 
> ({{MappedByteBuffer}}) are unchanged. GC behaviour is unchanged as well: a 
> duplicate of a mapped buffer stores the original in its attachment 
> ({{DirectByteBuffer.att}} in the JDK), so the cache entry stays reachable 
> while any duplicate is in use and can be cleared once all are dropped, as 
> with the object returned today. The cost is one buffer object per mapped 
> piece per read. A progress check in the {{readData}} loop would be a 
> reasonable separate hardening; with this change a returned buffer always 
> starts at position 0 with the mapped size as its limit, so the loop always 
> advances.
> With the patch {{TestMappedBufferManager}}, {{TestChunkUtils}}, 
> {{TestFilePerBlockStrategy}}, {{TestFilePerChunkStrategy}}, 
> {{TestKeyValueHandler}}, {{TestKeyValueHandlerWithUnhealthyContainer}}, 
> {{TestChunkManagerDummyImpl}}, {{TestBlockManagerImpl}} and 
> {{TestKeyValueContainerCheck}} pass (283 tests, 20 skipped in 
> {{TestBlockManagerImpl}}), and checkstyle on 
> {{hadoop-hdds/container-service}} is clean.
> Affected releases: 2.0.0 to 2.2.1, since HDDS-10488 introduced the cache.
> One coordination note: the patches proposed for two related 
> {{MappedBufferManager}} issues filed with this one (HDDS-17368, the permit of 
> a range that is mapped again after GC cleared its entry, and HDDS-17367, the 
> mapping map shared by all managers) also change {{TestMappedBufferManager}}. 
> This test change conflicts with both in the static imports and with the first 
> of them at the end of the class. The product change applies on top of either. 
> Whichever lands later needs a trivial rebase.
> Found by code review of the datanode memory mapped chunk read path, as part 
> of the TLA+ verification effort under HDDS-15926, on commit 
> 5fed99dab3c3204cd7bf1516074969a38d5eb00d. Checked against HDDS issues and 
> apache/ozone PRs for duplicates before filing. Related but not duplicates: 
> HDDS-10488 (introduced the cache), HDDS-15357 (WeakReference races in the 
> same method). The attached patch is a proposal for review. Generated with 
> Specula (Claude Opus 5.5).



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

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to