Siyao Meng created HDDS-16898:
---------------------------------

             Summary: ReplicationManager resends an expired force DELETE to the 
last known replica of a container after the other replicas are lost
                 Key: HDDS-16898
                 URL: https://issues.apache.org/jira/browse/HDDS-16898
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-1-do-not-resend-expired-force-delete-of-last-replica.patch, 
TestBugMC1ExpiredDeleteResentToLastReplica.java

h3. Mechanism
Since HDDS-12127, {{ContainerReplicaPendingOps.removeExpiredEntries}} keeps an 
expired DELETE op and {{ReplicationManager.opCompleted}} resends the same 
{{DeleteContainerCommand}} with a new deadline. The resend does not look at the 
replicas the container has now. Whether the delete is safe was checked once, 
when it was first sent: for a balancer move in 
{{MoveManager.handleSuccessfulAdd}}, for ReplicationManager in its over 
replication and unhealthy replica handlers. Nothing cancels the pending DELETE 
when the other replicas go away, so after {{DeadNodeHandler}} has removed the 
replicas of dead nodes, SCM still resends a force DELETE to the only replica it 
knows of. While that op is pending, {{RatisUnderReplicationHandler.getSources}} 
excludes its target as a replication source, so ReplicationManager cannot copy 
the container from it either.

