Siyao Meng created HDDS-17041:
---------------------------------

             Summary: Retry of a failed NSSummaryTask applies namespace summary 
deltas twice or skips them, so Recon file counts and sizes stay wrong
                 Key: HDDS-17041
                 URL: https://issues.apache.org/jira/browse/HDDS-17041
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-1-resume-nssummary-subtasks-from-committed-position.patch, 
TestBugMC1NSSummaryRetryReplay.java

h3. Mechanism
{{ReconTaskControllerImpl.processOMUpdateBatch}} retries a failed task once on 
the same event batch, passing the seek positions the failed attempt returned 
({{TaskResult.getSubTaskSeekPositions}}). {{NSSummaryTask.process}} runs three 
sub-tasks ({{NSSummaryTaskWithFSO.processWithFSO}}, 
{{NSSummaryTaskWithLegacy.processWithLegacy}}, 
{{NSSummaryTaskWithOBS.processWithOBS}}) and records one seek position per 
sub-task. Each sub-task skips that many events and adds the remaining ones to 
the stored totals (read, add, write back), so a retry is only correct if the 
seek position equals the number of leading events whose deltas are already 
committed to the Recon DB. Three exits of the sub-tasks break that:
* *A sub-task that succeeded reports the position it started from.* After its 
final flush it returns {{seekPos}}, which is 0 unless a threshold flush 
happened. If a sibling sub-task fails, the retry runs the successful sub-task 
again from that position and adds its committed deltas a second time.
* *The position after a threshold flush is one too far.* When the in memory map 
reaches {{ozone.recon.nssummary.flush.db.max.threshold}} entries, the sub-task 
commits it and sets {{seekPos = eventCounter + 1}}. {{eventCounter}} already 
counts the event that was just applied, so the retry also skips the next event, 
which was never applied.
* *An unchecked exception discards the flushed position.* The loops catch only 
{{IOException}}. Any other exception ends the sub-task, and 
{{NSSummaryTask.awaitSubTask}} records the seek position the attempt started 
from. The retry then applies again everything that the threshold flushes of the 
failed attempt had committed.

In each case the retry succeeds, the controller moves the task's 
{{lastUpdatedSeqNumber}} to the end of the batch and does not set 
{{tasksFailed}}, so no rebuild is queued. Later batches add to the wrong totals.

h3. Trigger
A sub-task of {{NSSummaryTask}} has to fail once and succeed on the immediate 
retry. A failure that repeats makes the retry fail as well, which sets 
{{tasksFailed}}, and the full rebuild that follows replaces the wrong totals. 
An I/O or corruption error of the Recon RocksDB does not qualify, because 
{{RocksDatabase}} then closes the database and the retry fails too.
* Double counting of a sibling: any batch in which one sub-task fails and 
another one succeeds. The batch size and the flush threshold do not matter.
* Skipped event, and double counting after an unchecked exception: in addition, 
the failing sub-task must have done a threshold flush earlier in the same 
batch, that is the batch touches at least 
{{ozone.recon.nssummary.flush.db.max.threshold}} distinct summary entries in 
that sub-task (150000 in ozone-default.xml). In practice this needs an FSO 
bucket, or a Legacy bucket with file system paths, with a wide directory tree. 
OBS would need that many buckets in one batch.

Argued from the code, not run: the sub-tasks look up buckets (OBS, Legacy) and 
Legacy parent directories in the Recon OM DB. 
{{OzoneManagerServiceProviderImpl.syncDataFromOM}} applies each delta to that 
DB before it queues the events, and keeps applying later deltas while the 
controller processes the queue, so the DB is ahead of the batch being processed 
and can change between the first attempt and the retry. If a bucket that the 
sub-task has not cached yet, or a Legacy parent directory, is deleted and 
recreated under the same name, and the first attempt runs after the delete is 
applied but the recreate is applied before the retry, the first attempt fails 
on the missing entry and the retry succeeds. The window is narrow, since the 
two attempts run back to back. Otherwise the failure repeats and the rebuild 
repairs the totals. (On such a retry the key is counted under the recreated 
bucket or directory, a separate inaccuracy that the patch does not change.) The 
reproductions inject the failure.

