[
https://issues.apache.org/jira/browse/HDDS-17675?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-17675:
------------------------------
Attachment: MC-2-bind-pending-write-future-to-entry-term.patch
> Datanode acknowledges and commits a WriteChunk without writing its data when
> a truncated entry at the same log index still has a pending write
> ----------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-17675
> URL: https://issues.apache.org/jira/browse/HDDS-17675
> 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, 2.2.2
> Reporter: Siyao Meng
> Priority: Major
> Attachments: MC-2-bind-pending-write-future-to-entry-term.patch,
> TestBugMC2StaleWriteFutureAfterTruncate.java
>
>
> h3. Mechanism
> {{ContainerStateMachine.writeStateMachineData}} keeps the pending data write
> of each WriteChunk in {{writeChunkFutureMap}}, keyed by log index only. When
> an entry for that index is already present, it returns the existing future
> without checking the term or the request (added by HDDS-12469 as a precaution
> for a retried write of the same entry). A map entry is removed only when its
> chunk write task finishes; {{truncate}} and {{notifyNotLeader}} only touch
> {{stateMachineDataCache}}.
> In Ratis 3.3.1, {{SegmentedRaftLog.appendImpl}} calls
> {{StateMachine.truncate}} and then {{StateMachine.write}} for each new entry
> while it builds the log tasks under the log write lock. Nothing waits for
> data writes that this datanode started for its own entries while it was
> leader, because those appends are not part of the follower append chain
> ({{RaftServerImpl.appendEntriesAsync}}). So if such an entry is truncated
> while its chunk write is still queued, and the new leader puts a different
> WriteChunk at the same index, {{write}} returns the future of the truncated
> entry:
> * the new chunk is never written;
> * the old task's completion completes the new entry's write and {{flush}}, so
> the datanode acknowledges the entry;
> * the COMMIT_DATA apply of the WriteChunk succeeds, because
> {{FilePerBlockStrategy.writeChunk}} does nothing in that stage;
> * a following PutBlock succeeds and the container BCSID moves past the
> missing chunk.
> h3. Trigger
> # Datanode A is leader of a Ratis pipeline. Its data writes for new
> WriteChunk entries are queued behind a slow volume (the condition HDDS-12469
> describes).
> # A loses leadership before those entries commit (for example a network
> partition), and another datanode becomes leader in a higher term without them.
> # A receives the new leader's entries. They truncate A's uncommitted entries,
> and a different WriteChunk lands on an index whose old data write is still
> pending. The old write must still be pending when the new leader's entries
> arrive (at least the time of an election) but younger than
> {{hdds.container.ratis.statemachine.write.wait.interval}} (default 10m),
> after which the next write marks the state machine unhealthy.
> # In the case that was run, the swallowed chunk is the last chunk of a block
> before its PutBlock(eof), and the block file already exists from an earlier
> chunk, so the file check of PutBlock(eof) passes. Other placements of the
> swallowed chunk were not run.
> Needs two faults at once (leadership loss and a slow volume on the old
> leader), so it is rare. No client action can cause it.
> The same reuse can happen for entries the datanode received as a follower,
> but only in the short window between the completion of the old write future
> and its removal from the map, because Ratis 3.3.1 chains follower appends
> (append entries compose, enabled by default). Ozone 2.0.0 ships Ratis 3.1.2,
> which does not chain them, so there this window is likely wider. That variant
> is argued from the code, not run.
> h3. Impact
> On the affected replica the container stays OPEN and healthy, its BCSID and
> the block metadata (GetBlock length) include the chunk, but the block file is
> short and ReadChunk of the committed chunk fails
> ({{CONTAINER_INTERNAL_ERROR}}, "Unexpected read size"). Nothing on the write
> path repairs it. A reader served by this replica gets an error and has to use
> another replica, and the container data scanner skips OPEN containers, so the
> replica counts as good until the container is closed and scanned. The
> replica's acknowledgement also counts towards the commit of a chunk it does
> not hold; with one other replica lagging, the write could be committed with a
> single good copy (argued from the Ratis code, not run).
> h3. Reproduction
> [^TestBugMC2StaleWriteFutureAfterTruncate.java]
> ({{hadoop-hdds/container-service}}) drives the real
> {{ContainerStateMachine}}, {{HddsDispatcher}}, {{KeyValueHandler}},
> {{FilePerBlockStrategy}}, an on disk container and the real Ratis
> {{SegmentedRaftLog}}, which issues the write, truncate and flush calls
> itself. {{RaftServerImpl}} and the other peers are not run; the test calls
> the log in the order {{RaftServerImpl}} does. The slow volume is one of the
> two chunk executors kept busy. Deterministic, unmodified source. On an
> unpatched build {{tailChunkOfNewLeaderIsCommittedWithoutItsData}} passes,
> which means the defect is present:
> {noformat}
> dataWrites(block@offset)=[100@0, 10@0, 10@4096, 2@0, 11@0]
> applyResults=[SUCCESS, SUCCESS, SUCCESS] containerState=OPEN containerBCSID=5
> lastIndex=5 block2Length=8192 block2FileLength=4096 readChunk(block 2, chunk
> 1)=CONTAINER_INTERNAL_ERROR readMatches=false
> {noformat}
> The data write of block 2 at offset 4096 never runs; the only write at that
> index is the truncated entry's chunk of block 11.
> {{controlWithoutSlowVolume}} runs the same sequence with the old writes
> finished before the truncation and reads the correct data.
> h3. Patch
> [^MC-2-bind-pending-write-future-to-entry-term.patch], against
> ec5ed5f7ea0e4b406aee53aae4906616819cba2b. It also applies to master at
> 7294fc2da18.
> {{WriteFutures}} records the term of its entry, and {{writeStateMachineData}}
> reuses a pending future only when the term matches, so a retried write of the
> same entry still gets the existing future (the HDDS-12469 intent), while a
> different entry at a truncated index starts its own write and replaces the
> map entry. The finishing task of the truncated entry then removes the map
> entry only if it is still its own, so that {{flush}} and the write timeout
> check keep tracking the new entry. {{truncate}} does not need to drop map
> entries: leftovers of truncated entries are no longer reused and leave the
> map when their task finishes, as today.
> Covered by {{testWriteAfterTruncateDoesNotReuseFutureOfTruncatedEntry}} in
> the existing {{ContainerStateMachineTests}} (run as leader and follower).
> Without the change it fails because the new WriteChunk is never dispatched;
> with only the term check it fails because {{flush}} no longer waits for the
> new write. With the patch {{TestContainerStateMachineLeader}},
> {{TestContainerStateMachineFollower}} and the mini cluster suites
> {{TestContainerStateMachine}}, {{TestContainerStateMachineFailures}},
> {{TestContainerStateMachineFlushDelay}} and {{TestCSMMetrics}} pass, and the
> reproduction fails at its first assertion because block 2 is written in full
> and reads back correctly.
> Found by TLA+ model checking and code review of the datanode container state
> machine apply and block commit paths under HDDS-15926, on commit
> ec5ed5f7ea0e4b406aee53aae4906616819cba2b. 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
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]