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

Siyao Meng updated HDDS-16443:
------------------------------
    Description: 
h3. Finding
On resubmit of a terminal job the cleanup service can purge the freshly-created 
live generation between the two persisted writes, deleting the active row and 
crashing the client's resubmit RPC with an uncaught NPE; harm is externally 
observable but bounded and client-retryable (retry is treated as a fresh 
submit), and the window is very small versus the default cleanup interval.

h3. Classification
* Verdict: REPRODUCED
* 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-snapshot-diff-jobs
Guidance:     
campaigns/ozone-9fbf9ee/targets/009-om-snapshot-diff-jobs/.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-snapshot-diff-jobs|apache/ozone|Java|Use the target-specific 
.prompt-extra.md"
{code}
Discovered under HDDS-16430 (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

1. Client submits a diff for (fromSnapshot, toSnapshot); it runs and then 
reaches a terminal state (CANCELLED via cancelSnapshotDiff, or FAILED/REJECTED).
2. Client resubmits the same diff. submitSnapshotDiff's resubmit branch calls 
createSnapDiffJob, which persists a new generation (fresh jobId) carrying the 
previous terminal status, then, in a separate write, calls updateJobStatus to 
move it to IN_PROGRESS.
3. SnapshotDiffCleanupService runs on its own thread, takes no 
SnapshotDiffManager lock, and purges any row whose status is 
FAILED/REJECTED/CANCELLED regardless of age. If it fires in the window between 
the two writes, it enqueues the new generation's jobId for purge and deletes 
the active row.
4. The submitting thread's updateJobStatus then finds no row and NPEs 
(dereferencing a null job); the catch handler updateJobStatusToFailed NPEs the 
same way. The resubmitted job is lost and the RPC crashes.

The window is small versus the default cleanup interval, so this is a 
low-probability but externally observable failure.

h3. Root cause

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java:
 on resubmit, createSnapDiffJob was called with the previous terminal status, 
so the freshly created generation was transiently persisted in an immediately 
purge-eligible state. The two persisted writes (create with terminal status, 
then transition to IN_PROGRESS) are not atomic with respect to the lock-free 
cleanup service, and updateJobStatus/updateJobStatusToFailed had no null-row 
guard.

h3. Suggested fix

Create the resubmitted generation as QUEUED, mirroring the fresh-submit path, 
so the row is never in an immediately purge-eligible state before it 
transitions to IN_PROGRESS (the transition then uses QUEUED as the expected 
prior status). Null-guard updateJobStatus and updateJobStatusToFailed so a row 
purged out from under them logs and returns instead of throwing. The added unit 
test verifies, via a spy, that the resubmit path creates the generation with 
QUEUED and never with a terminal status. See [^HDDS-16443.001.patch].

Patch is an AI-drafted proposal (Specula + Claude); pending human build, full 
test, and review before merge. A deterministic end-to-end reproduction of the 
race would require interposing the cleanup service between the two persisted 
writes, which shipping code does not expose; the test therefore pins the 
invariant that the resubmit generation is never created in a purge-eligible 
state.


  was:
h3. Finding
On resubmit of a terminal job the cleanup service can purge the freshly-created 
live generation between the two persisted writes, deleting the active row and 
crashing the client's resubmit RPC with an uncaught NPE; harm is externally 
observable but bounded and client-retryable (retry is treated as a fresh 
submit), and the window is very small versus the default cleanup interval.

h3. Classification
* Verdict: REPRODUCED
* 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-snapshot-diff-jobs
Guidance:     
campaigns/ozone-9fbf9ee/targets/009-om-snapshot-diff-jobs/.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-snapshot-diff-jobs|apache/ozone|Java|Use the target-specific 
.prompt-extra.md"
{code}
Discovered under HDDS-16430 (Specula TLA+ verification effort). The TLA+ 
specification, counterexample, and confirmation debate live in the Specula run 
artifacts.

Generated with Specula (Claude Opus 4.8).


> Snapshot diff cleanup can purge a freshly created generation on resubmit and 
> crash the client RPC
> -------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16443
>                 URL: https://issues.apache.org/jira/browse/HDDS-16443
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: HDDS-16443.001.patch
>
>
> h3. Finding
> On resubmit of a terminal job the cleanup service can purge the 
> freshly-created live generation between the two persisted writes, deleting 
> the active row and crashing the client's resubmit RPC with an uncaught NPE; 
> harm is externally observable but bounded and client-retryable (retry is 
> treated as a fresh submit), and the window is very small versus the default 
> cleanup interval.
> h3. Classification
> * Verdict: REPRODUCED
> * 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-snapshot-diff-jobs
> Guidance:     
> campaigns/ozone-9fbf9ee/targets/009-om-snapshot-diff-jobs/.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-snapshot-diff-jobs|apache/ozone|Java|Use the target-specific 
> .prompt-extra.md"
> {code}
> Discovered under HDDS-16430 (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
> 1. Client submits a diff for (fromSnapshot, toSnapshot); it runs and then 
> reaches a terminal state (CANCELLED via cancelSnapshotDiff, or 
> FAILED/REJECTED).
> 2. Client resubmits the same diff. submitSnapshotDiff's resubmit branch calls 
> createSnapDiffJob, which persists a new generation (fresh jobId) carrying the 
> previous terminal status, then, in a separate write, calls updateJobStatus to 
> move it to IN_PROGRESS.
> 3. SnapshotDiffCleanupService runs on its own thread, takes no 
> SnapshotDiffManager lock, and purges any row whose status is 
> FAILED/REJECTED/CANCELLED regardless of age. If it fires in the window 
> between the two writes, it enqueues the new generation's jobId for purge and 
> deletes the active row.
> 4. The submitting thread's updateJobStatus then finds no row and NPEs 
> (dereferencing a null job); the catch handler updateJobStatusToFailed NPEs 
> the same way. The resubmitted job is lost and the RPC crashes.
> The window is small versus the default cleanup interval, so this is a 
> low-probability but externally observable failure.
> h3. Root cause
> hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java:
>  on resubmit, createSnapDiffJob was called with the previous terminal status, 
> so the freshly created generation was transiently persisted in an immediately 
> purge-eligible state. The two persisted writes (create with terminal status, 
> then transition to IN_PROGRESS) are not atomic with respect to the lock-free 
> cleanup service, and updateJobStatus/updateJobStatusToFailed had no null-row 
> guard.
> h3. Suggested fix
> Create the resubmitted generation as QUEUED, mirroring the fresh-submit path, 
> so the row is never in an immediately purge-eligible state before it 
> transitions to IN_PROGRESS (the transition then uses QUEUED as the expected 
> prior status). Null-guard updateJobStatus and updateJobStatusToFailed so a 
> row purged out from under them logs and returns instead of throwing. The 
> added unit test verifies, via a spy, that the resubmit path creates the 
> generation with QUEUED and never with a terminal status. See 
> [^HDDS-16443.001.patch].
> Patch is an AI-drafted proposal (Specula + Claude); pending human build, full 
> test, and review before merge. A deterministic end-to-end reproduction of the 
> race would require interposing the cleanup service between the two persisted 
> writes, which shipping code does not expose; the test therefore pins the 
> invariant that the resubmit generation is never created in a purge-eligible 
> state.



--
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