Siyao Meng created HDDS-16911:
---------------------------------
Summary: SCM hangs half stopped instead of exiting when a step of
the root CA rotation commit fails
Key: HDDS-16911
URL: https://issues.apache.org/jira/browse/HDDS-16911
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: CR-2-throw-failed-rotation-commit-to-state-machine.patch,
TestBugCR2ShutdownOnApplyThread.java
h3. Mechanism
{{RootCARotationHandlerImpl.rotationCommit}} is a replicated request, so it
runs in {{SCMStateMachine.applyTransaction}} on the Ratis
{{StateMachineUpdater}} thread. When one of its three steps fails (either
directory move, or the write of the new certificate ID to the VERSION file) it
calls {{StorageContainerManager.shutDown}}, which is {{stop()}} followed by
{{ExitUtils.terminate}}. {{stop()}} reaches {{RaftServerProxy.close}} through
{{SCMHAManagerImpl.stop}} and {{SCMRatisServerImpl.stop}}. The close hands the
close of the Raft division to its executor and then waits for that executor to
terminate ({{ConcurrentUtils.shutdownAndWait}}, with a timeout of one day). The
division close ({{RaftServerImpl.close}}, {{ServerState.close}},
{{StateMachineUpdater.stopAndJoin}}) joins the {{StateMachineUpdater}} thread,
which is the thread waiting for the executor. Neither can proceed, so the rest
of {{stop()}} and the {{ExitUtils.terminate}} call are not reached.
Every other fatal failure in {{applyTransaction}} (any exception that is not an
{{SCMException}}, and an {{SCMException}} with result {{INTERNAL_ERROR}} or
{{IO_EXCEPTION}}) reaches the catch block of {{SCMStateMachine}}, which calls
{{ExitUtils.terminate}} with status 1 and does not stop the SCM on that thread.
An {{SCMException}} with any other result is a logical rejection, it is
returned to the caller and the SCM keeps running. {{rotationCommit}} declares
{{IOException}} but does not use that path. Its three calls are the only
callers of {{shutDown}} under {{applyTransaction}}. The calls in
{{RootCARotationManager}} run on the executor of the manager and are not
affected. HDDS-10643 fixed a related wait for the normal stop, where
{{SCMStateMachine.close}} called {{shutDown}} on this thread.
h3. Trigger
# Secure SCM HA with {{hdds.x509.ca.rotation.enabled=true}} (default false).
# A root CA rotation reaches {{rotationCommit}} and one of its steps fails on
an SCM, for example because {{scm/sub-ca-previous}} is left over and not empty,
because {{scm/sub-ca-next}} is missing, or because of an I/O error on the
metadata volume.
Run: the first move fails because the current sub CA directory is missing. The
other two steps make the same call (code reading, exercised at unit level by
the test in the patch).
The same three calls are in the release tags ozone-1.4.0, 1.4.1, 2.0.0, 2.1.0,
2.1.1, 2.1.2, 2.2.0 and 2.2.1 (read with git grep, not run). Those releases use
Ratis 3.0.1 to 3.2.1, and the close path was read only in Ratis 3.3.1.
h3. Impact
* The SCM process does not exit. Run in a child JVM with {{System.exit}}
enabled and the shutdown hook of {{StorageContainerManagerStarter}}: the
process was still alive 200s after the failed commit and was then ended by the
harness. By code reading it stays until the one day timeout in Ratis expires,
after which the stop continues and the process exits with status 0.
* By then the datanode, block and client RPC servers, the HTTP server and the
RPC service of the Raft server are stopped, and the Raft division has given up
its role. The SCM serves no RPC or HTTP requests and is an unreachable peer for
the other SCMs, while it is still a member of the Raft group. It still has its
RocksDB open (run) and holds the Raft storage directory (code reading).
* A supervisor that restarts the SCM when the process exits does nothing, and a
check that only looks at the process sees a running SCM. The log has no
"Terminating with exit status" line. Among the last lines of the applying
thread are "Stopping SCM HA services." and the Ratis "close".
* SIGTERM ends the process (run, 0.5s), because the shutdown hook finds the SCM
already stopped.
* This only shows after {{rotationCommit}} has already failed on that SCM, so
it causes no outage by itself. It turns a failed SCM that should exit into one
that has to be found and killed by hand. If the step fails on every SCM, all of
them hang.
h3. Reproduction
PASS, unmodified source. [^TestBugCR2ShutdownOnApplyThread.java]
({{hadoop-hdds/server-scm}}, about 20s) starts one real SCM with its Ratis
server, registers the handler through its builder and calls {{rotationCommit}}
through Ratis. The SCM is not secure and has no certificate client, so the
handler sees the SCM through a delegate that supplies one. Every other call,
including {{shutDown}}, goes to the real SCM. The sub CA directory does not
exist, so the first move fails. The test then finds the {{StateMachineUpdater}}
thread in {{ConcurrentUtils.shutdownAndWait}} under {{RaftServerProxy.close}},
{{StorageContainerManager.shutDown}} and {{rotationCommit}}, and a thread of
the Raft server in {{Thread.join}} under {{StateMachineUpdater.stopAndJoin}}.
The SCM reports that it is stopped, {{ExitUtils.terminate}} was not called and
the SCM RocksDB is still open. A passing test means the defect is present. A
secure SCM was not run for this report.
h3. Patch
[^CR-2-throw-failed-rotation-commit-to-state-machine.patch], against
b9b33851b20fa11baf4b07982de9afb8ea44f85a. It also applies to master at
5108acff480. The patch contains only this change and is written against the
first commit alone, so other fixes proposed for {{rotationCommit}} will need a
manual merge with it.
The three failure branches of {{rotationCommit}} throw an {{IOException}} with
the same message instead of calling {{scm.shutDown}}.
{{SCMStateMachine.applyTransaction}} then terminates the SCM with exit status
1, as it does for any other fatal apply failure. The status is 1 instead of the
0 that {{shutDown}} would have used, which matches the other fatal apply
failures. Nothing changes for a commit that succeeds, for the replicated
requests or for what is persisted, and {{StorageContainerManager.shutDown}} and
its other callers are untouched.
Run in the same child JVM setup with the patch: "Terminating with exit status
1" is logged at once and the process exits with status 1 about 60s later. The
60s are the timeout of the shutdown hook ({{ozone.service.shutdown.timeout}}),
whose {{stop()}} waits in the same Ratis close while the applying thread is
inside {{System.exit}}. That delay exists today for every fatal apply failure
and is not changed by the patch.
Covered by {{testFailedCommitStepIsThrownToStateMachine}} in the existing
{{TestRootCARotationManager}}, which fails without the change ("Expecting code
to raise a throwable"). With it {{TestRootCARotationManager}},
{{TestSCMStateMachine}}, {{TestSCMHAManagerImpl}},
{{TestReplicationAnnotation}}, {{TestScmInvokerCodeGenerator}} and
{{TestScmCodecFactoryReplicateCoverage}} (35 tests) pass and checkstyle is
clean. With the patch the reproduction fails at its first assertion, because
the SCM reaches {{ExitUtils.terminate}}.
The patch makes the SCM exit, it does not make the commit succeed. What the
next start does depends on the step that failed (code reading, a restart was
not run). After a failed first move nothing on disk has changed and the log
entry is not recorded as applied, so it is applied again and fails again until
the cause is repaired. After a failed second move {{scm/sub-ca}} has already
been moved to {{scm/sub-ca-previous}}, and a secure SCM does not start until
the directory is put back, because {{DefaultCAServer.init}} rejects a
subordinate CA that has neither keys nor certificates. After a failed write of
the certificate ID the VERSION file still names the old certificate while
{{scm/sub-ca}} holds the new one, and that start was not followed. The same
holds without the patch once the hung process has been killed.
Found by code review of the SCM root CA rotation paths, as part of the TLA+
verification effort under HDDS-15926, on commit
b9b33851b20fa11baf4b07982de9afb8ea44f85a. 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]