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

Siyao Meng updated HDDS-17041:
------------------------------
    Attachment: TestBugMC1NSSummaryRetryReplay.java

> 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
>            Priority: Minor
>         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