Siyao Meng created HDDS-17241:
---------------------------------

             Summary: Quota repair can leave bucket snapshotUsedBytes negative 
when a snapshot purge commits after the repair's checkpoint and before the 
snapshot scan
                 Key: HDDS-17241
                 URL: https://issues.apache.org/jira/browse/HDDS-17241
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
CR-11-fail-quota-repair-when-snapshot-changes-after-checkpoint.patch, 
TestBugCR11QuotaRepairSnapshotPurgeSkew.java

h3. Mechanism
{{QuotaRepairTask.repairTask}} takes a checkpoint of the active OM DB; the 
bucket baselines and all active table counts come from it. 
{{recalculateSnapshotDbPendingDeleteQuota}} instead opens each snapshot DB live 
through {{OmSnapshotManager.getSnapshot}} and counts its current 
{{deletedTable}} and {{deletedDirTable}}. The repair sends the computed value 
minus the baseline for {{snapshotUsedBytes}} and {{snapshotUsedNamespace}}, and 
{{OMQuotaRepairRequest.updateBucketInfo}} adds it to the current bucket.

A request that removes entries from a snapshot DB and lowers the bucket's 
{{snapshotUsed*}} in the same transaction ({{OMKeyPurgeRequest}} or 
{{OMDirectoriesPurgeRequestWithFSO}} with a snapshot table key, as sent by the 
deep clean of the deleting services) and commits after the checkpoint but 
before that snapshot is scanned is then subtracted twice: the scan no longer 
sees the entries, the baseline still has them, and the live bucket has already 
been lowered. A purge applied after both deleted tables of that snapshot have 
been read is harmless. The same undercount happens when a snapshot that is 
active in the checkpoint is deleted, has its entries moved to the active DB and 
is purged before the scan reaches it: the scan walks the live snapshot chain, 
so it skips that snapshot, and the moved entries are in neither the 
checkpoint's active tables nor any scanned snapshot. Nothing serializes the 
repair with the deleting services, and the counters are not clamped, so 
{{OMKeyRequest.checkBucketQuotaInBytes}} and {{checkBucketQuotaInNamespace}} 
(which use used plus snapshotUsed) see the bucket that much emptier.

h3. Trigger
* A bucket with a snapshot that still holds deleted keys or directories that 
are not yet deep cleaned.
* A quota repair ({{ozone repair om quota start}}, admin only).
* A deep clean purge of that snapshot, or its move and purge after a snapshot 
delete, committing between the checkpoint and the scan of that snapshot. The 
window covers opening the checkpoint DB, reading the bucket table and scanning 
the snapshots that come earlier in the scan.

h3. Impact
* {{snapshotUsedBytes}} and {{snapshotUsedNamespace}} of the bucket end below 
their true value by what the purge removed, negative if nothing else is pending 
(reproduced: -3000 and -1 where 0 and 0 are correct). The value is written to 
RocksDB and stays until a repair runs without the race.
* By the code, not run: quota checks undercount the bucket by the same amount, 
so writes beyond the quota are accepted.

h3. Reproduction
[^TestBugCR11QuotaRepairSnapshotPurgeSkew.java] goes into 
{{hadoop-ozone/ozone-manager}} (target package and Maven command in the class 
comment) and passes on unmodified source while the bug is present. It deletes a 
key into a snapshot and runs {{QuotaRepairTask}} with the request it submits 
captured and applied. The purge is a real {{OMKeyPurgeRequest}} with the 
snapshot table key; the only control is that it commits on the repair's first 
call to {{getOmSnapshotManager()}}, which comes from the snapshot scan after 
the checkpoint. A control with the purge committed before the repair ends at 0 
and 0.

h3. Patch
[^CR-11-fail-quota-repair-when-snapshot-changes-after-checkpoint.patch], 
against 20d95ed37fe35013b5320713665db03c06ee36fa. It also applies to master at 
41d95dd3286.

The repair now lists the snapshots of each repaired bucket from the 
checkpoint's {{snapshotInfoTable}} instead of the live snapshot chain, so a 
snapshot removed from the chain after the checkpoint is still visited. Every 
request that changes a snapshot DB or removes a snapshot (key and directory 
purge, snapshot moves, snapshot delete and purge) already updates the 
snapshot's {{lastTransactionInfo}} in the snapshot info cache, or removes the 
entry, when it is applied. After scanning a snapshot DB the repair compares the 
live value with the one from the checkpoint and fails the run ("Snapshot ... 
changed after the quota repair checkpoint was taken, run the quota repair 
again") instead of submitting deltas that mix two points in time; a snapshot 
that can no longer be opened fails the run as well. A change applied after that 
comparison cannot have been seen by the scan, and a change flushed before the 
checkpoint is in both, so neither fails the run. The run fails whenever a 
snapshot of a repaired bucket changed between the checkpoint and the end of its 
scan, which includes some changes that would not have skewed the result; 
rerunning is safe. Deep cleaning is on by default, so on a busy cluster a long 
repair can fail several times in a row; {{--buckets}} narrows the window to the 
snapshots of those buckets. Clamping at zero would hide the symptom but still 
be wrong when other deletes are pending. A possible follow up is to pause the 
deleting services for the scan instead, by holding {{BOOTSTRAP_LOCK}} from 
before the checkpoint until the snapshot scan ends, together with a double 
buffer flush, as {{OMDBCheckpointServlet}} does; that always converges but 
stalls deletion and follower bootstrap for the length of the scan.

Covered in the existing {{TestQuotaRepairTask}}: 
{{testQuotaRepairFailsWhenSnapshotPurgedAfterCheckpoint}} fails without the 
change ("expected: 0L but was: -3000L"), 
{{testQuotaRepairFailsWhenSnapshotLeavesChainAfterCheckpoint}} fails without it 
("expected: 3000L but was: 0L"), and 
{{testQuotaRepairSucceedsWhenSnapshotPurgedBeforeCheckpoint}} checks that a 
purge flushed before the repair does not fail the run. With the change, the 
quota repair, key and directory purge and snapshot move suites (46 tests) pass, 
including the existing {{testQuotaRepairSnapshotDbDeletedTableQuota}} for the 
case without a race, as does {{TestOzoneRepairShell}} on a mini cluster, and 
checkstyle is clean. With the patch the race test of the reproduction fails 
because the repair run fails, and its control passes.

Found by code review of the OM volume, bucket and snapshot lifecycle paths, as 
part of the TLA+ verification effort under HDDS-15926, on commit 
20d95ed37fe35013b5320713665db03c06ee36fa. 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