[
https://issues.apache.org/jira/browse/HDDS-17259?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-17259:
------------------------------
Affects Version/s: 2.2.1
2.0.0
1.4.0
2.1.2
2.1.0
1.4.1
2.1.1
2.2.0
> Compaction DAG is populated without the lock its readers take, so snapshot
> diff can miss keys and backup SST pruning can silently stop
> --------------------------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-17259
> URL: https://issues.apache.org/jira/browse/HDDS-17259
> Project: Apache Ozone
> Issue Type: Bug
> Affects Versions: 1.4.0, 2.0.0, 1.4.1, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
> Reporter: Siyao Meng
> Priority: Major
> Attachments:
> MC-1-synchronize-compaction-dag-population-with-dag-walks.patch,
> TestBugMC1DagDiffDuringCompactionListener.java
>
>
> h3. Mechanism
> {{RocksDBCheckpointDiffer.newCompactionCompletedListener}} calls
> {{CompactionDag.populateCompactionDAG}} inside {{synchronized (this)}}, but
> the block is in an anonymous {{AbstractEventListener}}, so it locks the
> listener object. Every DAG walk ({{getSSTDiffList}},
> {{internalGetSSTDiffList}}) and the DAG pruning tasks ({{pruneSstFiles}},
> {{pruneSstFileNodesFromDag}}) lock the differ instead. Nothing keeps them
> from running while the listener is adding a compaction to the DAG, a Guava
> {{MutableGraph}} that is not thread safe. The comment in {{pruneSstFiles}}
> shows the reader lock was meant to exclude the compaction thread.
> Two consequences follow.
> *SST file pruning.* {{pruneSstFiles}} streams over the forward DAG's nodes
> while the listener can add nodes. The scan can then throw
> {{ConcurrentModificationException}}. The task is scheduled with
> {{Scheduler.scheduleWithFixedDelay}} and has no exception handling, so the
> first such exception cancels it for good, and nothing is logged.
> *Snapshot diff.* {{populateCompactionDAG}} first adds the output node O, then
> the edges from O to each input, one at a time. If a walk runs after the edge
> to an input A that is in the older snapshot but before the edge to a newer
> input B, A counts as unchanged, B is never visited, the reachability check
> passes, and the result is a present but empty delta.
> {{CompositeDeltaDiffComputer}} falls back to a full diff only when the DAG
> walk returns no result (an empty {{Optional}}) or throws, so the keys that
> exist only in B are missing from the diff. If B's edge comes first, A is
> unreachable and the diff falls back to a full diff (safe). The edge order
> follows the {{HashMap}} order of the SST paths.
> h3. Trigger
> SST file pruning: a tracked compaction (an OM with at least one snapshot)
> completes while the {{pruneSstFiles}} scan runs. The scan runs every
> {{ozone.om.snapshot.compaction.dag.prune.daemon.run.interval}} (default 10m)
> and takes time proportional to the DAG size. No unusual scheduling is needed,
> only that overlap.
> Snapshot diff:
> # The older snapshot contains SST A.
> # A key k2 is written and flushed into SST B.
> # RocksDB compacts A and B into O; the completed listener starts adding O to
> the DAG.
> # A newer snapshot is created while the listener is between the edge to A and
> the edge to B.
> # A snapshot diff between the two reaches the DAG walk before the listener
> finishes.
> The unsafe window for the diff is only the in-memory edge loop of
> {{populateCompactionDAG}}. The part of the listener before it is safe (a node
> not yet in the DAG is read whole). So in practice the diff omission needs the
> compaction thread to be descheduled inside that loop for at least as long as
> creating a snapshot and starting a diff job. It was not observed without an
> injected pause.
> h3. Impact
> * Once the {{pruneSstFiles}} task is cancelled, backup SST files of compacted
> outputs are no longer removed from the compaction SST backup directory until
> the OM restarts or reopens its DB. Only the DAG pruning of entries older than
> {{ozone.om.snapshot.compaction.dag.max.time.allowed}} (default 30d) still
> removes them, so the directory holds up to that much compaction history on
> disk. Shown with a unit level probe against the real differ and its scheduler
> (prune interval 20 ms, 20000 compactions added to the DAG under a separate
> monitor in about 3 s): the task stopped on unmodified source and kept running
> with the patch. Not run on a cluster.
> * When the diff window is hit, the snapshot diff report silently omits the
> keys that were only in the newer input (CREATE k2 above). The finished job
> serves the stored report; nothing revalidates it. Exercised at the differ
> level only, not with an end to end report on a cluster.
> * Argued from code, not run:
> {{SnapshotDefragService.performIncrementalDefragmentation}} uses the same
> {{CompositeDeltaDiffComputer}}, so a defragmentation that hit the same window
> would build the snapshot without those keys.
> h3. Reproduction
> PASS on unmodified source, deterministic up to the edge order.
> [^TestBugMC1DagDiffDuringCompactionListener.java]
> ({{hadoop-hdds/rocksdb-checkpoint-differ}}) shows the diff omission. It uses
> a real RocksDB with the differ's real listeners, a real {{compactRange}} and
> real checkpoints, and issues the diff as {{RDBDifferComputer}} does. The only
> intervention is timing: a delegating proxy on the forward DAG holds the
> listener after its first edge until the diff has finished or is waiting.
> Every graph call is passed through unchanged. It repeats the scenario with
> shifted SST numbers until the omission order occurs. Output of the run on the
> unmodified source, round 1: {{diff during listener: present, files=[]
> keys=[]}}, then {{diff after listener: present, files=[000013] keys=[k2]}}. A
> passing test means the defect is present. The pruning consequence is covered
> by the regression test in the patch and the probe described above, not by
> this attachment.
> h3. Suggested fix
> Locking the differ in the listeners ({{synchronized
> (RocksDBCheckpointDiffer.this)}}) would also close both windows, but the
> differ monitor is held across blocking work: {{pruneSstFileValues}} rewrites
> backup SST files and acquires the bootstrap lock inside it, and RocksDB
> writes to the compaction log table happen under it. RocksDB compaction
> threads would then wait for that work. The attached patch uses a lock that
> only guards in-memory DAG operations instead.
> h3. Patch
> [^MC-1-synchronize-compaction-dag-population-with-dag-walks.patch], against
> b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master at
> d3b5f518556.
> {{CompactionDag.populateCompactionDAG}} and
> {{CompactionDag.pruneNodesFromDag}} become synchronized,
> {{RocksDBCheckpointDiffer.getSSTDiffList}} walks the DAG inside
> {{synchronized (compactionDag)}}, and the node scan in {{pruneSstFiles}}
> takes the same lock instead of the differ, which removes the
> {{ConcurrentModificationException}} that cancels the pruning task. A walk or
> scan then sees a compaction either not yet added or with all its edges.
> Holders of this lock only touch in-memory DAG structures, so the compaction
> thread waits at most for a walk or a prune in progress. No config, protocol
> or on disk change.
> Covered by two tests in the existing {{TestRocksDBCheckpointDiffer}} that
> hold {{populateCompactionDAG}} after the first edge.
> {{testGetSSTDiffListDuringCompactionDagPopulation}} runs {{getSSTDiffList}}
> meanwhile, for both edge orders: without the change the omission order
> returns an empty delta and the other order an empty {{Optional}}; with it
> both return the newer input.
> {{testPruneSstFilesDuringCompactionDagPopulation}} runs {{pruneSstFiles}}
> meanwhile: without the change (or with only the {{pruneSstFiles}} line
> reverted) the scan runs during the population; with it the scan waits and
> then prunes the backup file of the new output. With the change the tests of
> {{rocksdb-checkpoint-differ}} (123 run, 2 native library tests skipped), the
> OM differ users {{TestRDBDifferComputer}},
> {{TestCompositeDeltaDiffComputer}}, {{TestSnapshotDiffManager}} and
> {{TestOMSnapshotDirectoryMetrics}} (89), and the mini cluster suites
> {{TestOMSnapshotDAG}} and {{TestSnapshotBackgroundServices}} (6) pass, and
> checkstyle is clean. With the patch the reproduction fails, because the diff
> waits for the listener in every round and always contains k2.
> Found by TLA+ model checking and code review of the OM snapshot diff paths
> under HDDS-15926, on commit b10a7d427bdf83807a7e48a1be507d3b02d5a503. 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]