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

Siyao Meng updated HDDS-17291:
------------------------------
    Attachment: TestBugMC1PrunerSnapshotDiffCheckpointDeadlock.java

> SST value pruner waits for the bootstrap lock while holding the differ 
> monitor, so OM bootstrap can deadlock with snapshot diff or defrag
> -----------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17291
>                 URL: https://issues.apache.org/jira/browse/HDDS-17291
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 2.2.0, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-1-take-bootstrap-lock-before-differ-monitor-in-sst-pruner.patch, 
> TestBugMC1PrunerSnapshotDiffCheckpointDeadlock.java, 
> TestBugMC2DefragPrunerCheckpointDeadlock.java
>
>
> h3. Mechanism
> {{RocksDBCheckpointDiffer.pruneSstFileValues}} enters {{synchronized (this)}} 
> for each compaction log entry, rewrites the entry's backup SST files and, 
> still inside the monitor, takes the BOOTSTRAP read lock (key 
> {{rocksdbCheckpointDifferAOS}}, supplied by {{OmMetadataManagerImpl.loadDB}}) 
> around the {{Files.move}} that replaces each file. Apart from 
> {{loadAllCompactionLogs}}, which runs once while the DB is opened and removes 
> orphaned backup files under the bootstrap read lock inside the monitor, every 
> other user of the two locks takes them in the opposite order: 
> {{pruneOlderSnapshotsWithCompactionHistory}} and {{SnapshotDefragService}} 
> take the bootstrap lock first, and a snapshot diff job holds SNAPSHOT_DB read 
> locks on both snapshots ({{SnapshotCache.get}}) when 
> {{RDBDifferComputer.computeDeltaFiles}} enters {{synchronized (differ)}}. The 
> differ monitor is not a tracked lock resource, so the DAG lock tracker does 
> not catch this.
> The checkpoint servlet ({{OMDBCheckpointServletInodeBasedXfer}}) write-locks 
> every BOOTSTRAP stripe through {{OMDBCheckpointServlet.Lock}} and holds them 
> while it collects the files. A follower bootstrap always requests snapshot 
> data, so in its last batch the servlet also write-locks every SNAPSHOT_DB 
> stripe through {{SnapshotCache.lock()}}. That gives two cycles:
> * *Snapshot diff (default configuration).* The pruner holds the monitor and 
> waits for BOOTSTRAP read, which the servlet holds. The snapshot diff job 
> holds SNAPSHOT_DB read and waits for the monitor. The servlet holds BOOTSTRAP 
> write and waits for SNAPSHOT_DB write.
> * *Defrag (non default stripe count).* {{SnapshotDefragService}} holds 
> BOOTSTRAP read with key {{SnapshotDefragService}} and waits for the monitor 
> in {{RDBDifferComputer.computeDeltaFiles}}. The pruner holds the monitor and 
> waits for BOOTSTRAP read on its own stripe. The servlet, which takes the 
> stripes in ascending order, has already taken the pruner's stripe (or is 
> queued on it) and waits for the defrag stripe. This needs the pruner's stripe 
> at or below the defrag stripe. Both keys are constants, so the outcome is 
> fixed by {{ozone.om.lock.stripes.bootstrap_lock}}: no cycle at the default 
> 512 (or 32, 1024), a cycle at 1, 2, 4, 8, 16, 64, 128, 256 and 2048 to 32768 
> (computed from the Guava stripe mapping; other values round up to the next 
> power of two, so 100 behaves like 128). That key is not documented.
> h3. Trigger
> Both cycles need the rocks-tools native library, which Linux release builds 
> include (built with {{-Drocks_tools_native}}) and the OM loads by default; 
> without it the pruner is not scheduled. The 2.2.1 library does not load on 
> RHEL or Rocky Linux 9, including the {{apache/ozone:2.2.1}} image 
> (HDDS-17153, fixed in 2.3.0), so neither cycle forms on those hosts. 
> Snapshots must exist and a tracked column family must have been compacted 
> since, so that the prune queue is not empty.
> For the snapshot diff cycle a follower must be bootstrapping from the leader, 
> because only a follower bootstrap requests snapshot data and so makes the 
> servlet take {{SnapshotCache.lock()}}. The pruner's periodic run (every 10 
> minutes by default) has to start while the servlet holds the bootstrap lock, 
> or be in its rewrite when the servlet arrives. Either way the pruner ends up 
> parked on the bootstrap lock while holding the monitor. A snapshot diff job 
> that reaches its delta file computation before the servlet reaches 
> {{SnapshotCache.lock()}} then closes the cycle. The servlet holds the 
> bootstrap lock for its whole collection phase, so this window is as long as 
> copying the snapshot directories (argued from the code; the reproduction 
> orders the steps with a latch).
> For the defrag cycle, {{ozone.snapshot.defrag.service.interval}} must be 
> enabled (it is off by default) and the stripe count must be one of the values 
> above. Any request that takes the BOOTSTRAP write lock is enough, not only a 
> follower bootstrap: Recon's OM DB fetch (which does not request snapshot 
> data) and the v1 {{/dbCheckpoint}} servlet take the same lock.
> h3. Impact
> All waits are untimed lock or monitor waits, and the JVM's deadlock detection 
> does not report them because read locks have no owner. Only an OM restart 
> recovers. On the leader:
> * the checkpoint request never returns, so the follower cannot finish its 
> bootstrap; a retry queues behind the same lock;
> * every later user of the BOOTSTRAP read lock blocks behind the servlet: the 
> key, directory and snapshot deleting services when they submit purges, 
> {{SstFilteringService}}, {{SnapshotDefragService}} and the differ's pruning 
> tasks, so space is no longer reclaimed;
> * later snapshot diff jobs block on the differ monitor. Snapshot reads and 
> diffs whose snapshots map to the SNAPSHOT_DB stripes the servlet has already 
> taken also block (argued, not run).
> In the defrag cycle the servlet waits at the defrag stripe, so only bootstrap 
> readers on that stripe and the stripes below it block, and snapshot reads are 
> not affected. The requester (follower or Recon) never gets its checkpoint, 
> and defrag, the differ's pruning tasks (they share the pruner's thread) and 
> later snapshot diff jobs hang as above.
> Released versions 2.2.0 and 2.2.1 have the same code (read from the tags). 
> 2.1.x has the nesting, but its servlet does not take 
> {{SnapshotCache.lock()}}, so the snapshot diff cycle does not form there.
> h3. Reproduction
> Two self contained classes for {{hadoop-ozone/ozone-manager}} (target path 
> and Maven command in the class comment). Both pass on unmodified source when 
> the bug is present. They use the real {{OzoneManagerLock}}, the OM DB and 
> differ built by {{OmMetadataManagerImpl.loadDB}} (so the production lock 
> supplier), a prune queue entry produced by a real compaction of keyTable with 
> a snapshotInfoTable row present, {{pruneSstFileValues}}, 
> {{RDBDifferComputer.computeDeltaFiles}} and the servlet's 
> {{OMDBCheckpointServlet.Lock}}. The native SST reader is mocked as in 
> {{TestRocksDBCheckpointDiffer}}, so they run without the native library, and 
> the snapshot local data provider is mocked. The servlet request, the snapshot 
> diff job and the defrag task are replayed at their lock steps, not run end to 
> end through HTTP or the services.
> * [^TestBugMC1PrunerSnapshotDiffCheckpointDeadlock.java]: default 
> configuration. A latch in the mocked SST scan keeps the pruner in its rewrite 
> until the snapshot diff thread is blocked on the monitor and the servlet 
> thread waits in {{SnapshotCache.lock()}}. After the latch opens none of the 
> three finishes; the pruner waits on the lock owned by the servlet thread, the 
> snapshot diff thread on the monitor owned by the pruner, and a later 
> bootstrap read (key {{KeyDeletingService}}) blocks too.
> * [^TestBugMC2DefragPrunerCheckpointDeadlock.java]: defrag cycle with the 
> bootstrap lock of a real {{SnapshotDefragService}}, parameterized over 2, 256 
> and 2048 stripes (all three threads stuck, defrag blocked on the monitor 
> owned by the pruner) and a control at the default 512 (all three complete).
> Not exercised: a real follower bootstrap over HTTP, a real snapshot diff or 
> defrag run, and the real native library.
> h3. Patch
> [^MC-1-take-bootstrap-lock-before-differ-monitor-in-sst-pruner.patch], 
> against e0de543cbe9d73f3bf5473a45e1c59cd6eae7173. It also applies to master 
> at 1ff29ee6b86.
> {{pruneSstFileValues}} now takes the BOOTSTRAP read lock for each compaction 
> log entry before entering {{synchronized (this)}}, instead of around 
> {{Files.move}} inside it. The pruner then never waits for the bootstrap lock 
> while holding the monitor, which removes the edge both cycles need, at any 
> stripe count. The move is still done under the bootstrap read lock, so a 
> checkpoint that holds the write lock still cannot see a backup SST being 
> replaced (the protection added by HDDS-12501 and HDDS-13905). The rest of the 
> method is unchanged apart from indentation ({{git diff -w}} shows only the 
> real change).
> The cost is a longer wait for a bootstrap. The lock now covers one compaction 
> log entry, whose input file list can be long after a large or manual 
> compaction, and the pruner holds it while it waits for the differ monitor, so 
> a bootstrap that arrives then also waits for any snapshot diff or defrag 
> already in the monitor. While the servlet waits on the pruner's stripe it 
> holds the stripes below it, so bootstrap readers on those stripes wait as 
> well. The servlet is not starved: the pruner releases the lock between 
> entries, and with the default nonfair lock its next read waits behind the 
> queued writer.
> After the patch {{loadAllCompactionLogs}} is the only path left that takes 
> the monitor and then the bootstrap lock (through 
> {{cleanupOrphanedSstBackupFiles}}, added by HDDS-14742 after 2.2.1). It 
> cannot deadlock: it runs once in the {{RDBStore}} constructor, before the 
> pruner's first run (scheduled one interval after start) and before this OM 
> serves checkpoint requests. The patch leaves it unchanged.
> New test 
> {{TestRocksDBCheckpointDiffer#testPruneSstFileValuesWaitsForBootstrapLockOutsideDifferMonitor}}:
>  while another thread holds the bootstrap write lock it checks that the 
> backup SST file is unchanged and that a snapshot diff style {{synchronized 
> (differ)}} can proceed, then that the file is pruned once the lock is 
> released. It fails without the change. With it the 
> {{rocksdb-checkpoint-differ}} module tests (121), the related 
> {{ozone-manager}} unit tests (141, including {{TestSnapshotDefragService}}, 
> {{TestSnapshotDiffManager}}, {{TestRDBDifferComputer}}) and 
> {{TestOMDbCheckpointServletInodeBasedXfer}} (15) pass, and both attached 
> reproductions no longer show the deadlock.
> 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-13905 (resolved, made background bootstrap locks read locks 
> and kept this nesting), HDDS-9486 (resolved, an earlier pruner and checkpoint 
> deadlock), HDDS-16685 (snapshot diff takes SNAPSHOT_DB stripes out of order 
> against the same servlet step) and HDDS-17259 (the compaction listener 
> synchronizes on the wrong object in the same class). 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