[
https://issues.apache.org/jira/browse/HDDS-16439?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16439:
------------------------------
Description:
h3. Finding
A parent directory emptied during a scan is queued for expiry only in memory; a
crash after the cursor advances past it but before its deletion flushes strands
the emptied, rule-matching directory so storage/quota is not reclaimed and the
scan still marks complete, violating DirExpiryCompleteness. The external effect
is masked by the next-generation rescan that reclaims it; absent that rescan
the directory would remain stranded. Reachable through the FSO lifecycle scan
under a crash between committed child deletions and the parent's own deletion.
h3. Classification
* Verdict: MASKED
* Severity: High
* Source: Specula TLA+ model checking and confirmation debate, finding MC-2
h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula: v1.1.0 (commit c6aa3dfa)
Target: om-key-lifecycle
Guidance:
campaigns/ozone-9fbf9ee/targets/006-om-key-lifecycle/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=high --keep-original --max-parallel=2 \
--enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8
\
"om-key-lifecycle|apache/ozone|Java|Use the target-specific .prompt-extra.md"
{code}
Discovered under HDDS-16429 (Specula TLA+ verification effort). The TLA+
specification, counterexample, and confirmation debate live in the Specula run
artifacts.
Generated with Specula (Claude Opus 4.8).
h3. Steps to reproduce
# Enable a lifecycle policy on an FSO bucket with a nested tree, for example
Root containing dA (holds a key), dB (holds two empty subdirs dB1, dB2), and dC.
# The recursive scan pops siblings in descending name order. dB1 and dB2 are
found empty and their deletions are committed; dB is then re-evaluated, found
empty, and queued for its own deletion in the in-memory list, but not yet
committed.
# The scan proceeds to dA (which sorts below dB), commits dA's key deletions,
and persists the resume cursor lastScannedDir = dA on that key flush.
# The OM crashes (or loses leadership) before the queued deletion of dB is
flushed. The in-memory queue (deletedDirSet / dirList) is lost.
# On resume the scan restarts from the root. canSkipDir compares paths
lexicographically and skips any directory sorting above the cursor. dB sorts
above dA, so dB is skipped and never re-evaluated in that generation; the
emptied dB is left behind even though the scan reports the generation complete.
Masked: the next scan generation begins a fresh full scan (no cursor); it finds
dB genuinely empty in the directory table and reclaims it. The impact is
delayed reclamation of one generation rather than permanent retention, unless a
crash recurs at the same window every generation.
h3. Root cause
The resume cursor (lastScannedDir) is persisted on the key-deletion flush, but
a directory that has been decided empty and queued for its own deletion lives
only in the in-memory dirList / deletedDirSet until a later flush. Between
committing a lower-named directory's key deletions (which advances the
persisted cursor) and committing the queued higher-named parent's deletion, the
cursor certifies the parent as already scanned while its deletion is not yet
durable. A crash in that window, combined with canSkipDir skipping any path
above the cursor and the in-memory queue being lost, strands the emptied parent
for that scan generation.
h3. Suggested fix
The correct fix must ensure the persisted cursor never certifies a directory as
scanned before that directory's own deletion is durable: for example, advance
the persisted lastScannedDir only once the queued directory deletions up to
that point are committed, or persist the pending directory queue as part of the
scan state so it survives resume. This is a non-trivial change to the scan's
cursor and deletion ordering. Candidate minimal variants were evaluated and
rejected: attaching the cursor to the directory flush reads a stale
lastScannedDir (which is updated on every directory pop) and yields an
incorrect cursor, and flushing the queued directories at the mid-scan key-flush
point changes the delete-request sequence that the existing
fault-injector-based resume tests assert exactly. The fix is therefore deferred
to a focused change with its own review.
The attached patch [^HDDS-16439.001.patch] adds a targeted test that pins the
current resume behavior: an emptied, rule-matching directory whose path sorts
above the injected resume cursor is skipped and left un-reclaimed for that scan
generation. It documents the defect and the resume-skip path so a fix that
stops stranding the parent surfaces for review.
Patch is an AI-drafted proposal (Specula + Claude); pending human build, full
test, and review before merge.
was:
h3. Finding
A parent directory emptied during a scan is queued for expiry only in memory; a
crash after the cursor advances past it but before its deletion flushes strands
the emptied, rule-matching directory so storage/quota is not reclaimed and the
scan still marks complete, violating DirExpiryCompleteness. The external effect
is masked by the next-generation rescan that reclaims it; absent that rescan
the directory would remain stranded. Reachable through the FSO lifecycle scan
under a crash between committed child deletions and the parent's own deletion.
h3. Classification
* Verdict: MASKED
* Severity: High
* Source: Specula TLA+ model checking and confirmation debate, finding MC-2
h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula: v1.1.0 (commit c6aa3dfa)
Target: om-key-lifecycle
Guidance:
campaigns/ozone-9fbf9ee/targets/006-om-key-lifecycle/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=high --keep-original --max-parallel=2 \
--enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8
\
"om-key-lifecycle|apache/ozone|Java|Use the target-specific .prompt-extra.md"
{code}
Discovered under HDDS-16429 (Specula TLA+ verification effort). The TLA+
specification, counterexample, and confirmation debate live in the Specula run
artifacts.
Generated with Specula (Claude Opus 4.8).
> FSO lifecycle scan can strand an emptied directory after a crash before its
> deletion flushes
> --------------------------------------------------------------------------------------------
>
> Key: HDDS-16439
> URL: https://issues.apache.org/jira/browse/HDDS-16439
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Major
> Attachments: HDDS-16439.001.patch
>
>
> h3. Finding
> A parent directory emptied during a scan is queued for expiry only in memory;
> a crash after the cursor advances past it but before its deletion flushes
> strands the emptied, rule-matching directory so storage/quota is not
> reclaimed and the scan still marks complete, violating DirExpiryCompleteness.
> The external effect is masked by the next-generation rescan that reclaims it;
> absent that rescan the directory would remain stranded. Reachable through the
> FSO lifecycle scan under a crash between committed child deletions and the
> parent's own deletion.
> h3. Classification
> * Verdict: MASKED
> * Severity: High
> * Source: Specula TLA+ model checking and confirmation debate, finding MC-2
> h3. Reproduce
> {noformat}
> Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
> Specula: v1.1.0 (commit c6aa3dfa)
> Target: om-key-lifecycle
> Guidance:
> campaigns/ozone-9fbf9ee/targets/006-om-key-lifecycle/.prompt-extra.md
> {noformat}
> {code:none}
> specula run --agent=claude-code --effort=high --keep-original
> --max-parallel=2 \
> --enable-reviews --confirm-debate --tlc-memory-limit=28G
> --tlc-worker-limit=8 \
> "om-key-lifecycle|apache/ozone|Java|Use the target-specific
> .prompt-extra.md"
> {code}
> Discovered under HDDS-16429 (Specula TLA+ verification effort). The TLA+
> specification, counterexample, and confirmation debate live in the Specula
> run artifacts.
> Generated with Specula (Claude Opus 4.8).
> h3. Steps to reproduce
> # Enable a lifecycle policy on an FSO bucket with a nested tree, for example
> Root containing dA (holds a key), dB (holds two empty subdirs dB1, dB2), and
> dC.
> # The recursive scan pops siblings in descending name order. dB1 and dB2 are
> found empty and their deletions are committed; dB is then re-evaluated, found
> empty, and queued for its own deletion in the in-memory list, but not yet
> committed.
> # The scan proceeds to dA (which sorts below dB), commits dA's key deletions,
> and persists the resume cursor lastScannedDir = dA on that key flush.
> # The OM crashes (or loses leadership) before the queued deletion of dB is
> flushed. The in-memory queue (deletedDirSet / dirList) is lost.
> # On resume the scan restarts from the root. canSkipDir compares paths
> lexicographically and skips any directory sorting above the cursor. dB sorts
> above dA, so dB is skipped and never re-evaluated in that generation; the
> emptied dB is left behind even though the scan reports the generation
> complete.
> Masked: the next scan generation begins a fresh full scan (no cursor); it
> finds dB genuinely empty in the directory table and reclaims it. The impact
> is delayed reclamation of one generation rather than permanent retention,
> unless a crash recurs at the same window every generation.
> h3. Root cause
> The resume cursor (lastScannedDir) is persisted on the key-deletion flush,
> but a directory that has been decided empty and queued for its own deletion
> lives only in the in-memory dirList / deletedDirSet until a later flush.
> Between committing a lower-named directory's key deletions (which advances
> the persisted cursor) and committing the queued higher-named parent's
> deletion, the cursor certifies the parent as already scanned while its
> deletion is not yet durable. A crash in that window, combined with canSkipDir
> skipping any path above the cursor and the in-memory queue being lost,
> strands the emptied parent for that scan generation.
> h3. Suggested fix
> The correct fix must ensure the persisted cursor never certifies a directory
> as scanned before that directory's own deletion is durable: for example,
> advance the persisted lastScannedDir only once the queued directory deletions
> up to that point are committed, or persist the pending directory queue as
> part of the scan state so it survives resume. This is a non-trivial change to
> the scan's cursor and deletion ordering. Candidate minimal variants were
> evaluated and rejected: attaching the cursor to the directory flush reads a
> stale lastScannedDir (which is updated on every directory pop) and yields an
> incorrect cursor, and flushing the queued directories at the mid-scan
> key-flush point changes the delete-request sequence that the existing
> fault-injector-based resume tests assert exactly. The fix is therefore
> deferred to a focused change with its own review.
> The attached patch [^HDDS-16439.001.patch] adds a targeted test that pins the
> current resume behavior: an emptied, rule-matching directory whose path sorts
> above the injected resume cursor is skipped and left un-reclaimed for that
> scan generation. It documents the defect and the resume-skip path so a fix
> that stops stranding the parent surfaces for review.
> Patch is an AI-drafted proposal (Specula + Claude); pending human build, full
> test, and review before merge.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]