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

Siyao Meng updated HDDS-16827:
------------------------------
    Attachment: TestBugMC1RecoveryCommitClosesNewWriter.java

> Lease recovery commit closes the file of a writer that overwrote the path 
> during the recovery and drops its hsynced data
> ------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16827
>                 URL: https://issues.apache.org/jira/browse/HDDS-16827
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-1-reject-recovery-commit-with-block-not-in-open-key.patch, 
> TestBugMC1RecoveryCommitClosesNewWriter.java
>
>
> h3. Mechanism
> {{recoverLease}} is two OM transactions with datanode RPCs in between: 
> {{RecoverLease}}, which marks the writer's open key with {{LEASE_RECOVERY}} 
> and returns the file's blocks, and a {{CommitKey}} with {{recovery=true}} and 
> client id 0. For that commit {{OMKeyCommitRequestWithFSO.prepareCommit}} 
> takes the writer from {{HSYNC_CLIENT_ID}} of the current {{fileTable}} entry 
> and closes that writer's open key. Nothing ties the commit to the open key 
> that {{RecoverLease}} worked on: the request does not name the writer, and a 
> block in the request that the resolved open key does not have is dropped with 
> a warning ("Unknown BlockLocation" in {{OmKeyInfo.updateLocationInfoList}}) 
> instead of failing the commit.
> Overwriting an hsynced file is allowed (HDDS-10770 made it a supported 
> operation, and {{OMFileCreateRequestWithFSO}} has no hsync or recovery 
> check). If another client overwrites the file and hsyncs between the two 
> transactions, the recovery commit resolves the new writer's open key. The 
> blocks named in the commit belong to the old writer, so they are dropped as 
> unknown and the new writer's blocks are returned as uncommitted. Those go to 
> {{deletedTable}}, and the file is closed with the old writer's length and no 
> blocks.
> {{OMKeyCommitRequest}} has the same code. {{RecoverLease}} is refused for 
> buckets that are not FSO, so the race was only run on FSO. The same writer 
> resolution and block handling are in the ozone-2.0.0 and ozone-2.1.0 tags 
> (read, not run).
> h3. Trigger
> hsync is enabled ({{ozone.hbase.enhancements.allowed}} and 
> {{ozone.fs.hsync.enabled}}, both false by default), FSO bucket.
> # Writer 1 creates a file, writes, hsyncs and stalls.
> # A recoverer calls {{recoverLease}}. {{RecoverLease}} is applied and the 
> recoverer talks to the datanodes ({{FinalizeBlock}} or 
> {{GetCommittedBlockLength}}).
> # Writer 2 creates the same path with overwrite, writes and hsyncs.
> # The recoverer's commit arrives.
> No fault is needed. The window is the datanode phase of the recovery (not 
> measured). Writer 2 has to create and hsync inside it. With a create alone 
> the file table still names writer 1 and the recovery is correct.
> h3. Impact
> * The file is closed with writer 1's length (200 in the run) and an empty 
> block list. A reader gets 0 bytes. The 60 bytes that writer 2 had hsynced, 
> and that were readable just before, are gone.
> * Writer 2's block is in {{deletedTable}}. Deletion on the datanodes was not 
> waited for.
> * {{recoverLease}} returns true to the recoverer.
> * Writer 2's {{close()}} fails with {{KEY_NOT_FOUND}} ("entry is not found in 
> the OpenKey table").
> h3. Reproduction
> PASS, unmodified source. [^TestBugMC1RecoveryCommitClosesNewWriter.java] 
> ({{hadoop-ozone/integration-test}}) uses a mini cluster with one OM and three 
> datanodes and the FileSystem API only. {{ozone.om.lease.soft.limit}} is set 
> to 0s (default 60s), as in {{TestLeaseRecovery}}, so that the recovery does 
> not have to wait. The datanode fault injector that {{TestLeaseRecovery}} 
> already uses holds the recovery inside {{FinalizeBlock}} until the second 
> writer has overwritten the file and hsynced. It widens the window and changes 
> no state. The test logs the key before and after the recovery commit and then 
> asserts the wrong outcome, so a passing test means the defect is present.
> h3. Suggested fix
> The attached patch rejects a recovery commit that names a block the resolved 
> open key does not have. One case is left: a stale recovery commit with an 
> empty block list, that is writer 1 had hsynced before it wrote any data. 
> There is nothing to compare, the commit is accepted and closes writer 2's 
> file with no blocks (run as a temporary unit probe with the patch applied, 
> not included). Such a recovery has no datanode phase, so its window is one 
> client round trip (read from {{LeaseRecoveryClientDNHandler}}, the race was 
> not run for this case).
> Two other checks were considered:
> * Reject a recovery commit when the resolved open key does not carry 
> {{LEASE_RECOVERY}}. Not now: the soft limit check in {{RecoverLease}} uses 
> the local clock at apply (HDDS-16767, related), so one OM can lack the marker 
> that the others have, and that OM would reject a recovery commit the others 
> apply and stay different from them. Once HDDS-16767 makes the marker the same 
> on all OMs it is a useful additional guard. It narrows the empty list case, 
> but does not close it, and on its own it accepts the stale commit when a 
> third client has started a recovery of writer 2's file in between.
> * The exact fix: the client returns the writer id it got from 
> {{RecoverLease}} in the recovery commit and the OM rejects the commit when 
> the current writer is another one. This is the only one that closes the empty 
> list case. It is a protocol change that needs a version gate for clients and 
> OMs of different versions, so it is left for discussion.
> h3. Patch
> [^MC-1-reject-recovery-commit-with-block-not-in-open-key.patch], against 
> ea69b4a7d9abd040e5259dbcbb7c6ce52eb5d199. It also applies to master at 
> 53179f8f8b6.
> In {{OMKeyCommitRequestWithFSO.prepareCommit}} and 
> {{OMKeyCommitRequest.validateAndUpdateCache}} a commit with the recovery flag 
> is rejected with {{KEY_NOT_FOUND}} when it names a block that the resolved 
> open key does not have. The check runs before anything is modified, so the 
> second writer's file and open key stay as they are. The new 
> {{OmKeyInfo.hasAllBlocks}} compares blocks the way {{updateLocationInfoList}} 
> does (container id and local id, latest version) and the two share the code 
> that collects the open key's blocks. A block that is listed twice in the 
> request is not treated as unknown, because a recovering client can list its 
> last block twice. Commits without the recovery flag are not changed.
> Why a recovery that is not raced is not affected: every block in a recovery 
> commit comes from the {{RecoverLease}} response, which holds the file table 
> blocks and the last block of the open key, and the file table blocks of an 
> hsynced file are always blocks of its open key (read from the code, and the 
> existing suites below pass). OMs that agree on the open key's blocks decide 
> the same way, and the check does not read the {{LEASE_RECOVERY}} marker, so 
> an OM whose open key lacks it decides like the others. An OM that already 
> differs because of HDDS-16767 can lack a block the others have (it refused an 
> {{AllocateBlock}} that they applied). It rejects a recovery commit naming 
> that block, where without the patch it closes the file without the block 
> (read, not run). A retried commit that was already applied still gets 
> {{KEY_ALREADY_CLOSED}}, because that check comes first. The check also 
> rejects the stale commit when a third client has started a recovery of writer 
> 2's file in between. (That case needs the first recovery's commit to be still 
> outstanding when the third client's {{RecoverLease}} is accepted, that is the 
> third client uses {{force}}, or the new writer's open key has not been 
> modified for the lease soft limit, 60s by default.)
> The recoverer now gets an {{OMException}} with {{KEY_NOT_FOUND}} from 
> {{recoverLease}} (not a {{FileNotFoundException}}, only the {{RecoverLease}} 
> step maps to that). A caller that calls {{recoverLease}} again starts a 
> normal recovery of the second writer's file, subject to the soft limit.
> Compatibility: no new request, field or persisted state. An OM without the 
> change accepts a recovery commit that names a block the open key does not 
> have, and an OM with it rejects it, so OMs of two versions that apply the 
> same entry (rolling upgrade, or replay after a restart into the new version) 
> differ on that entry. This only affects an entry that hits this race, where 
> the old result is the data loss. No version gate was added. Whether one is 
> wanted is a question for review.
> Covered by {{testRecoveryCommitRejectedAfterOverwriteByAnotherWriter}} in the 
> existing {{TestOMRecoverLeaseRequest}} (with and without a second recovery in 
> between) and {{testRecoveryCommitChecksBlocksAgainstOpenKey}} in 
> {{TestOMKeyCommitRequest}} (inherited by the FSO subclass, so both commit 
> classes are covered; it also checks that a commit with only blocks of the 
> open key, one of them listed twice, is still accepted). Without the change 
> the four fail with "expected: <KEY_NOT_FOUND> but was: <OK>". With it the 
> lease recovery, key commit, allocate block, multipart commit and open key 
> cleanup unit suites and {{TestOmKeyInfo}} (161 tests) pass and checkstyle is 
> clean. On a mini cluster {{TestHSync}} (42) passes. In {{TestLeaseRecovery}} 
> (15) the only failures are in {{testGetCommittedBlockLengthTimeout}} and 
> {{testGetCommittedBlockLengthWithException}}. Which of them fail varies 
> between runs, they fail without the patch as well (the class is tagged flaky, 
> HDDS-11323), and the new error does not occur in the suite's output. With the 
> patch the reproduction fails at its first assertion, because {{recoverLease}} 
> is rejected, the file keeps the second writer's 60 bytes and the second 
> writer closes normally.
> Found by TLA+ model checking and code review of the lease recovery paths for 
> hsynced files under HDDS-15926, on commit 
> ea69b4a7d9abd040e5259dbcbb7c6ce52eb5d199. 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