[ 
https://issues.apache.org/jira/browse/HDDS-17458?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-17458:
------------------------------
    Attachment: MC-1-wait-for-snapshot-purge-flush-before-bucket-delete.patch

> DeleteBucket after the purge of the bucket's last snapshot succeeds or fails 
> depending on the OM's flush timing, so OM replicas diverge
> ---------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17458
>                 URL: https://issues.apache.org/jira/browse/HDDS-17458
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 1.4.0, 2.0.0, 1.4.1, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-1-wait-for-snapshot-purge-flush-before-bucket-delete.patch, 
> TestBugMC1SnapshotBucketDeleteDivergence.java
>
>
> h3. Mechanism
> {{OMBucketDeleteRequest.validateAndUpdateCache}} rejects the delete with 
> {{CONTAINS_SNAPSHOT}} when {{bucketContainsSnapshot}} finds a snapshot of the 
> bucket. That check runs on every OM when it applies the log entry, and it ORs 
> two lookups that both depend on how far this OM has flushed:
> * {{bucketContainsSnapshotInCache}} scans the {{snapshotInfoTable}} cache and 
> returns true for any key of the bucket, including the null value (delete 
> marker) that {{OMSnapshotPurgeRequest}} puts there. The code carries a TODO 
> for this case.
> * {{bucketContainsSnapshotInTable}} reads RocksDB only, so it still finds a 
> snapshot whose purge is applied but not yet flushed.
> {{snapshotInfoTable}} has a partial cache. The DB row disappears when this 
> OM's double buffer flushes the purge, and the delete marker shortly after, 
> when the cache evicts it. Flush timing is local to each OM and is not part of 
> the Ratis log, and nothing in the apply path waits for a flush. So for the 
> same log (CreateSnapshot, DeleteSnapshot, SnapshotPurge, DeleteBucket), an OM 
> that has flushed and evicted the purge deletes the bucket, and an OM that has 
> not returns {{CONTAINS_SNAPSHOT}} and keeps it.
> h3. Trigger
> The last snapshot of a bucket is purged by {{SnapshotDeletingService}}, and 
> DeleteBucket follows while at least one OM has not yet flushed that purge and 
> another has. A bucket with snapshots cannot be deleted before its snapshots 
> are purged, so a client that deletes the snapshots and then retries 
> DeleteBucket until it succeeds sends it right after the purge. An OM that 
> applies the two entries back to back, for example a follower catching up 
> after a restart or a slow disk, then decides differently from the leader. No 
> fault injection is needed.
> The reverse order (the leader rejects, a follower that flushed faster 
> deletes) follows from the same code but was not run. In that case a retry of 
> the client converges the replicas.
> h3. Impact
> * Permanent divergence between OM replicas: after both have flushed the 
> DeleteBucket entry, one has no bucket and the volume's {{usedNamespace}} is 
> decremented, the other still has the bucket and the old {{usedNamespace}} 
> (run). Nothing reconciles them short of a checkpoint install.
> * After a leader change to the OM that kept it, the bucket that the client 
> was told is deleted is visible again, and later requests on that bucket name 
> (create bucket, delete volume) get different results on different OMs 
> (follows from the state above, not run).
> h3. Reproduction
> PASS on unmodified source, deterministic, about 3 to 5 seconds. 
> [^TestBugMC1SnapshotBucketDeleteDivergence.java] 
> ({{hadoop-ozone/ozone-manager}}) builds two OM replicas, each with its own 
> RocksDB, {{OzoneManagerStateMachine}}, request handler and double buffer (the 
> {{OzoneManager}} itself is a mock), and applies the same four log entries to 
> both through {{OzoneManagerStateMachine.applyTransaction}}. Only flush timing 
> differs, controlled with the double buffer pause hook: r1 flushes the purge 
> and waits until the cache has evicted it before it applies DeleteBucket, r2 
> applies all four entries and then flushes. DeleteBucket returns OK on r1 and 
> {{CONTAINS_SNAPSHOT}} on r2, and with both flushed to index 4, r1's DB has no 
> bucket and {{usedNamespace}} 0 while r2's DB has the bucket and 
> {{usedNamespace}} 1. A passing test means the defect is present. Not 
> exercised: a real OM HA ring.
> h3. Suggested fix
> Making the check skip delete markers and read the cache before the DB is not 
> enough on its own. It lets an OM apply DeleteBucket while the CreateSnapshot 
> of the same bucket is still in its double buffer, and the flush of that 
> CreateSnapshot ({{OmSnapshotManager.createOmSnapshotCheckpoint}}) looks up 
> the bucket ID through the bucket table cache, gets {{BUCKET_NOT_FOUND}} and 
> terminates the OM. This was observed by running the attached reproduction 
> with that change applied (on r2). Today the check is protected from this by 
> counting the delete marker. The patch therefore keeps the delete marker as a 
> reason to wait instead of a reason to reject.
> A deterministic check without a wait is also possible: make the check 
> tombstone aware with the existing 
> {{OmMetadataManagerImpl.isKeyPresentInTableCache}} and 
> {{isKeyPresentInTable}} pair (the merge that {{containsIncompleteMPUs}} 
> uses), and make the CreateSnapshot flush resolve the bucket and volume IDs 
> from the DB instead of the table cache. At that flush the DB holds exactly 
> the state before the CreateSnapshot, because 
> {{OzoneManagerDoubleBuffer.splitReadyBufferAtCreateSnapshot}} puts 
> CreateSnapshot in a batch of its own after the earlier entries, so the bucket 
> is there even if a later DeleteBucket has removed it from the cache. The 
> patch does not take that route because it is a wider change to snapshot code, 
> and every other bucket lookup at flush time would need the same review.
> h3. Patch
> [^MC-1-wait-for-snapshot-purge-flush-before-bucket-delete.patch], against 
> a5814528cdc8d3c1b27f259e0db2256b585cb825. It also applies to master at 
> b6d0181075e.
> {{bucketContainsSnapshotInCache}} now returns true only for a live snapshot 
> entry. If it finds only purged entries of the bucket, it waits for this OM's 
> double buffer to flush ({{OzoneManager.awaitDoubleBufferFlush}}) before the 
> DB lookup. After that wait the purge and every earlier CreateSnapshot of the 
> bucket are in the DB on every OM, so the result depends only on the log, and 
> the bucket is deleted only after its snapshot entries are flushed. The wait 
> happens only for a DeleteBucket that finds a purged snapshot entry of its own 
> bucket still in the cache, which is the window this issue is about. It holds 
> the apply thread, and so every later write on this OM, together with the 
> bucket lock, until this OM's double buffer has completed its current and its 
> next flush. That is normally milliseconds, and up to about one second when 
> the flush thread is idle. It is longer when that flush itself waits, for 
> example for readers of the purged snapshot to close it (the purge's flush 
> takes the snapshot DB lock to delete its checkpoint), for a CreateSnapshot 
> checkpoint, or for the snapshot DB content lock held by snapshot 
> defragmentation. Live snapshots still reject the delete as before.
> Covered by {{testBucketDeleteAfterSnapshotsPurged}} in the existing 
> {{TestOMBucketDeleteRequest}} (also run by 
> {{TestOMBucketDeleteRequestWithFSO}}), for a purge that is flushed and one 
> that is not. Without the change both cases fail with {{expected: <OK> but 
> was: <CONTAINS_SNAPSHOT>}}. With it, the bucket request, snapshot request, 
> state machine and double buffer unit suites (254 tests) and 
> {{TestOzoneFsSnapshot}} on a mini cluster (20 tests, including the rejected 
> delete of a bucket with a snapshot) pass, and checkstyle is clean. With the 
> patch the reproduction fails at the assertion on r2's DeleteBucket status, 
> because r2 waits for its flush and then deletes the bucket as r1 does.
> Open pull request apache/ozone#10965 (HDDS-16060) also edits 
> {{bucketContainsSnapshot}} (it makes the method static and documents that a 
> deleted but not yet purged snapshot still counts). The two patches conflict 
> textually there, so whichever lands second needs a rebase. They agree on the 
> behaviour: this patch still counts such a snapshot.
> Found by TLA+ model checking and code review of the OM table cache, double 
> buffer flush and startup paths under HDDS-15926, on commit 
> a5814528cdc8d3c1b27f259e0db2256b585cb825. 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