Siyao Meng created HDDS-17739:
---------------------------------
Summary: Ordered snapshot diff: path reuse skip in the dependency
graph returns a non replayable order or a false cycle
Key: HDDS-17739
URL: https://issues.apache.org/jira/browse/HDDS-17739
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-2-MC-3-order-modify-by-from-snapshot-paths-only.patch,
TestBugMC2ReoccupiedPathOrderedReplay.java,
TestBugMC3NestedRecreateFalseCycle.java
h3. Mechanism
{{SnapDiffDependencyGraph}} orders the entries of an FSO snapshot diff when the
client asks for an ordered report. Among its edges,
{{addPathPrefixToSnapshotEdges}} puts a CREATE or RENAME target on an ancestor
path of the newer snapshot before every entry below it. To avoid false cycles
for a MODIFY reported at an old path that a new object now occupies,
{{shouldSkipReoccupiedPathToSnapshotEdge}} drops that edge when the ancestor
path is in the set built by {{buildReoccupiedPaths}} (paths that are a DELETE
path or a RENAME source and are also a CREATE path or RENAME target). Two
things are wrong with that skip:
# It is applied to RENAME entries too, and decided by the RENAME's source path.
The edges come from the prefixes of the RENAME's target path, and a RENAME
target below a reoccupied path is by construction below the new occupier, so
the edge is always needed. Dropping it hides a real cycle and the graph returns
an order that does not replay.
# For MODIFY it only treats a path as freed when that exact path was deleted or
renamed away. A path freed because an ancestor was renamed or deleted is not in
the set (a DELETE below a deleted directory is not reported), so the edge is
kept and closes a cycle although a valid order exists.
h3. Trigger
Optimized snapshot diff enabled ({{ozone.om.snapshot.diff.optimized.enabled}},
default false, not in any release yet), an FSO bucket, and a snapshot diff
requested with the ordered flag.
* Wrong order: {{mkdir a; create a/a; snapshot 1; mv a b; mkdir a; mv b/a a/a;
snapshot 2}}. The report entries are {{RENAME a/a -> a/a}}, {{RENAME a -> b}},
{{CREATE a}}. The file rename needs the old {{a}} as its source parent and the
new {{a}} as its target parent, so no order of these entries replays.
* False cycle: {{mkdir -p a/a; create a/a/a; snapshot 1; mv a b; mkdir -p a/a;
overwrite b/a/a; snapshot 2}}. The entries are {{MODIFY a/a/a}}, {{RENAME a ->
b}}, {{CREATE a}}, {{CREATE a/a}}, and that order replays (the MODIFY is
reported at its path in snapshot 1).
h3. Impact
* Wrong order: the job ends DONE with the reason "Report is dependency
ordered." and the order {{RENAME a/a -> a/a, RENAME a -> b, CREATE a}}.
Applying it to a copy of snapshot 1 through the file system leaves the file at
{{b/a}}, while snapshot 2 has it at {{a/a}}. A client that relies on the
ordered label to replay the report gets a different namespace without any error.
* False cycle: the ordered job ends FAILED with "Dependency graph cycle for job
...". The diff is deterministic, so every ordered request for the pair
recomputes the diff and fails again. An unordered request returns the report.
h3. Reproduction
Two self contained mini cluster classes,
[^TestBugMC2ReoccupiedPathOrderedReplay.java] and
[^TestBugMC3NestedRecreateFalseCycle.java] (target path and Maven command in
each class comment). They enable the optimized diff with a forced full diff and
native diff disabled, use only public client calls, and pass on unmodified
source when the bug is present. The first replays the ordered report through
the Ozone file system on a second bucket that holds the snapshot 1 tree and
compares it with snapshot 2. The second shows the ordered job FAILED and the
unordered job DONE with the entries above.
h3. Patch
[^MC-2-MC-3-order-modify-by-from-snapshot-paths-only.patch], against
d3b75c75bf9ad475551202477c3a21a3f4b968b7. It also applies to master at
bbc8505e00d.
Both symptoms come from the same skip, so one change fixes both. MODIFY entries
no longer get edges from ancestor paths of the newer snapshot, and the
reoccupied path skip ({{buildReoccupiedPaths}},
{{shouldSkipReoccupiedPathToSnapshotEdge}}) is removed, so CREATE and RENAME
always keep those edges. A MODIFY is reported at its path in the older snapshot
({{SnapDiffDependencyEntry}} contract, followed by
{{MergeJoinSnapDiffWriter}}), which exists before any new ancestor is created,
and it is still ordered by the older snapshot edges (before an ancestor DELETE
or RENAME source). Keeping the skip for MODIFY and extending it to paths freed
through an ancestor would fire for every MODIFY with a new occupier above it,
which is what the patch states directly.
Covered in the existing {{TestSnapDiffDependencyGraph}}:
{{testRenameSourceReuseWithDescendantRenameIntoNewOccupierDetectsCycle}} (first
trigger, expects a cycle) and
{{testRenameSourceReuseByNestedCreateWithDescendantModify}} (second trigger,
expects the replayable order), and
{{testDeleteAndRecreateNestedPathWithDescendantModifyAndRename}} (the second
defect with a deleted ancestor: {{DELETE B, MODIFY B/A/B, RENAME B/A/B -> A,
CREATE B, CREATE B/A}}, a false cycle at this commit, expects {{MODIFY B/A/B,
RENAME B/A/B -> A, DELETE B, CREATE B, CREATE B/A}}).
{{testDeleteAndRecreateWithOldTreeChildModifyAndRename}} asserted an order for
the first defect's shape with a deleted ancestor ({{DELETE A, CREATE A, RENAME
A/child -> A/renamed, MODIFY A/child}}), which replays the rename inside the
old {{A}} and then deletes it; it now expects a cycle. Comments of four
existing tests that described the removed skip are updated. Without the change
the four tests above fail; with it {{TestSnapDiffDependencyGraph}},
{{TestMergeJoinSnapDiffWriter}} and {{TestSnapshotDiffManager}} pass (124
tests), the existing one level reuse cases that the skip was added for still
pass, and checkstyle is clean. With the patch the first reproduction ends
FAILED with "Dependency graph cycle" instead of a wrong ordered report, and the
second ends DONE "Report is dependency ordered." with {{MODIFY a/a/a, RENAME a
-> b, CREATE a, CREATE a/a}}. {{TestOptimizedSnapshotDiff}} needs the native
library and was not run.
Behaviour change: a real cycle still fails the ordered job at this commit. With
this patch alone, diffs like the first trigger (a common directory rotation
such as {{mv a b; mkdir a; mv b/x a/x}}) and the shape of the changed test go
from a wrong DONE ordered report to a FAILED ordered job on every ordered
request. HDDS-17738 makes them return an unordered report instead, so this
patch should land together with or after HDDS-17738. With both patches the
first reproduction ends DONE "Report is dependency unordered.".
Found by TLA+ model checking and code review of the OM optimized snapshot diff
paths under HDDS-15926, on commit d3b75c75bf9ad475551202477c3a21a3f4b968b7.
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
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]