Siyao Meng created HDDS-17673:
---------------------------------

             Summary: WriteChunk appended after its container's CloseContainer 
commits with no data on the leader, so a lagging follower makes it close the 
pipeline
                 Key: HDDS-17673
                 URL: https://issues.apache.org/jira/browse/HDDS-17673
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: TestBugCR4LaggingFollowerReread.java

h3. Mechanism
{{ContainerStateMachine.startTransaction}} checks that the container is open, 
but the entry only gets its log index later, in 
{{RaftServerImpl.appendTransaction}}. {{CloseContainerCommandHandler}} marks 
the container CLOSING and submits a CloseContainer through Raft, and that entry 
can be appended between the check and the append of a WriteChunk. The 
WriteChunk then follows the CloseContainer in the log. If its data stage on the 
leader runs after the CloseContainer was applied, {{KeyValueHandler}} returns 
CLOSED_CONTAINER_IO, which {{ContainerStateMachine.handleCommandResult}} treats 
as success, so the entry commits although the leader wrote none of its bytes.

With {{hdds.datanode.wait.on.all.followers=false}} (the default) the leader 
drops the entry from {{stateMachineDataCache}} when it applies it. A follower 
that does not have the entry yet is then served by 
{{ContainerStateMachine.readStateMachineData}}, which reads the chunk range 
back from the leader's block file. That read fails ("Unexpected read size"), 
the leader marks its state machine unhealthy and the pipeline is closed. The 
follower never receives the entry.

CHUNK_FILE_INCONSISTENCY, the other result tolerated in the data stage, does 
not add a separate case: it marks the replica UNHEALTHY and the apply of the 
same WriteChunk then fails with CONTAINER_UNHEALTHY, which closes the pipeline 
anyway.

h3. Trigger
# A RATIS/THREE pipeline where one follower is behind (for example stopped or 
restarting) while the leader and the other follower commit.
# A client WriteChunk passes the leader's {{startTransaction}} check while the 
container is OPEN.
# Before that WriteChunk is appended, SCM's close command reaches the leader 
and the CloseContainer is appended.
# The leader runs the WriteChunk's data stage only after the CloseContainer has 
been committed and applied. If it runs while the container is still CLOSING, 
the bytes are written and nothing goes wrong.
# The lagging follower comes back and needs the WriteChunk.

The first window is short. The second needs the chunk writer thread on the 
leader to be backlogged for longer than the CloseContainer takes to commit and 
apply, which a busy disk can cause (argued, not measured). On top of that a 
follower has to be behind at the same time, so this is a rare race.

h3. Impact
* The leader's state machine goes unhealthy and the pipeline is closed. Writes 
to every other open container on the pipeline fail and clients have to continue 
on another pipeline.
* The lagging follower does not catch up through Raft. Its replica is closed 
with the pipeline and repaired by replication.
* No data is lost and no wrong data is served: the CloseContainer precedes the 
WriteChunk on every replica, so the WriteChunk and any later PutBlock for that 
container fail at apply on every replica.

h3. Reproduction
[^TestBugCR4LaggingFollowerReread.java] ({{hadoop-ozone/integration-test}}), 
unmodified source. One mini cluster with three datanodes, one follower stopped 
while the race happens and restarted afterwards. The only timing control is 
Ratis' {{CodeInjectionForTesting}} hook at 
{{RaftServerImpl.appendTransaction}}, which holds one WriteChunk after 
{{startTransaction}} while a real SCM close command is processed by the leader. 
A passing test means the defect is present; it passed in every run. Example 
output on the unfixed code (indexes, counts and the exact client error vary per 
run):
{noformat}
W1 result=CLOSED_CONTAINER_IO, leader block file length=1024, leader 
healthy=true, leader applied=10
ReadStateMachine failed. cmd ReadChunk logIndex 9 msg : Unexpected read size. 
expected: 1024, actual: -1 Container Result: CONTAINER_INTERNAL_ERROR
pipeline Action CLOSE on pipeline Pipeline-...Reason : Unexpected read size. 
expected: 1024, actual: -1
Pipeline Pipeline-... moved to CLOSED state
after follower restart: leader healthy=false, read failures=1, follower 
applied=3 of 10
write to the other open container on the same pipeline: 
java.util.concurrent.ExecutionException: 
org.apache.ratis.protocol.exceptions.RaftRetryFailureException: ...
{noformat}
The read failure is logged thousands of times per run because the leader keeps 
retrying the follower. The follower's applied index stays below the leader's 
(or reads as -1 when its division has not started yet), and the write to the 
other container fails with {{RaftRetryFailureException}} or a timeout, 
depending on the run.

h3. Suggested fix
Re-check the container state of a WriteChunk in 
{{ContainerStateMachine.preAppendTransaction}}, which Ratis calls on the leader 
under the log lock right before the entry gets its index, and reject it with a 
{{StateMachineException}} ({{leaderShouldStepDown=false}}) wrapping 
{{ContainerNotOpenException}} if the container is no longer open. Since the 
CloseContainer is submitted only after the container is marked CLOSING, a 
WriteChunk can then no longer follow it in the log, and the client sees the 
same error as for a rejection in {{startTransaction}}. With this change applied 
locally, the reproduction above fails at its first assertion: the held 
WriteChunk is rejected, the follower catches up and the pipeline stays open.

This depends on RATIS-2722, fixed in Ratis 3.4.0, which is not released yet. 
Ratis 3.3.1, used by Ozone today, does not release the leader's pending write 
quota for a request rejected in {{preAppendTransaction}}, so on 3.3.1 enough 
rejections in one leader term would leave the leader refusing all writes. The 
change should land together with or after the upgrade to Ratis 3.4.0.

Related: HDDS-1753 and HDDS-11955 hit the same re-read failure in 
{{readStateMachineData}} for a lagging follower, with a different cause (the 
block was deleted by a later entry).

Found by code review of the datanode container state machine apply and block 
commit paths, as part of the TLA+ verification effort under HDDS-15926, on 
commit ec5ed5f7ea0e4b406aee53aae4906616819cba2b. Checked against HDDS issues 
and apache/ozone pull requests for duplicates before filing. 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