Siyao Meng created HDDS-17219:
---------------------------------

             Summary: Decommission monitor on a stepped down SCM can overwrite 
the operational state reported by a datanode, so a later re-promotion drops the 
decommission
                 Key: HDDS-17219
                 URL: https://issues.apache.org/jira/browse/HDDS-17219
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-7-skip-admin-monitor-op-state-writes-on-follower-scm.patch, 
TestBugMC7StaleFollowerMonitor.java

h3. Mechanism
The operational state of a datanode is not replicated through Ratis. The leader 
SCM keeps it in memory and, when a heartbeat reports a different persisted 
state, sends {{SetNodeOperationalStateCommand}} to the datanode. A follower SCM 
copies the persisted state from each heartbeat into its own memory 
({{SCMNodeManager.updateDatanodeOpState}}).

{{DatanodeAdminMonitorImpl}} runs on every SCM, and after a step down it keeps 
the nodes it was tracking as leader. In {{processTransitioningNodes}} it reads 
the node status, decides, and writes the new state later through 
{{setNodeOpState}}, without checking that the SCM is still leader or that the 
state is still the one it read. On a follower, when the read returns IN_SERVICE 
(the datanode has not applied the decommission yet), the monitor aborts the 
workflow and writes IN_SERVICE. If a heartbeat reporting DECOMMISSIONING is 
applied between that read and the write, the write replaces DECOMMISSIONING 
with IN_SERVICE and the node is no longer tracked.

If this SCM becomes leader again before the next heartbeat from that datanode, 
{{NodeDecommissionManager.onBecomeLeader}} ({{continueAdminForNode}}) sees 
IN_SERVICE and does not resume the workflow, and the next heartbeat makes it 
send IN_SERVICE to the datanode, which applies it. The decommission was 
accepted and persisted by the datanode, and is now dropped.

h3. Trigger
# SCM A, as leader, accepts a decommission, sets DECOMMISSIONING and sends the 
command to the datanode; its monitor starts tracking the node.
# A steps down before the datanode reports DECOMMISSIONING. A heartbeat still 
reporting IN_SERVICE is copied on A. This also needs the datanode to hold the 
command past its next heartbeat to A: it normally applies a command about a 
second after the heartbeat round that delivered it, so it has to be delayed 
(for example a busy command thread, or an early heartbeat round triggered by 
other events).
# A monitor tick on A reads IN_SERVICE. The datanode has meanwhile applied the 
command, and its heartbeat reporting DECOMMISSIONING is applied on A before the 
tick writes IN_SERVICE.
# A becomes leader again before the datanode's next heartbeat reaches it.

The interim leader must not process a heartbeat reporting DECOMMISSIONING while 
it is leader; otherwise it sends IN_SERVICE itself and the decommission is lost 
anyway. HDDS-5090, which added the follower copy and {{onBecomeLeader}}, 
documents and accepts that loss for a failover before the state reaches the 
followers; here the state has already reached the datanode and this SCM, which 
is what {{onBecomeLeader}} was added to preserve. HDDS-3204 (a new leader 
starts from its own view) is related. The datanode must also take the command 
off its queue before any heartbeat response from a leader of a newer term, 
because that response carries the new term and {{StateContext.getNextCommand}} 
then drops queued commands of an older term; in practice no newer leader may 
answer the datanode in that heartbeat round, for example after a failed 
election that A wins again (code reading). The read to write window is short (a 
log line and an in-memory update) per tracked node and monitor tick, and A has 
to lose and regain leadership within about one heartbeat interval (30s by 
default). This is rare.

h3. Impact
An accepted decommission is silently undone: the datanode returns to IN_SERVICE 
and the operator has to issue the decommission again. No data is lost. The 
other writes of the monitor on a follower (DECOMMISSIONED, IN_MAINTENANCE, 
IN_SERVICE at the end of maintenance, for a dead node or on recommission) do 
not need the heartbeat race; they are only sent if the SCM is re-promoted 
before the next heartbeat, and each is the decision the leader would make on 
its next tick anyway. Only the abort write racing a heartbeat has a harmful 
effect (code reading, not run).

h3. Reproduction
PASS, deterministic, unmodified source. [^TestBugMC7StaleFollowerMonitor.java] 
({{hadoop-hdds/server-scm}}) uses the real {{SCMNodeManager}} (from 
{{HddsTestUtils.getScm}}), {{NodeDecommissionManager}}, 
{{DatanodeAdminMonitorImpl}} and {{SCMContext}}. Leadership is changed with 
{{SCMContext.updateLeaderAndTerm}} and {{onBecomeLeader}}, heartbeats are 
{{processHeartbeat}} calls carrying the persisted state a datanode would 
report, and there is no datanode process. The monitor sees the node manager 
through a Mockito spy that, once, runs a heartbeat to completion on another 
thread right after the monitor's status read; that is the only timing help. The 
first test shows the state overwritten to IN_SERVICE and the re-promoted SCM 
sending IN_SERVICE to the datanode. Two controls show the correct outcome (the 
decommission resumes and completes) when the heartbeat arrives after the tick, 
or when one more heartbeat arrives before re-promotion. The interim leader is 
not modelled. A passing test means the defect is present.

h3. Patch
[^MC-7-skip-admin-monitor-op-state-writes-on-follower-scm.patch], against 
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It applies to master at 20d95ed37fe 
with a three way merge ({{git apply -3}}), because the lines next to the 
monitor construction in {{NodeDecommissionManager}} changed.

{{DatanodeAdminMonitorImpl}} now gets the {{SCMContext}} and 
{{setNodeOpState}}, the single place where the monitor writes the operational 
state, returns without writing when {{SCMContext.isLeader()}} is false, as 
{{continueAdminForNode}} already does. On a follower the state then always 
comes from the datanode heartbeats, the monitor still stops tracking nodes as 
before, and {{onBecomeLeader}} resumes the workflow from the reported state. 
Behaviour on the leader and without SCM HA is unchanged. The check is at the 
write, not at the start of the tick, so it also covers a tick that is running 
when leadership is lost. One follower side change: an admin request that the 
monitor had not yet applied when the SCM stepped down (for example a 
recommission still queued for its next tick) is now dropped on that SCM as 
well, as it already is on any other new leader (HDDS-5090); the operator 
repeats it. Before the patch the follower wrote IN_SERVICE locally, and a 
re-promotion before the next heartbeat pushed it to the datanode.

Covered by {{testFollowerDoesNotOverwriteOpStateReportedByHeartbeat}} in the 
existing {{TestDatanodeAdminMonitor}}, which delivers the heartbeat right after 
the monitor's status read on a follower and then checks that the workflow 
resumes and completes after re-promotion. Without the leader check it fails 
with IN_SERVICE instead of DECOMMISSIONING. With it 
{{TestDatanodeAdminMonitor}}, {{TestNodeDecommissionManager}}, 
{{TestNodeDecommissionMetrics}} and {{TestSCMNodeManager}} pass, the mini 
cluster {{TestDecommissionAndMaintenance}} passes (9 tests, three of them, the 
SCM restart cases, on a rerun after a port clash on the shared test machine; it 
runs a single SCM, so it covers the leader path only), and checkstyle is clean. 
With the patch the first test of the reproduction fails because the state stays 
DECOMMISSIONING, and the two controls still pass.

Found by TLA+ model checking and code review of the SCM datanode decommission 
and maintenance admin paths under HDDS-15926, on commit 
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. 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