Siyao Meng created HDDS-17292:
---------------------------------

             Summary: OMKeyRequest takes a second BUCKET lock while holding 
one, out of stripe order, which can deadlock the OM apply thread
                 Key: HDDS-17292
                 URL: https://issues.apache.org/jira/browse/HDDS-17292
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-5-release-bucket-lock-before-resolving-source-bucket.patch

h3. Mechanism
{{OMKeyRequest.generateRequiredEncryptionInfo}} takes the BUCKET read lock of 
the requested bucket and reads its {{OmBucketInfo}}. When that bucket points to 
a source bucket, it calls {{OzoneManager.resolveBucketLink}} while it still 
holds that lock, and the resolution takes the BUCKET read lock of the source 
bucket in {{BucketManagerImpl.getBucketInfo}}. 
{{OMKeyRequest.getFileEncryptionInfoForMpuKey}} does the same: it takes the 
read lock of the requested bucket first and calls {{resolveBucketLink}} inside 
it. Both run on the RPC handler thread, and both locks are stripes of the same 
{{BUCKET_LOCK}} of {{OzoneManagerLock}}. So the handler holds one stripe and 
waits for a second one, in whatever order the two stripes happen to be.

On the Ratis apply thread, {{OMKeyPurgeRequest.updateBucketSize}}, 
{{OMDirectoriesPurgeRequestWithFSO}} and 
{{S3ExpiredMultipartUploadsAbortRequest}} write lock all buckets of one request 
with {{IOzoneManagerLock.acquireWriteLocks}}, which takes the stripes in 
ascending order ({{Striped.bulkGet}}). That order prevents a cycle between two 
bulk holders, but not between a bulk holder and a thread that nests two single 
acquisitions in the opposite order.

h3. Trigger
Two lock holders in opposite stripe order, with stripe(B) below stripe(A):
# An RPC handler, in one of the two methods above, holds the BUCKET read lock 
stripe of bucket A and waits for the read lock stripe of B, the source of A.
# The apply thread, in a bulk acquire over a set of buckets with one bucket on 
stripe(B) and one on stripe(A), holds stripe(B) for write and waits for 
stripe(A) for write.

Neither wait has a timeout, and nothing else releases either lock.

h3. Impact
* The apply thread of the OM leader waits forever, so no later write is applied 
on that OM and every write waits for its reply.
* Each such write keeps its RPC handler while it waits in 
{{OzoneManagerRatisServer.submitRequest}} for the reply, which only comes after 
the apply thread has applied it. Reads on the leader do not go through the 
apply thread by default ({{ozone.om.ha.raft.server.read.option}} is 
{{DEFAULT}}), but they share the handler pool ({{ozone.om.handler.count.key}}), 
so once all handlers are held by waiting writes, reads wait in the call queue 
too. Reads that need a bucket on one of the two stripes block on the lock 
directly, since one stripe is write held and the other has a queued writer 
(code reading).
* The followers keep applying the log, because the nested acquisition only runs 
on the leader before submission. The leader stays leader, so the hang lasts 
until that OM is restarted (argued, restart not run). No data is lost.

By code reading at the release tags, not run on older code: the nested 
acquisition is present from 1.2.0 (HDDS-5530). Before 1.4.0 each bucket has its 
own BUCKET lock (no striping), and the cycle would need the apply thread to 
write lock the requested bucket itself, which does not appear in a purge. From 
1.4.0 the BUCKET locks are striped (HDDS-8765) and 
{{OMDirectoriesPurgeRequestWithFSO}} holds several bucket write locks taken one 
at a time, and from 2.1.0 {{OMKeyPurgeRequest}} and 
{{OMDirectoriesPurgeRequestWithFSO}} use the sorted bulk acquire.

h3. Reproduction
No standalone reproduction is attached.

The regression test in the patch sets up the conflicting holder through 
internal calls. 
{{TestOMKeyCreateRequest.preExecuteReleasesBucketLockBeforeResolvingSourceBucket}}
 ({{hadoop-ozone/ozone-manager}}, also in the inherited {{...WithFSO}}, for 
both methods above) stubs {{resolveBucketLink}} so that, while the source 
bucket is resolved, another thread write locks the requested bucket and its 
source with {{acquireWriteLocks}}, as the apply thread does. That thread must 
complete within 5 seconds. Without the change all four cases fail with 
{{Expecting <CompletableFuture[Incomplete]> to be completed within 5S}}, 
because the resolving thread still holds the read lock of the requested bucket.

h3. Patch
[^MC-5-release-bucket-lock-before-resolving-source-bucket.patch], against 
e0de543cbe9d73f3bf5473a45e1c59cd6eae7173. It also applies to master at 
1ff29ee6b86.

{{generateRequiredEncryptionInfo}} now releases the read lock of the requested 
bucket before it calls {{resolveBucketLink}}, and then reads the resolved 
bucket as before. {{getFileEncryptionInfoForMpuKey}} calls 
{{resolveBucketLink}} before it takes the read lock. {{resolveBucketLink}} 
takes and releases the lock of each bucket it reads on its own, so on these 
paths the handler now holds at most one BUCKET lock at a time and the cycle 
cannot form. The locks taken and the data read are otherwise the same: the 
resolved bucket was not read under its own lock on this path before either, and 
in both versions the lock is released before the request is submitted. Nothing 
changes for a bucket that does not point to another bucket, and there is no 
wire or protocol change.

With the patch the OM request unit suites that cover these two methods and the 
purge requests pass (593 tests in {{hadoop-ozone/ozone-manager}}, 1 skipped as 
before), as does {{TestOzoneAtRestEncryption}} on a mini cluster, and 
checkstyle on {{hadoop-ozone/ozone-manager}} is clean.

Found by TLA+ model checking and code review of the Ozone Manager lock ordering 
paths under HDDS-15926, on commit e0de543cbe9d73f3bf5473a45e1c59cd6eae7173. 
Checked against HDDS issues and apache/ozone pull requests for duplicates 
before filing. Related but not duplicates: HDDS-13729 (sorted bulk bucket locks 
in the directory purge), HDDS-7755 (an earlier OM hang from a leaked lock, 
HDDS-7925 is its duplicate). 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