Siyao Meng created HDDS-17001:
---------------------------------
Summary: EC reconstruction read returns unfilled stripe buffers
when the stream is read again after a failed read or seek
Key: HDDS-17001
URL: https://issues.apache.org/jira/browse/HDDS-17001
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-3-invalidate-ec-reconstruction-buffers-on-failed-stripe-read.patch,
TestBugMC3StaleReconstructionBuffers.java
h3. Mechanism
{{ECBlockReconstructedInputStream.readStripe}} calls {{clearBuffers()}}, which
sets every stripe buffer to position 0 and limit of one cell, and then
{{stripeReader.readStripe(bufs)}}. If the stripe read throws (for example
{{InsufficientLocationsException}} once too many replicas have failed), the
buffers stay readable. The next {{read()}} on the same stream finds bytes
remaining in {{selectNextBuffer}}, copies the buffers to the caller without any
I/O and advances the position, without an error.
{{seek()}} has the same problem: it moves the stripe reader to the target
stripe and loads that stripe before it updates {{position}}. If loading fails,
the buffers hold part of the target stripe while {{position}} is still the old
one, and the next {{read()}} returns that data as if it were at the old
position.
h3. Trigger
# A block is read by the reconstruction reader, and a stripe read or a
{{seek()}} fails because not enough replicas can be read.
# The caller catches the {{IOException}} and reads the same stream again.
No in-tree caller does step 2: {{KeyInputStream}}, {{OzoneFSInputStream}}, the
S3 gateway GET and {{ozone sh key get}} pass the exception up, and positioned
reads use a fresh reader per call. An application that retries a read on the
same {{FSDataInputStream}} or {{OzoneInputStream}} after an exception is
exposed.
h3. Impact
The repeated read returns up to one stripe from the buffers, in which the bytes
that were not loaded before the failure (about one cell in the reproduction)
are not the block data, and the position moves past them. After a failed
{{seek()}} the returned data belongs to another stripe. A stream whose replicas
are exhausted should throw again instead.
h3. Reproduction
PASS on unmodified source, deterministic.
[^TestBugMC3StaleReconstructionBuffers.java] ({{hadoop-hdds/client}}) uses
rs-3-2 with 1 MiB cells, the stock {{ECBlockInputStreamFactoryImpl}} and the
client test replica streams for datanode read errors. Three cases: failover
from the plain reader at position 0 followed by exhausted replicas, a failed
{{seek()}} into stripe 2 after stripe 0 was read, and a failed read through
{{MultipartInputStream}} (the base class of {{KeyInputStream}}). In each the
caller catches {{InsufficientLocationsException}} and reads again, gets 3 MiB
without an exception, and about 1 MiB (3 MiB after the failed seek) differs
from the block data. A passing test means the defect is present.
h3. Patch
[^MC-3-invalidate-ec-reconstruction-buffers-on-failed-stripe-read.patch],
against 1fb64c8e499161d1452ae6948ffd461fc9d2c302. It also applies to master at
8664dc4fc3c.
When the stripe read fails, with an {{IOException}} or a {{RuntimeException}},
{{readStripe}} sets the limit of every stripe buffer to 0 and sets the existing
{{unBuffered}} flag before rethrowing, so nothing from the failed stripe is
returned and the next {{read()}} first seeks the stripe reader back to the
current position, as it does after {{unbuffer()}}. With exhausted replicas the
next read throws again. After a failed {{seek()}} the stream stays at its old
position.
Covered by two tests in the existing {{TestECBlockReconstructedInputStream}}:
{{testReadFailsAgainAfterFailedStripeRead}} (exhausted replicas; read, read,
seek and read all throw, position stays 0) and
{{testFailedSeekLeavesStreamAtOldPosition}} (the stripe read of a {{seek()}}
fails once with an injected exception after the stripe reader has moved to the
target stripe; the next read must return the data at the old position). Both
fail without the change. The second also fails if only the limits are reset
without the re-seek, and if only {{IOException}} is handled. With the change
the EC client stream suites (75 tests) and {{TestOzoneECClient}} plus
{{TestKeyInputStreamEC}} (36 tests) pass and checkstyle is clean. With the
patch all three reproduction cases fail because the second read throws
{{InsufficientLocationsException}}.
Found by TLA+ model checking and code review of the Ozone client read retry and
failover paths 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]