Siyao Meng created HDDS-17067:
---------------------------------

             Summary: Registration does not refresh a known datanode's 
heartbeat time, so the dead node handler keeps or erases its replicas wrongly
                 Key: HDDS-17067
                 URL: https://issues.apache.org/jira/browse/HDDS-17067
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-4-refresh-heartbeat-on-register-and-recheck-dead-node-before-replica-removal.patch,
 TestBugMC4DeadNodeRegisterReport.java, 
TestBugMC5DeadHandlerErasesReturnedNode.java

h3. Mechanism
{{SCMNodeManager.register}} refreshes nothing for a datanode it already knows. 
A new node gets a fresh heartbeat time from the {{DatanodeInfo}} constructor, 
and a node that registers with a changed address, port set or version gets one 
from {{updateNode}}, but a plain re-registration leaves the old heartbeat time 
in place. Health is computed from that time by 
{{NodeStateManager.checkNodesHealth}}, so a registered node keeps the health of 
its last heartbeat before the outage until its next heartbeat is processed. 
{{SCMDatanodeProtocolServer.register}} then fires the registration report, and 
{{ContainerReportHandler}} applies it whatever the node's health is.

{{DeadNodeHandler.onMessage}} reads the node status once when it starts, then 
removes every replica of the node ({{removeContainerReplicas}}) after closing 
its containers and destroying its pipelines, without reading the health again 
and without the {{DatanodeInfo}} monitor that {{ContainerReportHandler}} and 
{{IncrementalContainerReportHandler}} hold while they apply a report. DEAD_NODE 
and CONTAINER_REPORT are handled on different threads. DEAD_NODE fires only on 
the transition into DEAD (and again when the operational state of a DEAD node 
changes).

Recon uses the same code ({{ReconNodeManager.register}} delegates to it, 
{{ReconDeadNodeHandler}} extends {{DeadNodeHandler}}). In Recon the window is 
wider: when a heartbeat arrives after a gap of at least 3 x 
{{hdds.heartbeat.recon.interval}}, {{ReconNodeManager.processHeartbeat}} 
answers with {{ReregisterCommand}} and updates only its own heartbeat map, not 
the heartbeat time of {{NodeStateManager}}. The datanode registers again at 
once and sends its next heartbeat one {{hdds.heartbeat.interval}} (30s) later, 
so in Recon a returning node keeps its old health for about 30s after it has 
registered.

This gives three wrong outcomes:
* A DEAD node registers, its report re-adds its replicas, and it stops again 
before its next heartbeat. It never leaves DEAD, DEAD_NODE does not fire again, 
and the replicas stay.
* A node comes back while STALE, shortly before its dead deadline, and 
registers. The deadline still applies to its old heartbeat time, so it is 
declared DEAD after its registration report has been applied, and 
{{DeadNodeHandler}} removes the replicas of a live node.
* A node comes back while {{DeadNodeHandler}} for it is still running (or 
queued). The handler removes the replicas of the registration report that was 
applied in the meantime.

h3. Trigger
A datanode that restarts, or loses its network, for about the dead interval 
({{ozone.scm.dead.node.interval}}, 10 minutes by default). For the first 
outcome it must then stop again within about 30s of registering with Recon 
(about 2s with SCM, the time from registration to the first heartbeat, 
{{hdds.heartbeat.initial-interval}}). For the second it must come back within 
about 30s before Recon's dead deadline. For the third the DEAD_NODE handling 
must overlap the registration. No client action is involved.

h3. Impact
Reproduced in Recon with real node manager timers and handlers (see 
Reproduction); the SCM side is argued from the shared code, not run.
* *Recon reports a container HEALTHY that has no live replica* (first outcome): 
after the node stopped again and was DEAD, the replica on it was still in 
Recon's container manager and {{ReconReplicationManager}} reported the 
container HEALTHY instead of MISSING. It stays that way while the node is down. 
[^TestBugMC4DeadNodeRegisterReport.java]
* *Recon reports a container MISSING on a HEALTHY datanode* (second and third 
outcomes): the replica is removed after the registration report, the node is 
HEALTHY again at its next heartbeat, and the container stays MISSING until the 
node's next full container report. After a restart that is 
{{hdds.container.report.interval}} plus a random delay of up to the same 
interval, 60 to 120 minutes by default; an incremental report restores single 
containers earlier. [^TestBugMC5DeadHandlerErasesReturnedNode.java]
* *SCM (argued, not run):* {{RatisContainerReplicaCount}} does not look at node 
health, so a datanode that stops within about 2s of registering after a restart 
would keep its replicas on a DEAD node, and {{ReplicationManager}} would not 
re-replicate them for as long as the node stays down. The 2s is only the 
trigger window. A replica removed from a live node would cause unnecessary 
replication until the next full report.

