Siyao Meng created HDDS-16897:
---------------------------------
Summary: Datanode runs a resent DeleteContainerCommand against a
replica imported after the command arrived, so a balancer move can leave the
container under replicated while reporting COMPLETED
Key: HDDS-16897
URL: https://issues.apache.org/jira/browse/HDDS-16897
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-6-skip-delete-of-container-added-after-command-received.patch,
TestBugMC6StaleDeleteWipesMoveTarget.java
h3. Mechanism
SCM completes a pending DELETE op as soon as a report says the replica is gone
({{ContainerStateManagerImpl.removeContainerReplica}} calls
{{ContainerReplicaPendingOps.completeDeleteReplica}}), whichever command
removed it. Since HDDS-12127, {{ReplicationManager.opCompleted}} resends an
expired DELETE as the same command with a new deadline. If the first copy
already ran on the datanode but its DELETED report reaches SCM only after the
SCM deadline, SCM resends the command, and the late report then completes the
op while the resent copy is still queued on the datanode. SCM no longer knows
about that copy.
{{MoveManager.move}} checks the replicas and pending ops SCM knows of, but not
commands already handed to the datanode, so it accepts that datanode as a move
target. When the copy has been imported and reported,
{{MoveManager.handleSuccessfulAdd}} sees a healthy replica set and deletes the
source. The queued command then runs:
{{DeleteContainerCommandHandler.handleInternal}} checks only the deadline and
the SCM term and deletes by container ID, so it force deletes the replica the
move just created.
h3. Trigger
With the default timeouts ({{hdds.scm.replication.event.timeout}} 12 minutes,
datanode offset 6 minutes):
# A CLOSED RATIS/THREE container has 4 replicas. ReplicationManager sends a
force DELETE to datanode V.
# V runs it, but cannot reach SCM for long enough that the DELETED report
arrives after the 12 minute SCM deadline (so at least about 6 minutes, and less
than the dead node interval).
# The scrubber resends the DELETE to V.
# V reconnects. The heartbeat carries the late report and the response carries
the resent DELETE, which waits behind other work in V's delete container
threads (2 by default).
# Within the 6 minute datanode deadline of the resent command, the balancer
moves this container to V, the copy completes and MoveManager deletes the
source.
# The queued DELETE runs on V and deletes the imported replica.
h3. Impact
The container is left with 2 of 3 replicas while the balancer is told the move
{{COMPLETED}}. ReplicationManager re-replicates from the remaining two, so
there is no data loss on its own, only a window with reduced redundancy. The
trigger needs a long report delay, a backed up delete queue and the balancer
choosing that container and datanode within a few minutes, so it is unlikely.
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC6StaleDeleteWipesMoveTarget.java] ({{hadoop-hdds/server-scm}}) runs
the real ReplicationManager (over replication through its own health check and
{{OverReplicatedProcessor}}), {{ContainerReplicaPendingOps}},
{{ContainerManagerImpl}}, {{IncrementalContainerReportHandler}} and
{{MoveManager}}, and gives each datanode a real
{{DeleteContainerCommandHandler}}, {{ContainerController}} and
{{ContainerSet}}. Only the disk work of the container handler is mocked. The
test controls when the delayed report is processed and keeps V's delete thread
(1 thread in the test) busy until the moved replica is imported. Output on the
unmodified source:
{noformat}
late ICR completed (removed) the pending DELETE, DELETE #2 still queued on V
(V=d2) | scmReplicas=[d1, d3, d4] pendingOps=[] dnHas=[d1,d3,d4] V.deleteQueue=1
MoveManager accepted move d1 -> d2 (V=d2) | scmReplicas=[d1, d3, d4]
pendingOps=[ADD@d2] dnHas=[d1,d3,d4] V.deleteQueue=1
ADD completed, MoveManager sent DELETE to source d1 (V=d2) | scmReplicas=[d1,
d2, d3, d4] pendingOps=[DELETE@d1] dnHas=[d1,d2,d3,d4] V.deleteQueue=1
V delete thread drained, delete calls on V = 2 (V=d2) | scmReplicas=[d1, d2,
d3, d4] pendingOps=[DELETE@d1] dnHas=[d1,d3,d4] V.deleteQueue=0
RESULT: move=COMPLETED dnReplicas=2 scmReplicas=[d3, d4]
health=UNDER_REPLICATED (RF 3)
{noformat}
A passing test means the defect is present. The heartbeat transport and the
datanode disk work are simulated.
h3. Suggested fix
The patch makes the datanode skip a delete for a container that was not present
when the command arrived but is present when it runs. Other ways a queued
delete can meet a newer replica (the replica disappears for another reason,
such as a failed volume, while the original command is still queued) are not
covered. An SCM side alternative would keep a resent DELETE's target excluded
from moves until the command's datanode deadline has passed, which needs new
state in SCM; it is not part of this patch.
h3. Patch
[^MC-6-skip-delete-of-container-added-after-command-received.patch], against
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at
6818707698c.
{{DeleteContainerCommandHandler.handle}} records whether the container exists
when the command is received. If it did not, and it exists when the command
runs, the command is skipped with an INFO log. In every other case the
behaviour is unchanged: an absent container is still a no-op, and a replica
that was present on receipt is deleted as before, including when the container
object is replaced in between (for example by the disk balancer). No protocol
change.
The resend comes from HDDS-12127. HDDS-16447 is the same race on the pending op
in the opposite order (a report removes the op, then the resend inserts it
again); both follow from SCM tracking a DELETE per target and replica index
rather than per command. This patch also keeps the orphan resends described
there from deleting a later replica on that datanode, because the container is
absent when they arrive. The linked issue covers the other defect of the same
resend (sent without rechecking that other copies remain), which needs an SCM
side fix.
Covered by {{testContainerAddedAfterCommandReceivedIsNotDeleted}} and
{{testContainerPresentWhenCommandReceivedIsDeleted}} in the existing
{{TestDeleteContainerCommandHandler}}. Without the change the first fails
({{NeverWantedButInvoked}} on {{deleteContainer}}). With it the datanode state
machine and command handler unit suites pass and checkstyle is clean. With the
patch the reproduction fails: the queued DELETE is skipped, the moved replica
survives and the container ends with 3 replicas, HEALTHY.
Found by TLA+ model checking and code review of the SCM container move and
balancer 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]