On the datanode, {{DeleteContainerCommandHandler.handleInternal}} checks only 
the command deadline and the SCM term, and {{KeyValueHandler.deleteInternal}} 
skips the open and non empty checks when {{force}} is set. A unit level run 
during review confirmed that a force {{DeleteContainerCommand}} passed to 
{{DeleteContainerCommandHandler.handle}} removes a CLOSED container with blocks 
from the container set and deletes its directory; the existing 
{{TestKeyValueHandler#testDeleteDecrementsVolumeUsedSpace}} also force deletes 
a CLOSED container with blocks.

h3. Trigger
# A CLOSED RATIS/THREE container has replicas on A, B and C. The balancer moves 
it from A to D.
# D reports its replica, the move check passes and MoveManager sends a force 
DELETE to A.
# A stays alive but does not run that DELETE, for example because the delete 
waited longer than {{hdds.datanode.delete.container.timeout}} (60s) for the 
container lock (the datanode then skips it and relies on the SCM resend), or 
because its delete container queue is full.
# B, C and D become dead and their replicas are removed. SCM knows only A, the 
container is under replicated, and no replication command is sent because A has 
a pending delete.
# The deadline passes. MoveManager ends the move with 
{{DELETION_FAIL_TIME_OUT}} and ReplicationManager resends the force DELETE to A 
with a fresh deadline.

The resend repeats every {{hdds.scm.replication.event.timeout}} (12 minutes) 
for as long as A is alive and has not deleted the replica, so the other nodes 
do not have to die within one move timeout. They do have to die while A keeps 
failing to run the delete.

h3. Impact
If A runs the resent command, the last copy SCM knows of is deleted and the 
container has no known replica. The data then exists only on the dead nodes and 
is lost if they do not come back. For a RATIS container this needs three 
replica holders to become dead while a fourth stays alive but does not run 
deletes, so it is unlikely, but the delete is issued by SCM itself and nothing 
stops it.

For EC the condition is per replica index and much easier to meet: MoveManager 
also moves EC replicas with a force delete of the source index, so only the 
move target has to die after the source delete was sent. The resend then 
deletes the last copy of that index, which must be reconstructed. Redundancy 
drops, but there is no data loss on its own.

A weaker RATIS case is not changed by the patch: if only B and C die, the 
resend leaves D as the only replica (1 of 3). The container stays recoverable 
from D.

h3. Reproduction
PASS, deterministic, unmodified source. 
[^TestBugMC1ExpiredDeleteResentToLastReplica.java] ({{hadoop-hdds/server-scm}}) 
uses the real {{ContainerManagerImpl}}, {{ContainerReplicaPendingOps}}, 
{{ReplicationManager}}, {{MoveManager}} and {{DeadNodeHandler}}, subscribed in 
the same order as in {{StorageContainerManager}}. Only the node manager (node 
status and command sink), the placement policy and the SCM context are mocks. A 
not running the first DELETE is modelled by never delivering it, and the 
scrubber is {{removeExpiredEntries}} after the clock passes the move timeout. 
Output on the unmodified source:
{noformat}
move result: DELETION_FAIL_TIME_OUT
replicas known to SCM: 1 (only A), pending DELETE ops on A: 1
commands sent by the scrubber run: [(<A>,deleteContainerCommand: ... 
containerID: 1, replicaIndex: 0, force: true)]
{noformat}
A passing test means the defect is present. No datanode runs in this test; the 
datanode side is covered by the run described under Mechanism.

h3. Suggested fix
Before resending a force DELETE, check that another copy of the same replica 
index can still serve as a replication source, and drop the pending delete if 
none can. The patch counts as a copy only a replica that is not UNHEALTHY, is 
on a HEALTHY node (the same node check 
{{RatisUnderReplicationHandler.getSources}} uses for sources) and is not itself 
pending deletion. A copy on a STALE node therefore does not count, which also 
protects A during the 10 minutes before the failed nodes are declared dead. If 
such a peer comes back, the container is over replicated again and 
ReplicationManager issues a fresh, checked delete.

A broader recheck of the container health before each resend would also cover 
the 1 of 3 case, but would change the retry of deletes that ReplicationManager 
issues on purpose while a container is under replicated 
({{RatisUnderReplicationHandler.removeUnhealthyReplicaIfPossible}}), so it is 
not part of this patch.

h3. Patch
[^MC-1-do-not-resend-expired-force-delete-of-last-replica.patch], against 
b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at 
6818707698c.

Before resending an expired force DELETE, {{ReplicationManager.opCompleted}} 
checks for another usable copy of the same index as described above. If there 
is none, it logs a warning and removes the pending op instead of resending, so 
the replica becomes a replication source again. Non force deletes (empty and 
DELETING containers) are resent as before. The op is removed with a new 
{{ContainerReplicaPendingOps.removeExpiredOp}}, which does not notify 
subscribers: {{removeOp}} would report the op as completed, and because 
ReplicationManager is subscribed before MoveManager, MoveManager would then 
finish the move as {{COMPLETED}} instead of {{DELETION_FAIL_TIME_OUT}}.

Covered in the existing {{TestReplicationManager}} by 
{{testExpiredDeleteOfLastReplicaIsResentOnlyIfNotForce}} (the force delete is 
dropped and later subscribers see only the expiry; a non force delete is still 
resent), {{testExpiredForceDeleteIsNotResentWhenOtherCopyIsNotUsable}} (the 
other copy is UNHEALTHY, or CLOSED on a STALE node), 
{{testExpiredForceDeleteOfLastCopyOfEcIndexIsNotResent}} and 
{{testExpiredForceDeletesKeepOneReplicaWhenAllRemainingArePendingDelete}}; 
{{testPendingOpExpiry}} now registers the replicas it relies on. Without the 
change every force case fails because the DELETE is resent. With it the 
replication manager, pending ops and balancer unit suites pass and checkstyle 
is clean. With the patch the reproduction fails: no command is sent, the 
pending DELETE is gone and the move result is still {{DELETION_FAIL_TIME_OUT}}.

The resend was added by HDDS-12127. HDDS-16447 proposes another precondition 
for the same resend in {{ReplicationManager.opCompleted}} (resend only while 
the target replica is still present). The two checks are independent, but both 
patches change the same lines and should be combined when applied. HDDS-16897 
is a separate defect enabled by the same resend (a resent copy that outlives 
its pending op and deletes a newer replica on the datanode); it needs a 
datanode side fix.

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]

Reply via email to