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

Siyao Meng updated HDDS-16733:
------------------------------
    Attachment: MC-2-versioned-overwrite-orphans-previous-blocks.patch

> Concurrent open of an absent key on a versioned bucket drops the first 
> committed version and leaks its blocks
> -------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16733
>                 URL: https://issues.apache.org/jira/browse/HDDS-16733
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Critical
>         Attachments: MC-2-versioned-overwrite-orphans-previous-blocks.patch
>
>
> h3. Mechanism
> Bucket versioning preserves an overwritten version only through block 
> inheritance done at OPEN time ({{OMKeyRequest.prepareFileInfo}}, the 
> {{dbKeyInfo != null}} branch at {{OMKeyRequest.java:1013}}), not through 
> anything commit does. When two writers open the same not-yet-existing key, 
> neither open key inherits anything.
> The first commit publishes its record. The second commit finds {{keyToDelete 
> != null}} but, versioning being enabled, skips the overwrite cleanup branch 
> at {{OMKeyCommitRequest.java:348}} and falls into the {{else}} at {{:391}}, 
> which only does quota accounting: no old version retention, no deletion 
> enqueue, no reference tracking. The committed record is built from the second 
> writer's open key, which never contained the first writer's blocks.
> The first version is therefore silently discarded despite versioning being 
> on, and its blocks are referenced by nothing and queued for deletion by 
> nothing. The same branch also calls {{incrUsedNamespace(1L)}} for what is 
> really an overwrite.
> h3. Trigger
> Bucket {{v/b}} with versioning enabled (settable through the public native 
> client: {{OzoneBucket.setVersioning(true)}} to {{RpcClient.java:1224}} to 
> {{OMBucketSetPropertyRequest.java:175-181}}, no gate), key {{k}} absent:
> # Writer A sends {{CreateKey v/b/k}}. {{dbKeyInfo == null}}, so A's open key 
> holds only A's blocks.
> # Writer B sends {{CreateKey v/b/k}} before A commits. Still {{dbKeyInfo == 
> null}}, so B's open key holds only B's blocks.
> # A sends {{CommitKey}}. {{keyToDelete == null}}; key table gets A's record, 
> version 0, blocks {{{bA}}}.
> # B sends {{CommitKey}}. {{keyToDelete}} is A's record, versioning on, so 
> control goes to the {{else}} at {{:391}}. Key table gets B's record, version 
> 0, blocks {{{bB}}}. A's version is gone and {{{bA}}} was never enqueued.
> Both are plain unconditional PUTs, so {{validateAtomicRewrite}} 
> ({{OMKeyRequest.java:1367}}) is a no-op on this path. Four ordinary requests, 
> no injected fault.
> h3. Impact
> The first committed version is unrecoverable and its blocks are leaked 
> permanently: nothing in the tree scans containers for unreferenced blocks, 
> and a block absent from {{deletedTable}} is never reclaimed. The bucket's 
> {{usedNamespace}} and {{usedBytes}} served to users are also inflated, which 
> {{ozone repair om quota}} can recompute later but which does not reclaim the 
> blocks.
> h3. Developer intent
> {{TestOmBlockVersioning}} enables versioning ({{:78}}) and asserts versions 
> accumulate 0, 1, 2 through the public client ({{:99-135}}), so version 
> retention on the native path is a promised property and losing a version is 
> incorrect. Blame on the {{else}} block shows only quota work; nothing 
> discusses old block ownership on the versioned path. The "versioning is not 
> supported" notes in the tree are about the S3 API and the MPU path, which is 
> awareness that versioning is incomplete rather than a report of this defect, 
> and they do not disable the native flag the commit path honours.
> h3. Reproduction
> PASS at escalation level 1. 
> {{repro/test_bugMC-2_versioned_concurrent_open_orphans_blocks.java}}, run as 
> {{TestSpeculaMC2VersionedConcurrentOpen extends OMKeyRequestTests}} in the 
> existing {{request/key}} suite, no new cluster.
> Versioning ON, the bug lines are the last four:
> {noformat}
> [MC-2] bucket versioning enabled = true
> [MC-2] after A commit, keyTable versions=[version:0 conID 1001 locID 101 || ]
> [MC-2] after B commit, keyTable versions=[version:0 conID 1002 locID 102 || ]
> [MC-2] blocks referenced by keyTable: [{blockID={conID: 1002 locID: 102}}]
> [MC-2] blocks enqueued in deletedTable: []
> [MC-2] bucket usedNamespace=2 usedBytes=2000
> [MC-2] writer A block conID: 1001 locID: 101 referenced=false 
> queuedForDelete=false
> {noformat}
> Negative control, identical interleaving with versioning OFF, which passes:
> {noformat}
> [MC-2] bucket versioning enabled = false
> [MC-2] blocks enqueued in deletedTable: [{blockID={conID: 1001 locID: 101}}]
> [MC-2] bucket usedNamespace=1 usedBytes=1000
> [MC-2] writer A block conID: 1001 locID: 101 referenced=false 
> queuedForDelete=true
> {noformat}
> Expected correct behaviour is the control's: A's blocks either retained as 
> version 0 of the versioned key, or enqueued in {{deletedTable}}. Flipping 
> only {{isVersionEnabled}} moves control from {{:348}} (enqueues, quota 
> correct at 1 key and 1000 bytes) to {{:391}} (enqueues nothing, quota 
> inflated to 2 and 2000), so the cause is isolated and a harness artifact is 
> ruled out.
> The OM apply path is single threaded by construction, so the interleaving is 
> chosen rather than raced. The only harness adjustment of note is that the 
> shared base class's {{allocateBlock}} stub returns one fixed block ID for 
> every call, which real SCM never does and which would have hidden the leak 
> for a mock only reason.
> h3. Suggested fix
> On the versioned commit path, treat {{keyToDelete != null}} as an overwrite 
> that must preserve or release the previous version's blocks, rather than 
> falling through to quota accounting only. Either retain the previous record's 
> version group in the committed record (which is what versioning promises) or 
> enqueue its blocks in {{deletedTable}} as the non versioned branch does. The 
> quota call on that branch should also stop counting an overwrite as a new 
> namespace entry.
> Add coverage to the existing {{request/key}} suite: two writers opening the 
> same absent key on a versioned bucket, asserting after both commits that 
> every block from the first commit is either referenced by {{keyTable}} or 
> present in {{deletedTable}}, and that {{usedNamespace}} is 1.
> h3. Patch
> [^MC-2-versioned-overwrite-orphans-previous-blocks.patch], against 
> 20e6a1c0f6039248373efd3eb1d7f2afcf7f1535.
> Widens the overwrite cleanup branch in {{OMKeyCommitRequest}} and 
> {{OMKeyCommitRequestWithFSO}} from {{keyToDelete != null && 
> !getIsVersionEnabled()}} to also cover the versioned case where the committed 
> record carries over none of the previous record's blocks, which is exactly 
> the concurrent open shape. A new shared {{hasBlocksNotCarriedOver}} in 
> {{OMKeyRequest}} is the predicate. An ordinary versioned overwrite, where the 
> open key did inherit the previous version, still takes the untouched {{else}} 
> branch, so version retention and its quota arithmetic are unchanged. On the 
> orphaning path, {{filterOutBlocksStillInUse}} keeps any block the new record 
> does share, and the branch's existing {{decrUsedNamespace}} plus 
> {{incrUsedNamespace}} arithmetic nets the inflated namespace count back to 1.
> Scope note, for reviewers to decide: this makes the first version's blocks 
> reclaimable and fixes the namespace count. It does not add true version 
> retention, which would mean renumbering the committing writer's version group 
> and prepending the previous one, a change to version numbering semantics that 
> belongs in its own issue. After this patch the first version is still lost, 
> as it is today, but its blocks are no longer leaked.
> True version retention is also in scope for the active S3 object versioning 
> work (HDDS-15728 and its T1 to T7 subtasks), so that half may belong there 
> rather than in a separate fix.
> Covered by {{testVersionedConcurrentOpenDoesNotOrphanTheFirstVersionsBlocks}} 
> in the existing {{TestOMKeyCommitRequest}}, inherited by the FSO subclass. 
> Two writers commit the same absent key on a versioned bucket; the test 
> asserts the first writer's block is enqueued for deletion and that 
> {{usedNamespace}} is 1. Verified to fail without the change and pass with it 
> on OBS and FSO. Checkstyle clean. The 23 tests in each of those two classes 
> pass.
> Found by TLA+ model checking and code review of the OM conditional create, 
> overwrite and delete paths under HDDS-15926, on commit 
> 20e6a1c0f6039248373efd3eb1d7f2afcf7f1535. Checked against open HDDS issues 
> for duplicates before filing, in addition to the repository's merged git 
> history. Generated with Specula (Claude Opus 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