Siyao Meng created HDDS-17674:
---------------------------------
Summary: Datanode closes the Raft division on the Ratis thread
that called write(), so after a write timeout the close hangs and the replica
stays CLOSING
Key: HDDS-17674
URL: https://issues.apache.org/jira/browse/HDDS-17674
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
CR-12-close-raft-division-asynchronously-on-write-failure.patch,
TestBugCR12ClosedDivisionStaysActive.java
h3. Mechanism
HDDS-15443 added {{ContainerStateMachine.closeServer}}, which {{write()}} calls
when writing state machine data fails. It calls {{RaftServer.Division.close()}}
on the thread that completes the failed future. When {{write()}} fails
synchronously, that is the Ratis thread that called {{write()}}: on the write
timeout ({{validateLongRunningWrite}}), {{writeStateMachineData}} returns an
already failed future and {{whenComplete}} runs {{closeServer}} inline. On a
leader this is a {{clientExecutor}} thread (observed); on a follower it is a
{{serverExecutor}} thread ({{RaftServerImpl.appendLog}}, read only).
In Ratis 3.3.1 {{RaftServerImpl.close()}} ends with
{{ConcurrentUtils.shutdownAndWait(clientExecutor)}} and
{{shutdownAndWait(serverExecutor)}}, which wait up to {{TimeDuration.ONE_DAY}}
for the executor to terminate. The calling thread belongs to that executor, so
the close waits for itself for a day, with the division in CLOSING.
{{RaftServerImpl.groupRemove}} waits on {{closeFinishedLatch}} for that close,
so {{notifyGroupRemove}} does not run and
{{XceiverServerRatis.activePipelines}} keeps the group.
{{XceiverServerRatis.isExist}} therefore stays true.
h3. Trigger
# A WriteChunk on a Ratis pipeline stays pending longer than
{{hdds.container.ratis.statemachine.write.wait.interval}} (10 minutes by
default, for example a stalled disk) while another WriteChunk arrives.
# {{validateLongRunningWrite}} throws and {{closeServer}} runs inline on the
Ratis thread.
Other synchronous failures of {{writeStateMachineData}} take the same path
(read only). When the chunk write itself fails, {{closeServer}} usually runs on
a chunk executor thread, which {{close()}} does not wait for.
h3. Impact
What happens next depends on whether the in-flight WriteChunk, cancelled by the
timeout, fails or completes:
* If it fails, its log entry reaches {{notifyLogFailed}} and SCM closes the
pipeline and the container. On the datanode {{CloseContainerCommandHandler}}
sees {{isExist}} true and submits the close through the dead group, which fails
with {{ServerNotReadyException}} and later {{GroupMismatchException}}.
ClosePipeline blocks in {{groupRemove}}. The replica, and the container in SCM,
stay CLOSING. Observed: 19 failed close attempts in 90 seconds.
* If it completes, nothing tells SCM: the only failed entry is the timed out
one, and its log task is never created because its constructor is the frame
stuck in {{closeServer}}. Observed: no pipeline action, ClosePipeline or
CloseContainer in 90 seconds, {{isExist}} true.
* The datanode runs ClosePipeline commands on a single thread
({{DatanodeStateMachine}}), so while it waits in {{groupRemove}} every other
ClosePipeline on that datanode queues behind it (read from code, not run).
* The state clears when the one day wait in {{shutdownAndWait}} expires or the
datanode restarts (read from the Ratis source, not run).
Only master is affected; HDDS-15443 is not in a release.
h3. Reproduction
[^TestBugCR12ClosedDivisionStaysActive.java]
({{hadoop-ozone/integration-test}}) starts a MiniOzoneCluster with one datanode
and a RATIS/ONE pipeline, sets
{{hdds.container.ratis.statemachine.write.wait.interval}} to 1us as a stand in
for a stalled disk, writes one small key to create an OPEN container, then
writes a multi chunk key so that a second WriteChunk trips the timeout. It then
expects the replica to be QUASI_CLOSED and {{isExist}} to be false within 90
seconds. Public client writes and a real config key only, no source change.
Which branch a run takes is not under the test's control; most runs took the
first one.
Without the fix it failed in both runs with {{pipeline ... of the closed
division is still reported as existing}}; one run took each branch, and the
first branch run ended with {{isExist=true replica=CLOSING scmPipeline=REMOVED
scmContainer=CLOSING}}. With the fix it passed in every run that started (three
with the asynchronous close alone, one with the attached patch), ending with
{{isExist=false replica=QUASI_CLOSED scmPipeline=REMOVED
scmContainer=QUASI_CLOSED}}; all took the first branch.
To check the second branch with the patch I forced it in a scratch copy only
(the timeout cancels without interrupting, and each chunk write first waits 1.5
seconds without being interruptible, so the in-flight write completes). With
just the asynchronous close the reproduction ended {{isExist=true replica=OPEN
scmPipeline=OPEN scmContainer=OPEN}}: closing the division stops the Raft log
worker before it runs the failed entry's log task, so {{notifyLogFailed}} never
runs. With the attached patch it ended {{isExist=false replica=QUASI_CLOSED
scmPipeline=REMOVED scmContainer=QUASI_CLOSED}}.
h3. Suggested fix
The patch runs the close on another thread, which removes the self wait, and
reports the failure to SCM from there. Two alternatives were considered and not
used: removing the group from {{activePipelines}} in {{closeServer}} (or
checking the division in {{isExist}}) would make ClosePipeline skip
{{removeGroup}} and leave the hang in place; a quasi close fallback in
{{CloseContainerCommandHandler}} on {{ServerNotReadyException}} would also fire
while a healthy division is starting.
h3. Patch
[^CR-12-close-raft-division-asynchronously-on-write-failure.patch], against
ec5ed5f7ea0e4b406aee53aae4906616819cba2b. It also applies to master at
7294fc2da18.
{{closeServer}} still logs and counts the failure on the calling thread. It
then runs, with {{CompletableFuture.runAsync}},
{{XceiverServerRatis.handleNodeLogFailure}} (the pipeline action
{{notifyLogFailed}} would send; it is idempotent, so the normal log failure
path is unaffected) followed by {{getDivision(groupId).close()}}. This is the
same idiom {{ContainerStateMachine.notifyServerShutdown}} uses to leave the
Ratis thread before work that waits for the group to close. The close then
finishes in about 3 seconds (the log worker's own shutdown wait), ClosePipeline
removes the group, and {{notifyGroupRemove}} removes it from
{{activePipelines}} and quasi closes its containers. Writes that do not fail
are unaffected.
The close runs on the common pool, so a stall that closes many divisions at
once keeps several common pool threads busy for a few seconds each. No existing
executor fits: the chunk executors and the container op executor of
{{ContainerStateMachine}} can be stuck behind the stalled disk, and the latter
is shut down by this very close. A small dedicated executor is the alternative
if that is preferred. The patch drops {{e.addSuppressed(t)}} for a failed
close, because by then {{e}} has been handed to Ratis; the close failure is
still logged.
Covered by {{ContainerStateMachineTests#testWriteTimout}} (leader and
follower), extended to assert that {{close()}} does not run on the thread that
called {{write()}} and that {{handleNodeLogFailure}} is called for each failed
write. On the unmodified code it fails at the thread assertion; with only the
asynchronous close it fails at the {{handleNodeLogFailure}} check. With the
patch the two state machine suites, {{TestCloseContainerCommandHandler}} and
{{TestClosePipelineCommandHandler}} pass (40 tests),
{{TestClientRetryContainerStateMachineFailures}} passes (5 tests) and
checkstyle is clean.
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. 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]