Siyao Meng created HDDS-17047:
---------------------------------

             Summary: Failed rename while Recon swaps in its rebuilt DB leaves 
no open DB; Recon DB reads and later rebuilds fail until restart
                 Key: HDDS-17047
                 URL: https://issues.apache.org/jira/browse/HDDS-17047
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: CR-9-restore-current-db-on-failed-swap.patch, 
TestBugCR9FailedDbSwap.java

h3. Mechanism
{{ReconDBProvider.replaceStagedDb}} deletes {{.backup}}, closes the live DB, 
renames the live DB to {{.backup}} and the staged DB to the live path, then 
opens it. Its rollback, run when {{dbStore}} is null, always renames the live 
path to {{.staged}} and {{.backup}} to the live path, whichever renames 
actually happened:
* If the first rename fails, the live DB is still in place but closed; the 
rollback's rename of the live DB to {{.staged}} fails as well (the staged DB is 
still there).
* If the second rename fails, the live path does not exist; the rollback fails 
on it and {{.backup}} is not restored.

The rollback's exception replaces the original one and {{dbStore}} stays null. 
Separately, {{initializeDBStore}} returns null without an exception when the 
swapped DB cannot be opened, so the swap can also return normally with 
{{dbStore}} null.

{{ReconTaskControllerImpl.reInitializeTasks}} then calls 
{{ReconNamespaceSummaryManagerImpl.reinitialize}}, which throws a 
NullPointerException outside its {{catch (IOException)}}, before the container 
metadata, global stats and file metadata managers are reinitialized. All four 
managers keep the closed DB, and every later swap throws a NullPointerException 
at {{dbStore.getDbLocation()}}.

h3. Trigger
A rename in the Recon DB directory fails during a swap after the {{.backup}} 
delete succeeded. Because {{.backup}} stays in place after each successful 
swap, a directory wide fault (permission change, read only remount) usually 
fails that delete first, before anything is closed, which is harmless. The bug 
is therefore reached on the first swap of an installation, by a fault that hits 
the rename but not the delete, or by a RocksDB open failure of the swapped DB.

h3. Impact
Until Recon restarts: reads of the Recon DB (namespace summaries, container key 
mappings, OM insight counters, file size counts) fail with "Rocks Database is 
closed", and every later rebuild fails at the swap. A restart restores 
{{.backup}} when it exists. No data is lost.

Related but different: HDDS-17037 covers reads that see the closed DB during a 
successful swap.

h3. Reproduction
PASS on unmodified source, deterministic. [^TestBugCR9FailedDbSwap.java] 
({{hadoop-ozone/recon}}) uses a real {{ReconDBProvider}}:
* {{renameOfStagedDbFails}}: the staged directory is removed before the swap, 
so the second rename fails. Afterwards no DB is open, the live path does not 
exist, {{.backup}} is left, {{reinitialize}} throws NullPointerException, a 
read fails with a closed DB, and the next swap throws NullPointerException. A 
new provider (restart) restores {{.backup}}.
* {{renameOfCurrentDbFails}}: on the first swap the Recon DB directory is made 
unwritable, so the first rename fails. Afterwards no DB is open although the 
live DB is intact, and the next swap throws NullPointerException. Skipped where 
the directory cannot be made unwritable (for example as root).

A passing test means the defect is present.

h3. Patch
[^CR-9-restore-current-db-on-failed-swap.patch], against 
0665932dd35da4d05087e02748af0a64eb846ad5. It also applies to master at 
2264d4d48ac.

A null result of opening the swapped DB now throws, and the rollback undoes 
only what happened: {{.backup}} exists only if the live DB was moved, so then 
the live path (if present) goes back to {{.staged}} and {{.backup}} back to the 
live path; then the live path is reopened. A failed rename inside the rollback 
is added to the original exception as suppressed, and the DB is then not 
reopened, so that an absent live path is not recreated empty and the restart 
can still restore {{.backup}}. A failed reopen is added as suppressed too. A 
successful swap is unchanged.

{{ReconTaskControllerImpl.reInitializeTasks}} now returns false when the swap 
throws, so the controller retries the rebuild through its existing failure path 
(a new checkpoint after the retry delay, a full OM snapshot after repeated 
failures). Before, it reported success and kept the old derived data, which 
then permanently missed the events drained when the rebuild was queued. 
Behaviour change: a failing {{.backup}} delete, which already left the old DB 
in service, now also triggers these retries instead of being ignored.

Covered by {{TestReconDBProvider#testFailedSwapKeepsCurrentDb}} (second rename 
fails), {{TestReconDBProvider#testUnopenableSwappedDbKeepsCurrentDb}} (the 
swapped DB has a corrupt {{CURRENT}} file) and 
{{TestReconTaskControllerImpl#testReInitializeTasksFailsWhenDbSwapFails}}, 
which all fail without the change. With it, {{TestReconDBProvider}}, 
{{TestReconReplicaHistoryDBProvider}}, 
{{TestOmTableInsightTaskStaleCounterAfterReinit}}, 
{{TestReconTaskControllerImpl}}, {{TestContainerEndpoint}}, 
{{TestReconVersionManager}} and {{TestNSSummaryUnifiedControl}} pass (93 tests, 
1 skipped as before) and checkstyle is clean. With the patch both reproduction 
tests fail because the previous DB is open again after the failed swap.

Found by code review of the Recon namespace summary and task sync paths, as 
part of the TLA+ verification effort under HDDS-15926, on commit 
0665932dd35da4d05087e02748af0a64eb846ad5. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. 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