Siyao Meng created HDDS-17605:
---------------------------------

             Summary: Deleting services can mark the wrong snapshot deep 
cleaned after a snapshot rename, or undo the deep clean reset of a snapshot 
purge
                 Key: HDDS-17605
                 URL: https://issues.apache.org/jira/browse/HDDS-17605
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-3-recheck-snapshot-before-setting-deep-clean-flag.patch, 
TestBugCR3StaleDirDeepCleanFlagAfterPurge.java, 
TestBugMC3RenamedSnapshotDeepCleanFlag.java

h3. Mechanism
{{KeyDeletingService}} and {{DirectoryDeletingService}} deep clean one snapshot 
per task. When a task has nothing left to purge, it sends 
{{SetSnapshotProperty}} with {{deepCleanedDeletedKey=true}} or 
{{deepCleanedDeletedDir=true}}, and every later task skips a snapshot that 
carries the flag. The flag request names the snapshot only by the table key 
({{/volume/bucket/name}}) that the task read when it started, and carries 
nothing about the previous snapshot the scan ran against. 
{{OMSnapshotSetPropertyRequest.validateAndUpdateCache}} sets the flag on 
whatever row has that key when it is applied. Unlike {{PurgeKeys}} and 
{{PurgeDirectories}}, which call {{SnapshotUtils.validatePreviousSnapshotId}} 
at apply time, nothing checks that the snapshot is still the one that was 
scanned, against the same previous snapshot.

Two cases follow.
* *Rename.* {{OMSnapshotRenameRequest}} takes the bucket lock and the snapshot 
name locks, not the {{SNAPSHOT_GC_LOCK}} the deleting services use, so a rename 
of the scanned snapshot followed by a new snapshot with the old name can be 
applied while the task runs. The flag then lands on the new snapshot, which was 
never scanned. The renamed snapshot stays unmarked and is scanned again 
(harmless).
* *Purge of the previous snapshot.* When a deleted snapshot is purged, 
{{OMSnapshotPurgeRequest}} sets {{deepClean}} and {{deepCleanedDeletedDir}} 
back to false on the next two snapshots so they are cleaned again against their 
new previous snapshot. {{SnapshotDeletingService}} submits the purge after it 
releases its lock, the purge apply takes no lock, and 
{{DirectoryDeletingService}} submits the directory flag after its workers have 
released their GC locks. A flag built against the old previous snapshot can 
therefore be applied after the purge and overwrite the reset.

h3. Trigger
* *Rename:* from ozone-2.2.0 it needs {{ozone.om.snapshot.rename.allowed=true}} 
(default false). ozone-2.0.0 to ozone-2.1.2 have no rename switch, but there 
snapshot deep cleaning is off by default 
({{ozone.snapshot.deep.cleaning.enabled=false}}; HDDS-15187 turned it on by 
default in 2.2.0), so they are affected only with it enabled. A user renames 
snapshot S and creates a new snapshot named S in the same bucket while a 
deleting service task on S is running. For a {{DirectoryDeletingService}} task, 
which sends the flag only when it purged nothing, and for a 
{{KeyDeletingService}} task that purges nothing, the window is the whole task, 
from the start of the scan to the apply of the flag. For a 
{{KeyDeletingService}} task that purges, it starts at the apply of its last 
{{PurgeKeys}}: a {{PurgeKeys}} applied after the rename goes to the old name, 
fails its previous snapshot check, and no flag follows.
* *Purge:* no special config from ozone-2.2.0; on ozone-2.1.x only with 
{{ozone.snapshot.deep.cleaning.enabled=true}}. Snapshot S1 is deleted while 
{{DirectoryDeletingService}} deep cleans the next snapshot S2 against S1 and 
finds nothing it can purge, then {{SnapshotDeletingService}} purges S1 (it has 
nothing to move) and the purge is applied before the directory flag. The 
directory workers hold GC read locks on S1 and S2 while they scan, so 
{{SnapshotDeletingService}} can only evaluate S1 after they release them; its 
purge then races the coordinator's flag request through Ratis. This is argued 
from the code; the reproductions control the order and do not measure how often 
the race is won in a running cluster.

h3. Impact
The wrong snapshot carries a deep clean flag until a later purge in its chain 
resets it or the snapshot itself is deleted.
* Rename, key flag: the new snapshot is skipped by every {{KeyDeletingService}} 
run, so its deleted keys that are reclaimable against the renamed snapshot are 
not reclaimed. Rename, directory flag: the same for the deleted directories of 
the new snapshot (shown at flag level only).
* Purge, directory flag: a deleted directory that became reclaimable against 
the new previous snapshot stays in the snapshot's {{deletedDirTable}}, together 
with whatever it holds.
No data is lost and nothing is reclaimed early; the effect is space that is not 
reclaimed. The same purge mechanism for the key flag is HDDS-16464. At this 
commit that case is already narrow: {{KeyDeletingService}} keeps its GC read 
locks on the snapshot and its previous snapshots until its 
{{SetSnapshotProperty}} is applied, and its filter refuses a previous snapshot 
that is not active, so {{SnapshotDeletingService}} cannot purge the previous 
snapshot of a run that read any entry before the key flag is applied. The 
{{DirectoryDeletingService}} coordinator submits its flag after the workers 
have released those locks, which is why the directory flag is the purge case 
reported here.

