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

Siyao Meng updated HDDS-16732:
------------------------------
    Attachment: MC-1-open-key-reaper-retains-live-blocks.patch

> Open key reaper enqueues a live key's inherited blocks for deletion on a 
> versioned bucket
> -----------------------------------------------------------------------------------------
>
>                 Key: HDDS-16732
>                 URL: https://issues.apache.org/jira/browse/HDDS-16732
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Critical
>         Attachments: MC-1-open-key-reaper-retains-live-blocks.patch
>
>
> h3. Mechanism
> With bucket versioning enabled, {{OMKeyRequest.prepareFileInfo}} calls 
> {{OmKeyInfo.addNewVersion(locations, false, 
> omBucketInfo.getIsVersionEnabled())}} ({{OMKeyRequest.java:1017}}), and 
> {{addNewVersion}} clears {{keyLocationVersions}} only when 
> {{keepOldVersions}} is false ({{OmKeyInfo.java:417}}). An open key created 
> over an existing key therefore keeps the live key's block version groups, and 
> that whole record is what {{OMKeyCreateRequest}} writes to {{openKeyTable}}.
> If that open key is abandoned, {{OpenKeyCleanupService}} submits a 
> {{DeleteOpenKeysRequest}}. 
> {{OMOpenKeysDeleteRequest.updateOpenKeyTableCache}} 
> ({{OMOpenKeysDeleteRequest.java:186}}) checks only that the key is still in 
> {{openKeyTable}}, never {{keyTable}}, and 
> {{OMOpenKeysDeleteResponse.addToDBBatch}} 
> ({{OMOpenKeysDeleteResponse.java:74}}) hands the entire record to 
> {{AbstractOMKeyDeleteResponse.addDeletionToBatch}}, which puts every version 
> group into {{deletedTable}} with no reference check.
> {{filterOutBlocksStillInUse}} ({{OMKeyRequest.java:1248}}), the guard written 
> for exactly this hazard, is wired into {{OMKeyCommitRequest.java:364}}, 
> {{OMKeyCommitRequestWithFSO.java:277}} and 
> {{S3MultipartUploadCompleteRequest.java:354}}, but not into the reaper path. 
> {{ReclaimableKeyFilter.isReclaimable}} ({{ReclaimableKeyFilter.java:87}}) 
> decides purely from previous snapshots and returns true immediately when none 
> exist, so nothing downstream catches it either.
> h3. Trigger
> No conditional write, no fault injection, no timing race.
> # Bucket has versioning enabled.
> # Client A writes key {{k}}; {{keyTable[k]}} references block {{b1}}.
> # Client B calls {{createKey(k)}} to overwrite. The {{openKeyTable}} entry 
> now holds two version groups, {{[{b1}, {b2}]}}.
> # Client B never commits (process killed, partition, abandoned upload). The 
> key is not hsync'd, so {{getExpiredOpenKeys}} routes it to the delete branch.
> # After {{ozone.om.open.key.expire.threshold}} (default 5 min), 
> {{OpenKeyCleanupService}} submits {{DeleteOpenKeysRequest}}.
> # {{deletedTable}} now holds an entry whose blocks include {{b1}}, while 
> {{keyTable[k]}} still references {{b1}}.
> # {{KeyDeletingService}} ships {{b1}} to SCM 
> ({{KeyDeletingService.java:171-172}}). Client A's committed key is destroyed.
> h3. Impact
> A committed key's blocks are enqueued for physical deletion while the key is 
> still live and readable. The state is durable in RocksDB, nothing re-checks 
> it, and {{deletedTable}}'s only consumer acts on it. Default configuration, 
> no cluster faults required.
> h3. Developer intent
> {{git log -S filterOutBlocksStillInUse}} returns exactly two commits, both 
> fixing this hazard class at other sites. The first introduced the helper for 
> key commit and states the intent plainly: "Remove any block from 
> oldVerKeyInfo that share the same container ID and local ID with omKeyInfo 
> blocks'. Otherwise, it causes data loss once those shared blocks are added to 
> deletedTable and processed by KeyDeletingService for deletion." The second 
> extended it to S3 MPU complete three weeks before this head. Neither touches 
> the open key reaper. {{TestOMOpenKeysDeleteRequest}} asserts only table 
> membership, metrics and audit, never the blocks that reach {{deletedTable}}, 
> so nothing encodes the current behaviour as intended.
> h3. Reproduction
> PASS at escalation level 0, pure black box. Every step is a real OM write 
> request driven through {{preExecute}} plus {{validateAndUpdateCache}} plus 
> {{addToDBBatch}} with the RocksDB batch committed, exactly as the Ratis state 
> machine drives them. No state injection, no product code patch, no fault 
> injection.
> {{repro/test_bugMC-1_run.sh}}, which runs {{mvn -B -o -pl 
> hadoop-ozone/ozone-manager test 
> -Dtest=TestSpeculaBugMC1OpenKeyReaperBlockLoss}}.
> {noformat}
> [repro] live keyTable blocks      = [conID: 5001 locID: 1]
> [repro] abandoned openKey blocks  = [conID: 5001 locID: 1, conID: 5002 locID: 
> 1] (versionGroups=2)
> [repro] deletedTable blocks       = [conID: 5001 locID: 1, conID: 5002 locID: 
> 1]
> [repro] blocks KeyDeletingService would send to SCM = [conID: 5001 locID: 1, 
> conID: 5002 locID: 1]
> [repro] LIVE block conID: 5001 locID: 1 in deletedTable? true ; handed to 
> SCM? true
> [control-commit] deletedTable = []
> [control-nover] deletedTable = [conID: 5002 locID: 1]
> {noformat}
> Block 5001 is the live key's committed block. After the reaper runs it is in 
> {{deletedTable}} and in the list {{keyManager.getPendingDeletionKeys(...)}} 
> returns, which is what {{KeyDeletingService.java:171-172}} submits to 
> {{ScmBlockLocationProtocol.deleteKeyBlocks}}. Correct behaviour would have 
> put only 5002 there.
> Two negative controls pin the mechanism. {{[control-commit]}}: same scenario 
> but client B commits, which enqueues nothing, so the defect is the reaper 
> path rather than the inheritance itself. {{[control-nover]}}: same reaper 
> scenario on a non versioned bucket, where {{keyLocationVersions.clear()}} 
> runs and only 5002 reaches {{deletedTable}}, pinning the trigger to 
> {{getIsVersionEnabled()}}.
> Stated limitation: the final DataNode level block removal was not executed, 
> since a request level unit test has no SCM or DataNode. What is demonstrated 
> is that OM durably enqueues a live key's block and hands it to the SCM delete 
> path.
> h3. Suggested fix
> Call {{filterOutBlocksStillInUse}} on the open key reaper path as the commit 
> and MPU complete paths already do: in 
> {{OMOpenKeysDeleteRequest.updateOpenKeyTableCache}}, look the key up in 
> {{keyTable}} under the bucket lock already held, strip from the expiring 
> record every block the live key still references, and skip the 
> {{deletedTable}} put entirely when nothing is left.
> A root cause alternative is to stop the {{openKeyTable}} record carrying the 
> live key's version groups at all, since the open key only needs its own newly 
> allocated blocks and the old versions are re-read from {{keyTable}} at 
> commit. That removes the hazard from every reaper like consumer at once but 
> touches versioning semantics, so the targeted filter is the safer first patch.
> Either way, add the missing negative assertion to 
> {{TestOMOpenKeysDeleteRequest}}: on a versioned bucket with a pre-existing 
> live key, the reaped open key must contribute no block that {{keyTable}} 
> still references. The reproduction is written against {{OMKeyRequestTests}}, 
> the shared base for that package, so it can be folded in as a test method 
> rather than a new class.
> h3. Patch
> [^MC-1-open-key-reaper-retains-live-blocks.patch], against 
> 20e6a1c0f6039248373efd3eb1d7f2afcf7f1535.
> Routes the record the reaper enqueues through a new 
> {{withLiveBlocksRetained}}, which resolves the live key from the open key 
> name (dropping the trailing client id segment inverts both {{getOpenKey}} and 
> {{getOpenFileName}}, so it is layout agnostic) and runs the existing 
> {{filterOutBlocksStillInUse}} over a copy of the expiring record. A record 
> left with no blocks is still removed from openKeyTable and simply contributes 
> nothing to deletedTable, because 
> {{AbstractOMKeyDeleteResponse.addDeletionToBatch}} already guards that put 
> with {{isKeyEmpty}}, so no response class changes.
> Covered by {{testReapRetainsBlocksTheCommittedKeyStillReferences}} in the 
> existing {{TestOMOpenKeysDeleteRequest}}, parameterized over both bucket 
> layouts. It asserts both directions: the inherited key must not reach 
> deletedTable, and a key whose block no committed key holds must still be 
> reclaimed. Verified to fail without the change and pass with it on OBS and 
> FSO. Checkstyle clean. The 14 tests in that class 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