Siyao Meng created HDDS-17293:
---------------------------------
Summary: OM double buffer flush of SnapshotMoveTableKeys can
deadlock with the DB checkpoint servlet on snapshot DB lock stripes, stopping
all OM writes
Key: HDDS-17293
URL: https://issues.apache.org/jira/browse/HDDS-17293
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-3-lock-snapshot-db-stripes-in-order-for-move-table-keys.patch,
TestBugMC3MoveTableKeysCheckpointDeadlock.java
h3. Mechanism
{{OMSnapshotMoveTableKeysResponse.addToDBBatch}} runs on the double buffer
flush thread. It opens the from snapshot and then, still holding it, the next
snapshot, both through {{OmSnapshotManager.getSnapshot}}. Each
{{SnapshotCache.get}} takes a read lock on the one {{SNAPSHOT_DB_LOCK}} stripe
its snapshot ID hashes to, so the two stripes are locked in caller order.
{{SnapshotCache.lock()}}, which {{OMDBCheckpointServletInodeBasedXfer}} calls
when the request includes snapshot data, write locks every stripe in ascending
index order. When the from snapshot is on the higher stripe, the servlet takes
the lower stripe and waits for the higher one, while the flush thread holds the
higher one and waits for the lower one. Neither wait has a timeout.
The flush thread takes no {{BOOTSTRAP_LOCK}}, and
{{OMDBCheckpointServlet.Lock.acquireWriteLock}} calls
{{awaitDoubleBufferFlush}} before it takes the bootstrap write lock, so a
SnapshotMoveTableKeys batch applied in between can still be flushing when the
servlet sweeps the stripes. SnapshotDeletingService holds the bootstrap read
lock for its whole run, so only such a batch, applied before the servlet gets
the bootstrap write lock, can take part. The response already takes its
{{SNAPSHOT_DB_CONTENT_LOCK}} locks for both snapshots in one ordered
{{acquireReadLocks}} call, but not the {{SNAPSHOT_DB_LOCK}} locks.
This is the same stripe order inversion that HDDS-16685 describes for the
snapshot diff callers, at a different call site. Here the blocked thread is the
flush thread, so the impact is on every write.
h3. Trigger
# SnapshotDeletingService moves the keys of a deleted snapshot into the next
active snapshot (SnapshotMoveTableKeys with a next snapshot).
# A follower bootstraps from the leader at the same time. The default follower
download uses {{/v2/dbCheckpoint}} with snapshot data, so the leader's servlet
calls {{SnapshotCache.lock()}}.
# The flush of that SnapshotMoveTableKeys batch has opened the from snapshot
when the servlet's stripe sweep has passed the next snapshot's stripe but not
the from snapshot's stripe.
For a given pair the stripe order is fixed, so about half of all (from, next)
pairs can hit it. Whether the timing lines up in a real bootstrap depends on
how far the flush thread lags behind and on how long the servlet spends
collecting snapshot files before it takes the cache lock. I did not run a real
follower bootstrap.
h3. Impact
The flush thread never finishes the batch, so its unflushed transaction permits
are not returned. Once {{ozone.om.unflushed.transaction.max.count}}
transactions (default 10000) are applied,
{{OzoneManagerStateMachine.applyTransaction}} blocks and no further OM write
completes. The servlet keeps the bootstrap write lock, so the background
services that take the bootstrap read lock stop too, and the follower bootstrap
never finishes. The leader stays leader.
{{ThreadMXBean.findDeadlockedThreads()}} does not report the cycle because read
holds have no owner. Only a restart of the OM recovers.
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC3MoveTableKeysCheckpointDeadlock.java]
({{hadoop-ozone/ozone-manager}}) uses the real lock manager, snapshot cache,
snapshot manager and double buffer, with snapshots made by the real create
request. A test thread does what the servlet does around the cache lock
({{awaitFlush}}, bootstrap write lock, {{SnapshotCache.lock()}}). The only
timing help is a pause of the flush thread at the start of the nested
{{getSnapshot(next)}} until the servlet thread is parked in its sweep. In each
run the test picks a pair with the from snapshot on the higher stripe (for
example 303 and 203), and the servlet stays parked in {{SnapshotCache.lock()}},
the flush never completes, and with a permit limit of 2 only 1 later apply is
admitted while the next one blocks. A passing test means the defect is present.
h3. Patch
[^MC-3-lock-snapshot-db-stripes-in-order-for-move-table-keys.patch], against
e0de543cbe9d73f3bf5473a45e1c59cd6eae7173. It also applies to master at
1ff29ee6b86.
{{addToDBBatch}} now takes the {{SNAPSHOT_DB_LOCK}} read locks of both
snapshots with one {{acquireReadLocks}} call, which locks the stripes in
ascending order, right after the content locks, and releases them in the same
{{finally}}. The two {{getSnapshot}} calls then reacquire read locks the thread
already holds, which {{ReentrantReadWriteLock}} grants even when a writer is
queued. The flush thread and the servlet now lock stripes in the same order.
Covered by {{testMoveTableKeysDoesNotDeadlockWithSnapshotCacheLock}} in the
existing {{TestOMSnapshotMoveTableKeysResponse}}. It pauses the move before it
opens the next snapshot, lets {{SnapshotCache.lock()}} start and wait, then
lets the move continue, for both orders of two snapshots on different stripes.
Without the change the move does not complete within 30s in one of the two
orders. With it the snapshot request and response suites,
{{TestSnapshotCache}}, {{TestOmSnapshotManager}}, {{TestOzoneManagerLock}},
{{TestMultiSnapshotLocks}}, {{TestOzoneManagerDoubleBuffer}} and
{{TestSnapshotDeletingService}} (232 tests) pass and checkstyle is clean. With
the patch the reproduction fails because the servlet thread finishes.
{{OMSnapshotMoveDeletedKeysResponse}} has the same nesting. Nothing at this
commit submits that request (SnapshotDeletingService sends
SnapshotMoveTableKeys, and the request is rejected from clients), so it is only
applied when replaying log entries written by older OMs. The patch leaves it
unchanged.
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. 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]