h3. Reproduction
Two self contained classes for {{hadoop-ozone/ozone-manager}} (target path and 
Maven command in each class comment) run one real OM through 
{{OmTestManagers}}, real {{KeyDeletingTask}} / {{DirDeletingTask}} runs, and 
requests sent through {{OzoneManagerProtocol}} or Ratis. No state is injected; 
the only aid is a hook that runs the rename or purge at a chosen point of the 
task.
* [^TestBugMC3RenamedSnapshotDeepCleanFlag.java]: with rename allowed, snapshot 
snapa (id1) is renamed to snapb and a new snapa (id2) holding one reclaimable 
deleted key is created after the task's {{PurgeKeys}} request. id2 gets 
{{deepCleanedDeletedKey=true}}, id1 stays false, and a later task on id2 leaves 
the key in id2's {{deletedTable}}.
* [^TestBugCR3StaleDirDeepCleanFlagAfterPurge.java]: snap0, snap1, snap2 with a 
directory that only snap1 references in snap2's {{deletedDirTable}}. snap1 is 
deleted during the {{DirDeletingTask}} on snap2 and purged with the request 
{{SnapshotDeletingService}} sends, applied before the flag. snap2's previous 
becomes snap0 with {{deepCleanedDeletedDir=true}} in RocksDB, and a second task 
on snap2 skips it and leaves the directory in place.
Both classes also run a case where the change happens after the flag request 
has been built; see Patch.

h3. Suggested fix
The strict fix is for {{SetSnapshotPropertyRequest}} to carry the snapshot ID 
and the expected previous snapshot ID, checked at apply time like the purge 
requests. That is a new request field, so it needs a new layout feature to keep 
OMs of two versions applying the same log in agreement (HDDS-16464 proposes the 
previous snapshot ID field for the key flag only, without a layout feature). 
The attached patch stays on the submitting side.

h3. Patch
[^MC-3-recheck-snapshot-before-setting-deep-clean-flag.patch], against 
284fad307bfd129fca3f4966d854ac45ea3b0d11. It also applies to master at 
6135e81a09ac6d618dc0ad4d220dd8cf254586b6.

A new helper {{SnapshotUtils.getTableKeyToMarkDeepCleaned}}, used by both 
services right before they build the flag request, looks the snapshot's table 
key up again by snapshot ID and returns nothing, so no flag is sent, when the 
snapshot is gone, its previous snapshot is not the one the scan used, or that 
previous snapshot is no longer active. The last condition covers a purge 
already on its way: {{SnapshotDeletingService}} only purges deleted snapshots. 
A skipped flag only means the snapshot is scanned again in the next run; 
exclusive size updates in the same request are sent as before. No request, 
config or apply path changes. Because the helper is shared, 
{{KeyDeletingService}} gets the same check for the key flag, which also narrows 
what is left of the HDDS-16464 case (a run that read no entry and so took no GC 
lock) on the submitting side.

This narrows the windows, it does not close them: a rename plus a create, or a 
delete, its flush and a {{SnapshotDeletingService}} run, all landing between 
the check and the apply of the flag request (one Ratis round trip) still mark 
the wrong snapshot. Each reproduction class has a case for that order, and it 
still shows the bug with the patch. A rename plus a create in the few lines 
between a task reading the snapshot by ID and opening its DB by name also 
remains: the task then scans the new snapshot while the flag goes to the 
renamed one, and any purge in that run is rejected.

New tests in existing suites: 
{{TestDirectoryDeletingService#testDeepCleanFlagAfterSnapshotChangeDuringRun}} 
(no change, previous purged during the run, previous deleted with its purge 
applied just before the flag, rename and reuse of the name) , 
{{TestKeyDeletingService.SnapshotDbHandleLifecycle#testDeepCleanFlagFollowsRenamedSnapshot}}
 and {{TestSnapshotUtils#testTableKeyToMarkDeepCleanedWhenPreviousIsPurged}} 
(the previous snapshot purged between the helper's two lookups counts as not 
active instead of failing the whole request). Without the change the three 
change cases and the rename case fail; with it they pass, as do 
{{TestKeyDeletingService}}, {{TestDirectoryDeletingService}}, 
{{TestSnapshotDeletingService}}, 
{{TestOMSnapshotSetPropertyRequestAndResponse}}, 
{{TestOMSnapshotPurgeRequestAndResponse}}, {{TestSnapshotUtils}} and the 
{{TestReclaimable*}} filter suites (578 tests), and checkstyle is clean.

Found by TLA+ model checking and code review of the OM snapshot purge, set 
property and double buffer apply paths under HDDS-15926, on commit 
284fad307bfd129fca3f4966d854ac45ea3b0d11. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not a 
duplicate: HDDS-16464 (the purge case for the key flag, with a proposed apply 
side check of the previous snapshot ID; it does not cover the directory flag, 
and its text does not discuss renames, though its proposed check would reject 
most of them for the key flag because the new snapshot's previous is the 
renamed one). 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