Siyao Meng created HDDS-17004:
---------------------------------

             Summary: Streaming block read does not try another replica after a 
client side checksum mismatch
                 Key: HDDS-17004
                 URL: https://issues.apache.org/jira/browse/HDDS-17004
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: CR-1-fail-over-streaming-read-on-checksum-mismatch.patch, 
TestBugCR1StreamReadChecksumNoFailover.java

h3. Mechanism
With {{ozone.client.verify.checksum=true}} (default) 
{{StreamBlockInputStream.StreamingReader.onNext}} verifies each ReadBlock 
response and fails the stream with {{OzoneChecksumException}} on a mismatch. 
The reader surfaces it as {{IOException("Streaming read failed")}} wrapping an 
{{ExecutionException}}. {{ConnectionFailureUtils.unwrapCause}} only stops at 
{{StorageContainerException}} and {{TimeoutIOException}}, and 
{{StreamBlockInputStream.handleExceptions}} (sequential reads) and 
{{handlePreadException}} (positioned reads) retry only those and connectivity 
errors. So the checksum error is rethrown and no other replica is tried. On the 
sequential path the failed reader is also kept, so later sequential reads of 
other ranges on the same stream fail with the same error until the stream is 
unbuffered or reopened.

The non streaming path validates checksums inside the per datanode loop of 
{{XceiverClientGrpc.sendCommandWithRetry}} and moves on to the next replica for 
that request. Datanodes do not verify checksums for client reads by default, so 
silent corruption on one replica reaches the client as successful responses 
with bad bytes.

h3. Trigger
# {{ozone.client.stream.readblock.enable=true}}, a RATIS/THREE key.
# Silent corruption of the block data on one replica (one byte flipped in 
place).
# Read the key with a client whose stream starts on that replica.

Which replica a stream starts on follows the order OM returns 
({{ozone.network.topology.aware.read=true}} by default). Replicas at the same 
distance are shuffled per lookup, so a client hits the corrupt replica first on 
every read only when it is strictly the closest one (for example on the same 
host), and otherwise on a share of the reads.

h3. Impact
* A streaming read that starts on the corrupt replica fails, although two 
healthy replicas exist. The non streaming read of the same key succeeds.
* No wrong data is returned.
* Only with streaming block reads enabled (off by default).
* On the current code the failure appears as a false EOF ({{Inconsistent read 
... numBytesRead=-1}}) instead of the checksum error, because of HDDS-16680: 
the error arrives while {{StreamingReader.poll}} waits and is turned into end 
of stream before the retry decision. The fix for this issue depends on 
HDDS-16680 (apache/ozone PR #11388): only with both does the read succeed.

h3. Reproduction
PASS, deterministic in a mini cluster, unmodified source. 
[^TestBugCR1StreamReadChecksumNoFailover.java] 
({{hadoop-ozone/integration-test}}) writes a 32 KiB RATIS/THREE key on three 
datanodes, flips one byte of the block file on the first pipeline node, then 
reads the key 5 times without and 5 times with streaming block reads. All 
datanodes have the client's address there, so topology aware read puts the 
corrupted node first for every read. A passing test means the defect is 
present. Results:
||Code||Non streaming||Streaming||Streaming failure||
|unmodified|5/5|0/5|false EOF|
|with the attached patch|5/5|0/5|false EOF (HDDS-16680)|
|with PR #11388 (HDDS-16680)|5/5|0/5|{{Streaming read failed}} caused by 
{{OzoneChecksumException}}|
|with both|5/5|5/5|none, each read skips the corrupt replica once|

The short circuit read path appears to have the same gap ({{BlockInputStream}} 
rereads the same local file after a checksum error until its retries are used 
up); this is from code reading only, was not run and is not covered by the 
attached patch.

h3. Patch
[^CR-1-fail-over-streaming-read-on-checksum-mismatch.patch], against 
1fb64c8e499161d1452ae6948ffd461fc9d2c302. It also applies to master at 
fd2f6092c6d, and together with PR #11388.

{{ConnectionFailureUtils.unwrapCause}} (used only by 
{{StreamBlockInputStream}}) also returns an {{OzoneChecksumException}} found in 
the cause chain. On such an error the sequential path ({{handleExceptions}}) 
and the positioned path ({{readPositioned}}) release the failed reader and skip 
that replica, then open a stream on the next replica, as the non streaming path 
does per request. The skip lasts for one positioned read, or for one refill of 
the sequential read buffer (the set is cleared when the buffer is refilled). 
This cannot loop: within that scope every checksum failure either adds a 
replica not tried yet or rethrows, and a refill that succeeds returns data that 
the read then consumes. A checksum mismatch is a fault of the data in one 
range, not of the datanode, so the replica is not added to the stream's 
{{failedStreamingDatanodes}}: reads of other ranges, and later reads of the 
same range, start again from the first replica. There is no retry sleep, no 
block location refresh and no use of the read retry limit 
({{ozone.client.max.read.retries}}) for this case, on either path; it is 
bounded by the number of replicas. When every replica fails the check, the read 
throws {{IOException("Streaming read failed")}} with the 
{{OzoneChecksumException}} of the last replica tried as its root cause (through 
an {{ExecutionException}}), the same exception as today, and later reads of 
other ranges still work.

Covered in the existing unit {{TestStreamBlockInputStream}} with three mocked 
replicas, each case for sequential and positioned reads: 
{{testChecksumMismatchFailsOverToAnotherReplica}} (first replica corrupt, the 
read returns the data), 
{{testChecksumMismatchOnAllReplicasSurfacesChecksumError}} (all replicas 
corrupt, {{OzoneChecksumException}} is the root cause), 
{{testChecksumMismatchInOneRangeDoesNotAffectOtherRanges}} (offset 0 corrupt on 
all replicas, a read of offset 2 on the same stream returns the data) and 
{{testChecksumSkipDoesNotUseReadRetry}} (first replica corrupt, second answers 
CONTAINER_NOT_FOUND, one read retry allowed, the read returns the data from the 
third). The responses arrive synchronously, so the tests do not depend on 
HDDS-16680. Without the change the first fails with {{IOException: Streaming 
read failed}} caused by {{OzoneChecksumException}}, and the third fails on its 
sequential half because the failed reader is kept. With it 
{{TestStreamBlockInputStream}} (33 tests), {{TestConnectionFailureUtils}} (21), 
{{TestBlockInputStream}} (17), {{TestChunkInputStream}} (10), 
{{TestMultipartInputStream}} (6), and in a mini cluster 
{{TestStreamReadDatanodeFailover}} (3), {{TestStreamRead}} (4) and the 
integration {{TestStreamBlockInputStream}} (3) pass, checkstyle is clean.

Related: HDDS-16873 proposes reporting replicas that fail client side checksum 
verification; it assumes the client already moves to another replica.

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