Siyao Meng created HDDS-17621:
---------------------------------

             Summary: SCM brings a datanode back from DEAD without a full 
container report, so it forgets the node's replicas for up to two hours
                 Key: HDDS-17621
                 URL: https://issues.apache.org/jira/browse/HDDS-17621
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: MC-4-reregister-datanode-returning-from-dead.patch, 
TestBugMC4ReturnedDeadNodeReplicasForgotten.java

h3. Mechanism
When a datanode goes DEAD, {{DeadNodeHandler}} removes its replicas from SCM 
({{removeContainerReplicas}}), unless the node is in maintenance. If the 
datanode was not restarted (network partition, long pause), it never registers 
again: by default {{HeartbeatEndpointTask}} only counts the failed heartbeats, 
and full container reports consumed by failed heartbeats are not put back. When 
its heartbeats reach SCM again, {{SCMDatanodeHeartbeatDispatcher.dispatch}} 
processes them normally (it sends {{ReregisterCommand}} only to unregistered 
nodes and to STALE or DEAD nodes with an incompatible version), the health 
check moves the node from DEAD to HEALTHY, and 
{{UnhealthyToHealthyNodeHandler}} only notifies services. Nothing asks for a 
full container report, so SCM learns the node's replicas again only from its 
next periodic report, which comes every {{hdds.container.report.interval}} plus 
a random delay of up to the same interval (60 to 120 minutes by default). 
Incremental reports cover only containers whose state changes.

In that window the node is HEALTHY and in service, but SCM's replica set for 
its containers does not include it. That set is used by:
* Target selection in ReplicationManager. 
{{ReplicationManagerUtil.getExcludedAndUsedNodes}} only excludes nodes SCM 
knows hold a replica, and it is shared by {{ECUnderReplicationHandler}}, 
{{RatisUnderReplicationHandler}}, {{MisReplicationHandler}} and 
{{QuasiClosedStuckUnderReplicationHandler}}. The returned node can therefore be 
chosen as a target for a container it already holds. On the datanode an EC 
reconstruction then fails as a whole, because 
{{ECReconstructionCoordinator.reconstructECContainerGroup}} first creates a 
recovering container on every target and 
{{KeyValueHandler.handleCreateContainer}} rejects it with 
CONTAINER_ALREADY_EXISTS for any index. A Ratis replication is pushed by the 
source and rejected by {{SendContainerRequestHandler}} with CONTAINER_EXISTS. 
The existing replica is not touched. SCM is not told; the pending adds expire 
after {{hdds.scm.replication.event.timeout}} (12 minutes) and the next round 
picks targets again.
* Read pipelines for containers that are not open. 
{{SCMClientProtocolServer.getContainerWithPipelineCommon}} builds them with 
{{createPipelineForRead}} from the same replica set, so they leave the returned 
node out.

h3. Trigger
# One or more datanodes lose contact with SCM, without a restart, for longer 
than {{ozone.scm.dead.node.interval}} (10 minutes by default), and SCM removes 
their replicas.
# Their heartbeats reach SCM again.

No client action is involved. If every holder of a container is away at the 
same time (for example SCM cut off from the datanodes, or a network outage 
covering all of a container's racks), SCM has no replica of that container at 
all until the full reports arrive.

h3. Impact
* *Reads (correlated case):* when every holder of a closed container returned 
from DEAD together, SCM returns a read pipeline with no nodes until the first 
holder's next full report (run in the reproduction below), and 
ReplicationManager reports the container MISSING meanwhile (argued from code). 
What a client then sees was not run: OM fetches pipelines from SCM on a 
location cache miss ({{ozone.om.container.location.cache.ttl}}, 360 minutes), 
so a read that misses OM's cache in that window gets the empty pipeline and 
cannot reach the data (argued from code).
* *Reads (single node):* the read pipeline lacks the returned node, so a Ratis 
read has one replica fewer and an EC read may have to reconstruct the index 
that node holds (argued from code).
* *Repair:* replication or reconstruction commands that cannot succeed are sent 
to the returned node. With EC 3-2 on five nodes the returned node is the only 
candidate, so every round fails until its full report (harmless there, the 
index is present). If another index or replica is truly lost and the returned 
node is among the few eligible targets, its repair is held up the same way, for 
up to about two hours. With more spare nodes a round that draws k targets 
includes the returned node with a probability of about k in the number of 
eligible nodes, and each bad pick costs one event timeout. In the same window 
SCM also rebuilds copies that are present on the returned node onto other 
nodes, which later turns into over replication. Argued from code, not run here.

No data is deleted ({{EmptyContainerHandler}} does not act on a container with 
no replicas, argued from code), and the state clears with the next full report.

h3. Reproduction
[^TestBugMC4ReturnedDeadNodeReplicasForgotten.java] 
({{hadoop-hdds/server-scm}}, Maven command in the class comment) runs a real 
in-process SCM ({{SCMNodeManager}} with health checks, {{DeadNodeHandler}} on 
the SCM event queue, {{ContainerManager}}), the real 
{{SCMDatanodeHeartbeatDispatcher}} and the client protocol server, with 
heartbeats sent from the test and stale and dead intervals of 300 ms and 600 
ms. A closed Ratis container has a replica on each of three datanodes. The 
three stop heartbeating until they are DEAD and {{DeadNodeHandler}} has removed 
the replicas, then heartbeat again: each reply carries no command, all three 
become HEALTHY, SCM still lists no replica for the container, and 
{{getContainerWithPipeline}} returns a pipeline with no nodes. A passing test 
means the defect is present. It runs in about 2 seconds and does not start 
datanodes.

h3. Patch
[^MC-4-reregister-datanode-returning-from-dead.patch], against 
e4ae41fc4047c99af983020b4f33bd297c9d0ac5. It also applies to master at 
3fc2387040f.

{{SCMDatanodeHeartbeatDispatcher.dispatch}} queues a {{ReregisterCommand}} 
before processing a heartbeat from a node that SCM has as DEAD, so it is 
returned in the reply to that heartbeat; the heartbeat itself is processed as 
before. The datanode registers again at once, and registration carries a fresh 
full container report, so SCM knows the node's replicas again within seconds 
instead of after the next periodic report. This is the same command and 
datanode path used after every SCM restart, and Recon already asks a returning 
datanode to register again after a heartbeat gap 
({{ReconNodeManager.processHeartbeat}}). There is no protocol, configuration or 
datanode change. The existing parameterized test 
{{TestSCMDatanodeHeartbeatDispatcher.testUnhealthyNodeWithCompatibleVersionProcessesHeartbeatNormally}}
 now expects one re-register command for a DEAD node and none for a STALE one; 
it fails without the change. The SCM node and server unit suites (256 tests) 
pass and checkstyle is clean. With the patch the reproduction fails at its 
first assertion because the reply contains the re-register command.

Each return of a DEAD datanode now causes one re-registration and one full 
container report per SCM. Recon shares the dispatcher and already asks a 
returning datanode to re-register; with this change it asks once more on the 
following heartbeat. Narrow races remain and are no worse than today: a 
registration report applied while {{DeadNodeHandler}} for the same node is 
still pending can be undone by it (HDDS-17067), {{DeadNodeHandler}} clearing 
the command queue between the two dispatcher steps would drop the command, and 
a heartbeat that arrives while the health check is marking the node DEAD is not 
asked.

Found by TLA+ model checking and code review of the SCM EC container 
replication and placement repair paths under HDDS-15926, on commit 
e4ae41fc4047c99af983020b4f33bd297c9d0ac5. 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]

Reply via email to