h3. Reproduction
Both attached classes go in {{hadoop-ozone/recon}} (package 
{{org.apache.hadoop.ozone.recon.scm}}, Maven command in the class comment) and 
pass on unmodified source when the bug is present. They run a real 
{{ReconNodeManager}} with short stale and dead intervals, 
{{ReconDeadNodeHandler}}, {{ReconContainerReportHandler}} and 
{{ReconReplicationManager.processAll}}, and drive the datanode side through 
{{register}} and {{processHeartbeat}}. {{TestBugMC4DeadNodeRegisterReport}} and 
{{returnsShortlyBeforeDeadDeadline}} in 
{{TestBugMC5DeadHandlerErasesReturnedNode}} use no hooks. 
{{comesBackWhileDeadHandlerRuns}} holds the dead node handler at its first 
container lookup until the node has come back, which only changes timing.

h3. Patch
[^MC-4-refresh-heartbeat-on-register-and-recheck-dead-node-before-replica-removal.patch],
 against 916aa1d9949cbb10f2750a40c8704340893131a2. It also applies to master at 
14e0611fdd5.

Two parts, both needed:
* {{SCMNodeManager.register}} calls {{DatanodeInfo.updateLastHeartbeatTime}} 
for an already registered node. A registered node then leaves STALE or DEAD at 
the next health check, and if it stops again it goes through DEAD again and its 
replicas are removed. This closes the first and second outcomes. 
{{ReconNodeManager.loadExistingNodes}} no longer registers nodes it already 
knows again when an SCM DB snapshot sync reloads its node table, so the reload 
does not bring dead nodes back. The table holds what those nodes registered 
with, so the reload had nothing to apply to them.
* {{DeadNodeHandler.removeContainerReplicas}} takes the {{DatanodeInfo}} 
monitor that the report handlers hold, checks under it that the node is still 
DEAD and only then removes the replicas; otherwise it logs that it keeps them. 
This closes the third outcome.

Applying the registration report was kept on purpose: it is the only full 
report a returning datanode sends for 60 to 120 minutes, so skipping reports 
from DEAD nodes would lose replicas instead. Behaviour change in SCM and Recon: 
a known datanode that registers with the same address, ports and version now 
leaves STALE or DEAD at the next health check (3s) instead of after its first 
heartbeat, through the usual transition and its events. New nodes, and 
registrations with a changed address, ports or version, already get a fresh 
heartbeat time at registration, and fenced registrations are rejected before 
the refresh. Not covered: a dead node handler that rechecks in the seconds 
between the registration and that health check, and a registration report that 
waits in the report queue for longer than the dead interval.

Covered by {{testRegisterMovesDeadNodeOutOfDead}} in {{TestSCMNodeManager}} 
(DEAD, HEALTHY after registering again, DEAD again after it stops), 
{{testDeadNodeHandlerKeepsReplicasOfNodeThatCameBack}} in 
{{TestDeadNodeHandler}} and {{testReinitializeKeepsDeadNodeDead}} in 
{{TestReconNodeManager}}. Without the change the first two fail with "expected: 
HEALTHY but was: DEAD" and with a replica removal that should not happen; 
without the {{loadExistingNodes}} change the third fails with "expected: DEAD 
but was: HEALTHY". With it the {{hadoop-hdds/server-scm}} node, server and 
container tests pass (369 tests), as do the unit tests in the Recon {{scm}} and 
{{fsck}} packages and {{TestReconTasks}}, checkstyle is clean, and the attached 
reproductions no longer show the bug.

Found by TLA+ model checking and code review of the datanode container report 
and Recon container tracking paths under HDDS-15926, on commit 
916aa1d9949cbb10f2750a40c8704340893131a2. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-16633 (re-registration racing DEAD handling in the network 
topology), HDDS-16632 (container list lost on re-registration with a new 
address), HDDS-14834 and HDDS-16623 (resolved, network topology after DEAD in 
the same handler), HDDS-14950 (NodeManager concurrency in general). 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