The first two exits exist since HDDS-12062 (ozone-2.0.0 to ozone-2.2.1 by 
reading the tagged source, not run on a release). The third exists since 
HDDS-15335 (ozone-2.2.0, ozone-2.2.1). Before HDDS-15335 an unchecked exception 
was not retried at all.

h3. Impact
The namespace summary that Recon serves for {{/api/v1/namespace/summary}} and 
{{/api/v1/namespace/du}} keeps wrong file counts and sizes for the affected 
buckets and directories until a full rebuild. Nothing compares the incremental 
totals with a rebuild. No OM data is affected.

h3. Reproduction
Run on unmodified source. [^TestBugMC1NSSummaryRetryReplay.java] 
({{hadoop-ozone/recon}}) builds the summary of one bucket per layout with a 
full rebuild, then sends one batch through the real 
{{ReconTaskControllerImpl.processReconEvent}}, which runs the real 
{{NSSummaryTask}} on the real Recon RocksDB and then its retry. The only change 
is a Mockito spy of the summary manager that throws once when the OBS bucket's 
summary is read. Each test also checks that the batch is recorded as applied 
and that {{hasTasksFailed}} is false. A passing test means the defect is 
present.
* Legacy put plus OBS put, OBS fails once: the Legacy bucket ends with 3 files, 
a rebuild gives 2.
* Flush threshold 1, two OBS puts, the second fails once with {{IOException}}: 
the bucket ends with 2 files, a rebuild gives 3.
* The same with {{IllegalStateException}}: the bucket ends with 4 files, a 
rebuild gives 3.

With the patch all three tests fail because the totals equal the rebuild.

h3. Patch
[^MC-1-resume-nssummary-subtasks-from-committed-position.patch], against 
0665932dd35da4d05087e02748af0a64eb846ad5. It also applies to master at 
2264d4d48ac.

The patch makes the seek position of each sub-task mean "events of this batch 
already committed", counted from the start of the batch. In the three 
sub-tasks: a successful run returns the end of the batch ({{itrPos + 
eventCounter}}), so a retry skips it; a threshold flush records {{itrPos + 
eventCounter}} instead of {{eventCounter + 1}}; and the per event {{catch}} 
also takes {{RuntimeException}}, so the sub-task returns its last flushed 
position the same way it already does for {{IOException}}. In FSO and Legacy 
the threshold flush moves inside that {{try}}, as it already is in OBS, so an 
unchecked exception from the flush is covered too. A failed flush commits 
nothing (the batch is atomic), so the last committed position is still right. 
Not covered: an unchecked exception from the final flush after the loop, an 
{{Error}} and an interrupt, which still fall back to the position the attempt 
started from. A failure that repeats still fails the retry and leads to the 
rebuild as before. The controller and {{NSSummaryTask}} are unchanged. Retrying 
only the failed sub-tasks would need extra state in {{NSSummaryTask}}, the seek 
position already carries what is needed.

Covered by four new cases in the existing {{TestNSSummaryTask}}: 
{{testRetryDoesNotReapplySucceededSubTask}}, 
{{testRetryAfterThresholdFlushResumesAfterIOException}}, 
{{testRetryAfterThresholdFlushResumesAfterRuntimeException}} (OBS) and 
{{testRetryResumesAfterUncheckedExceptionInThresholdFlush}} (Legacy, the flush 
throws). They fail without the change ({{expected: 2 but was: 3}}, {{expected: 
3 but was: 2}}, {{expected: 3 but was: 4}}, {{expected: 3 but was: 4}}). Three 
existing assertions on the returned seek positions are updated to the new 
meaning ({{testIndependentSubTaskSeekPositions}} in {{TestNSSummaryTask}}, two 
in {{TestNSSummaryTaskWithFSO}}). With the change the Recon namespace summary, 
task controller, delta sync and namespace summary endpoint suites pass (287 
test runs) and checkstyle is clean. The patch does not repair totals that are 
already wrong.

Found by TLA+ model checking and code review of the Recon namespace summary and 
task sync paths under HDDS-15926, on commit 
0665932dd35da4d05087e02748af0a64eb846ad5. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-12062 (resolved, introduced the seek position retry), 
HDDS-5146 (a failure between the Recon OM DB update and the task run loses a 
batch) and HDDS-16463 (sync the Recon DB before the cursor advances). 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]

Reply via email to