[
https://issues.apache.org/jira/browse/HDDS-16442?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16442:
------------------------------
Description:
h3. Finding
After a routine OM upgrade, every pre-existing DONE snapshot-diff job
deserializes with a null largestEntryKey, so the first client read of the
report trips the integrity check and hits an uncaught NPE (wrong-key
updateJobStatus) instead of the designed retriable error; the report is intact
yet permanently unreadable and the job is stuck DONE (not resubmittable) until
the retention TTL GC, affecting all old jobs broadly rather than via a rare
race.
h3. Classification
* Verdict: REPRODUCED
* Severity: Critical
* Source: Specula TLA+ model checking and confirmation debate, finding CR-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
Precondition (reachable through a routine upgrade): a snapshot diff job for
(fromSnapshot, toSnapshot) completed and was persisted as DONE by an OM build
that predates the largestEntryKey field on SnapshotDiffJob. After the OM is
upgraded, that job deserializes with largestEntryKey == null and stays in the
active job table (loadJobsOnStartUp leaves DONE jobs untouched), with its
report rows fully intact.
1. Client reads the completed diff: getSnapshotDiffReport routes the DONE job
to createPageResponse.
2. On the last page, checkReportsIntegrity sees largestEntryKey == null and
treats it as an integrity failure.
3. The recovery path calls updateJobStatus(diffJob.getJobId(), DONE, FAILED).
The job table is keyed by the dedup jobKey (fromSnapshotId + DELIMITER +
toSnapshotId), not by the per-generation jobId, so the lookup returns null and
dereferencing its status throws NullPointerException.
4. The designed retriable IOException ("Report integrity check failed. Retry
after: ...") is never thrown, the DONE to FAILED transition never lands, and
the job is stuck DONE (not a resubmittable status), so the client cannot
recover until the report-retention TTL GC. Every read repeats the NPE.
The same NPE occurs for any genuine last-page integrity mismatch (non-null
largestEntryKey that disagrees with the report tail), since the recovery call
always passes the wrong key.
h3. Root cause
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java:
checkReportsIntegrity called updateJobStatus(diffJob.getJobId(), DONE,
FAILED), passing the jobId where updateJobStatus expects the dedup jobKey (it
does snapDiffJobTable.get(jobKey)). Two problems compound: (1) a null
largestEntryKey (legacy DONE job) is treated as a failure instead of a "no
stored last entry" value, and (2) updateJobStatus has no null guard, so the
wrong-key lookup NPEs on getStatus().
h3. Suggested fix
Thread the dedup jobKey down through createPageResponse into
checkReportsIntegrity and pass it to updateJobStatus. Treat a null
largestEntryKey on a DONE job as a legacy value and skip the last-page
integrity assertion so upgraded jobs stay readable. Null-guard updateJobStatus
so a missing row logs and returns instead of throwing. Regression tests cover
both the legacy-null (stays readable) and genuine-mismatch (flips to FAILED via
jobKey, throws the retriable IOException) cases. See [^HDDS-16442.001.patch].
Patch is an AI-drafted proposal (Specula + Claude); pending human build, full
test, and review before merge.
was:
h3. Finding
After a routine OM upgrade, every pre-existing DONE snapshot-diff job
deserializes with a null largestEntryKey, so the first client read of the
report trips the integrity check and hits an uncaught NPE (wrong-key
updateJobStatus) instead of the designed retriable error; the report is intact
yet permanently unreadable and the job is stuck DONE (not resubmittable) until
the retention TTL GC, affecting all old jobs broadly rather than via a rare
race.
h3. Classification
* Verdict: REPRODUCED
* Severity: Critical
* Source: Specula TLA+ model checking and confirmation debate, finding CR-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).
> After OM upgrade, pre-existing DONE snapshot diff jobs are permanently
> unreadable
> ---------------------------------------------------------------------------------
>
> Key: HDDS-16442
> URL: https://issues.apache.org/jira/browse/HDDS-16442
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Critical
> Attachments: HDDS-16442.001.patch
>
>
> h3. Finding
> After a routine OM upgrade, every pre-existing DONE snapshot-diff job
> deserializes with a null largestEntryKey, so the first client read of the
> report trips the integrity check and hits an uncaught NPE (wrong-key
> updateJobStatus) instead of the designed retriable error; the report is
> intact yet permanently unreadable and the job is stuck DONE (not
> resubmittable) until the retention TTL GC, affecting all old jobs broadly
> rather than via a rare race.
> h3. Classification
> * Verdict: REPRODUCED
> * Severity: Critical
> * Source: Specula TLA+ model checking and confirmation debate, finding CR-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
> Precondition (reachable through a routine upgrade): a snapshot diff job for
> (fromSnapshot, toSnapshot) completed and was persisted as DONE by an OM build
> that predates the largestEntryKey field on SnapshotDiffJob. After the OM is
> upgraded, that job deserializes with largestEntryKey == null and stays in the
> active job table (loadJobsOnStartUp leaves DONE jobs untouched), with its
> report rows fully intact.
> 1. Client reads the completed diff: getSnapshotDiffReport routes the DONE job
> to createPageResponse.
> 2. On the last page, checkReportsIntegrity sees largestEntryKey == null and
> treats it as an integrity failure.
> 3. The recovery path calls updateJobStatus(diffJob.getJobId(), DONE, FAILED).
> The job table is keyed by the dedup jobKey (fromSnapshotId + DELIMITER +
> toSnapshotId), not by the per-generation jobId, so the lookup returns null
> and dereferencing its status throws NullPointerException.
> 4. The designed retriable IOException ("Report integrity check failed. Retry
> after: ...") is never thrown, the DONE to FAILED transition never lands, and
> the job is stuck DONE (not a resubmittable status), so the client cannot
> recover until the report-retention TTL GC. Every read repeats the NPE.
> The same NPE occurs for any genuine last-page integrity mismatch (non-null
> largestEntryKey that disagrees with the report tail), since the recovery call
> always passes the wrong key.
> h3. Root cause
> hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java:
> checkReportsIntegrity called updateJobStatus(diffJob.getJobId(), DONE,
> FAILED), passing the jobId where updateJobStatus expects the dedup jobKey (it
> does snapDiffJobTable.get(jobKey)). Two problems compound: (1) a null
> largestEntryKey (legacy DONE job) is treated as a failure instead of a "no
> stored last entry" value, and (2) updateJobStatus has no null guard, so the
> wrong-key lookup NPEs on getStatus().
> h3. Suggested fix
> Thread the dedup jobKey down through createPageResponse into
> checkReportsIntegrity and pass it to updateJobStatus. Treat a null
> largestEntryKey on a DONE job as a legacy value and skip the last-page
> integrity assertion so upgraded jobs stay readable. Null-guard
> updateJobStatus so a missing row logs and returns instead of throwing.
> Regression tests cover both the legacy-null (stays readable) and
> genuine-mismatch (flips to FAILED via jobKey, throws the retriable
> IOException) cases. See [^HDDS-16442.001.patch].
> 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]