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

Siyao Meng updated HDDS-17003:
------------------------------
    Attachment: CR-3-wait-for-ec-cell-reads-on-interrupt.patch

> Interrupted EC reconstruction read returns while its cell reads still write 
> into stripe buffers that close() gives back to the buffer pool
> ------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17003
>                 URL: https://issues.apache.org/jira/browse/HDDS-17003
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: CR-3-wait-for-ec-cell-reads-on-interrupt.patch
>
>
> h3. Mechanism
> {{ECBlockReconstructedStripeInputStream.loadDataBuffersFromStream}} submits 
> one cell read per selected index to the EC reconstruct executor and then 
> waits for the futures in turn. When a wait is interrupted it catches the 
> {{InterruptedException}}, sets the interrupt flag again and moves to the next 
> future. With the flag set, {{Future.get()}} on every read that has not 
> finished throws at once, so the loop ends and the method throws while those 
> reads are still running. The comment at that catch says the intent is to 
> handle all futures before throwing; setting the flag inside the loop defeats 
> it. The code dates from HDDS-5952 and is the same in every release from 1.3.0 
> to 2.2.1.
> The reads that are left running keep writing into the buffers they were 
> given: the caller's stripe buffers ({{ECBlockReconstructedInputStream}} takes 
> them from the client's {{ByteBufferPool}}) and the parity buffers the stripe 
> reader takes from the same pool. {{ECBlockReconstructedInputStream.close()}} 
> and {{unbuffer()}} put the stripe buffers back into the pool, and the stripe 
> reader's {{close()}} and {{unbuffer()}} put back its parity buffers, without 
> waiting for those reads. A read that has not yet opened its replica stream 
> opens it through {{ECBlockInputStream.getOrOpenStream}} after the close, and 
> that stream is not closed again (argued from code, not run).
> {{BoundedElasticByteBufferPool.getBuffer}} hands out the same {{ByteBuffer}} 
> object. The client has one pool, used by every EC reconstruction stream and 
> by {{ECKeyOutputStream}}. A late cell read therefore writes into a buffer 
> that is already in use by another stream of the same client, and it also 
> moves that buffer's position.
> h3. Trigger
> # A key in an EC bucket is read through the reconstruction reader (a data 
> replica is missing or has failed).
> # The reading thread is interrupted while it waits for the cell reads of a 
> stripe.
> # The stream is closed or unbuffered before those reads have finished. 
> {{ECBlockInputStreamProxy.readPositioned}} closes its per call reader in a 
> try with resources block, so an interrupted positioned read takes this path 
> by itself.
> h3. Impact
> * The late cell read overwrites a buffer that the pool has handed to another 
> stream. If that stream is a reconstruction read that has just cleared the 
> buffer, its own read of that cell finds no space left and the stripe is 
> returned with the wrong cell, and a decoded cell computed from it is wrong 
> too, without an error.
> * Not run: the same applies when the buffer is taken by 
> {{ECKeyOutputStream}}, which would then encode and write a cell with wrong 
> content. The datanode offline reconstruction 
> ({{ECReconstructionCoordinator}}) uses the same method with its own pool; 
> there an interrupt comes only from a shutdown.
> * Argued, not run: a read started after the interrupt on the same stream 
> submits new cell reads for the same indexes and buffers while the old ones 
> may still run.
> h3. Reproduction
> No standalone reproduction is attached. The regression tests 
> {{testInterruptedReadWaitsForCellReadRunningOnPoolThread}} and 
> {{testInterruptedReadRunsCellReadsNotStartedByExecutor}} in the attached 
> patch fail on unmodified source (see Patch).
> h3. Patch
> [^CR-3-wait-for-ec-cell-reads-on-interrupt.patch], against 
> 1fb64c8e499161d1452ae6948ffd461fc9d2c302. It also applies to master at 
> fd2f6092c6d.
> {{loadDataBuffersFromStream}} now creates a {{FutureTask}} per cell read and 
> hands it to the executor with {{execute}}. It then calls {{run()}} on each 
> task in the calling thread and waits for it with 
> {{Uninterruptibles.getUninterruptibly}} (Guava, already a dependency of the 
> module) instead of {{Future.get()}}, and the catch that set the flag again is 
> removed. {{FutureTask.run}} does nothing once a pool thread has started the 
> task, so the calling thread only runs a read the executor has not started: 
> one a pool thread has been handed but has not started yet (the calling thread 
> then does that read itself and the pool thread skips it, so the reads of a 
> stripe still run in parallel), one that {{CallerRunsPolicy}} dropped because 
> the executor was shut down, or one the executor rejected. The calling thread 
> runs such a read with its interrupt status cleared and restored afterwards, 
> as a pool thread would, so that the datanode read does not fail because of 
> the interrupt and mark a healthy replica as failed. Every read that is waited 
> for therefore runs or has run, and the wait always ends. 
> {{getUninterruptibly}} restores the interrupt status when it returns, so the 
> existing check after the loop still throws {{InterruptedException}} and 
> {{read}} still throws an {{IOException}} with the flag set, but only after 
> every read of the stripe has completed or failed. No read writes into the 
> stripe or parity buffers after the method returns, so {{close()}} and 
> {{unbuffer()}} put back only idle buffers. Read failures seen while waiting 
> are recorded as before. The same approach is used for the healthy reader in 
> the open pull request for HDDS-16867.
> Behaviour change: an interrupted reconstruction read returns only after the 
> cell reads of the stripe that are in flight have completed or failed, which 
> takes as long as an uninterrupted read of that stripe would (each datanode 
> call has the {{ozone.client.read.timeout}} deadline, 30s by default, and a 
> cell read can make several calls, including read retries and a switch to a 
> spare replica), instead of at once. A read whose executor has been shut down 
> no longer waits for a cell read that will never run: on unmodified source it 
> waits until the thread is interrupted, with the patch the calling thread runs 
> that cell read. Cancelling the futures was not used: {{cancel(true)}} would 
> make the datanode reads fail and count as failed locations, and a cancelled 
> future no longer tells when its read has stopped writing.
> Covered by three tests in the existing 
> {{TestECBlockReconstructedInputStream}}. In the first two, index 3 has no 
> location, so a stripe is read by two data reads and one parity read, and 
> {{read}} is called with the interrupt flag set.
> * {{testInterruptedReadWaitsForCellReadRunningOnPoolThread}} lets the 
> executor start the first cell read on a pool thread and holds that read until 
> the reading thread waits for it. It asserts an {{IOException}} caused by 
> {{InterruptedException}}, the flag still set, and all three cell reads, 
> including the held one, done when {{read}} throws. On unmodified source it 
> fails because {{read}} returns while the held read is not done.
> * {{testInterruptedReadRunsCellReadsNotStartedByExecutor}} uses an executor 
> that keeps the cell reads without running them, and replica streams whose 
> read fails when its thread is interrupted, as a datanode read does. It 
> asserts the same, plus that no replica was marked as failed, then takes the 
> buffers back from the pool after {{close()}}, clears them, runs any read left 
> over and asserts the buffers are untouched. On unmodified source it fails 
> because {{read}} returns with the three reads not done; run after 
> {{close()}}, those reads would write into three of the four buffers taken 
> back from the pool, which the rest of the test checks.
> * {{testInterruptedReadReturnsWhenExecutorIsShutDown}} reads with the 
> interrupt flag set through a {{SynchronousQueue}} executor with 
> {{CallerRunsPolicy}} that has been shut down, and asserts that the read 
> throws within 10 seconds. It passes on unmodified source, where the interrupt 
> ends the wait, and fails if the wait cannot end.
> With the change the EC client stream suites (78 tests), {{TestOzoneECClient}} 
> and {{TestKeyInputStreamEC}} (36 tests) pass, and checkstyle and PMD are 
> clean.
> The patch and the patch for HDDS-17001 both add static imports at the top of 
> {{TestECBlockReconstructedInputStream}}, so whichever lands second needs a 
> rebase of that import block; the main code changes do not overlap.
> Found by code review of the Ozone client read retry and failover paths, as 
> part of the TLA+ verification effort under HDDS-15926, on commit 
> 1fb64c8e499161d1452ae6948ffd461fc9d2c302. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. The attached patch 
> is a proposal for review. Generated with Specula (Claude Opus 5.5).



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

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

Reply via email to