Siyao Meng created HDDS-17090:
---------------------------------

             Summary: Follower SCM that has not applied finalization yet 
rejects a datanode the leader already finalized, and the datanode shuts down
                 Key: HDDS-17090
                 URL: https://issues.apache.org/jira/browse/HDDS-17090
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-4-let-datanode-retry-when-non-leader-scm-rejects-registration.patch, 
TestBugMC4LaggingFollowerFencesFinalizedDatanode.java

h3. Mechanism
Only the leader SCM sends {{FinalizeVersionCommand}} to datanodes, as soon as 
it has applied FINALIZE itself 
({{SCMNodeManager.sendFinalizeToDatanodeIfNeeded}}). It does not wait for the 
other SCMs. A follower raises its apparent version only when it applies the 
same entry, and its datanode protocol server keeps serving while it is behind: 
{{SCMStateMachine.tryStartDNServerAndRefreshSafeMode}} starts it once the 
follower reaches a fixed catch up index and never checks again.

When a finalized datanode registers with such a follower, 
{{SCMNodeManager.shouldFenceDatanode}} finds the datanode's apparent version 
above the follower's and {{register}} returns {{errorNodeNotPermitted}}. 
{{RegisterEndpointTask.call}} moves that endpoint to SHUTDOWN, and 
{{RunningDatanodeState.computeNextContainerState}} turns any SHUTDOWN endpoint 
into a shutdown of the whole datanode. A datanode registers with every SCM, so 
one lagging SCM is enough. Nothing in Ozone restarts the datanode after the 
follower catches up.

HDDS-15482 intends this shutdown for a cluster that is genuinely not finalized. 
Here the cluster is finalized and one SCM is only behind.

h3. Trigger
# SCM HA with all SCMs and datanodes pre-finalized.
# One follower stops receiving or applying new Raft log entries while datanodes 
can still reach it (for example a partition between it and the leader, or a 
stalled follower).
# Finalize. The leader and the other follower commit and apply FINALIZE, and 
the leader finalizes every datanode on its next heartbeat.
# A finalized datanode restarts (ordinary operation, for example a rolling 
restart) or re-registers, and registers with all SCMs.

Ordinary apply lag is not enough: without the cut off the follower applied 
FINALIZE within milliseconds and nothing happened. The window is open for as 
long as the follower stays behind.

By code reading only (not run): a follower installing a checkpoint keeps its 
datanode server up and is pre-finalized until the install completes, so the 
same rejection can happen during an install. At default config 
({{ozone.scm.ha.ratis.log.purge.enabled=false}}) a running follower does not 
need an install to catch up.

h3. Impact
Each finalized datanode that registers with the lagging follower stops, and 
stays stopped after the follower has applied FINALIZE. Its replicas are 
unavailable until it is restarted and are re-replicated once it is declared 
dead. A rolling restart of datanodes during the window stops every restarted 
datanode. No data loss was observed or is expected.

h3. Reproduction
PASS, deterministic, unmodified source. 
[^TestBugMC4LaggingFollowerFencesFinalizedDatanode.java] 
({{hadoop-ozone/integration-test}}) starts 3 SCMs and 3 datanodes, all 
pre-finalized, and cuts one follower off from new log entries with the Ratis 
{{CodeInjectionForTesting}} hook on {{RaftServerImpl.appendEntries}} (requests 
with entries are rejected on that follower, heartbeats pass). It finalizes 
through the public client, waits until the leader has finalized every datanode, 
restarts one datanode, then lets the follower catch up. Output on the 
unmodified code: the follower is at {{INITIAL_VERSION}} while all datanodes are 
finalized, the restarted datanode logs "SCM rejected this datanode's 
registration ... This datanode will shut down.", it is stopped, and it is still 
stopped after the follower has applied FINALIZE. A passing test means the 
defect is present.

h3. Suggested fix
A rejection from an SCM that is not the ready leader should not be final, 
because that SCM may not have applied the leader's latest state. Alternatives 
considered and not taken: letting the leader wait for all SCMs before sending 
{{FinalizeVersionCommand}} (the leader does not know the followers' applied 
index, and finalization would block while any SCM is down), letting followers 
accept the registration (they would keep datanodes the leader rejects), and 
deciding on the datanode (the register response does not say which SCM is the 
leader, so this needs a new field).

h3. Patch
[^MC-4-let-datanode-retry-when-non-leader-scm-rejects-registration.patch], 
against 7f9b6237deb2b06d73d1ac2b78cccaa60780c3ef. It also applies to master at 
268654683b4.

{{SCMDatanodeProtocolServer.register}} throws {{ServerNotLeaderException}} 
instead of returning {{errorNodeNotPermitted}} when this SCM is not the ready 
leader ({{SCMContext.isLeaderReady}}, read before the registration decision is 
made). The datanode handles an exception from register as a failed call and 
retries on its next cycle, so it keeps running and registers with the follower 
once that has caught up. The ready leader still returns 
{{errorNodeNotPermitted}}, so a datanode that is incompatible with the cluster 
is still shut down. The registration rules themselves are unchanged and still 
applied on every SCM. There is no protocol change, the SCM RPC server already 
suppresses logging of this exception, and Recon (no {{SCMContext}}) is not 
affected.

This applies to every rejection reason on a non leader SCM, not only the 
apparent version. Two outcomes change otherwise. A follower whose software is 
older than a datanode that the leader accepts no longer shuts that datanode 
down; it keeps running, registered with the leader. A datanode that would be 
rejected but cannot get an answer from a ready leader (it reaches only 
followers, the SCM quorum is lost, or a new leader is not ready yet) now keeps 
running unregistered and retries until the ready leader rejects it, instead of 
being shut down by a follower. A final shutdown then always comes from the SCM 
whose decision is authoritative.

Covered by {{testOnlyReadyLeaderRejectsRegistrationAsFinal}} in the existing 
{{TestSCMNodeManager}}: a pre-finalized SCM and a finalized datanode; a 
follower and a leader that is not ready yet make the datanode retry, the ready 
leader rejects it, and it is not registered. A pre-finalized datanode still 
registers with the follower. Without the change it fails because the follower 
returns {{errorNodeNotPermitted}}. With it {{TestSCMNodeManager}}, 
{{TestSCMStateMachine}}, {{TestSCMHAManagerImpl}} and {{TestSCMContext}} (90 
tests) pass, and on a mini cluster {{TestScmHAFinalization}} (including 
{{testFinalizedDatanodesShutDownWithPrefinalizedScm}}), 
{{TestSCMDatanodeProtocolServer}} and 
{{TestSCMFollowerCatchupWithContainerReport}} pass. Checkstyle is clean. With 
the patch the reproduction fails at its first assertion: the restarted datanode 
keeps running while the follower lags and registers with it after it has 
applied FINALIZE.

Remaining window, by code reading only: a deposed leader that has not yet 
stepped down still considers itself the ready leader and can still reject.

Found by TLA+ model checking and code review of the SCM upgrade finalization 
paths under HDDS-15926, on commit 7f9b6237deb2b06d73d1ac2b78cccaa60780c3ef. 
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]

Reply via email to