Siyao Meng created HDDS-16882:
---------------------------------
Summary: Recon swallows the RocksDB open failure of a fetched OM
DB snapshot, logs success and keeps using its deleted previous OM DB
Key: HDDS-16882
URL: https://issues.apache.org/jira/browse/HDDS-16882
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: CR-18-recon-report-failed-open-of-fetched-om-db.patch,
TestBugCR18ReconOmDbOpenFailureReportedAsSuccess.java
h3. Mechanism
{{ReconOmMetadataManagerImpl.updateOmDB}} opens the new snapshot through
{{initializeNewRdbStore}}, which catches the {{IOException}} from
{{DBStoreBuilder.build}}, logs "Unable to initialize Recon OM DB snapshot
store." and returns normally. The store is not replaced, so Recon keeps its
previous OM DB handle, whose directory
{{OzoneManagerServiceProviderImpl.getOzoneManagerDBSnapshot}} deleted at the
start of the fetch. {{updateReconOmDBWithNewSnapshot}} then logs "Successfully
updated Recon OM DB with new snapshot.", increments {{snapshotDownloadSuccess}}
and returns true.
{{executeFullSnapshot}} continues as if the snapshot had been installed and
queues the task reinitialization. That creates a RocksDB checkpoint of the
previous DB, which fails because its directory is gone ({{NotFound: ... Failed
to createCheckpoint}}). Only this second failure marks the run as failed: the
snapshot task status becomes -1, health turns false and
{{GET_OM_DB_SNAPSHOT_FAILED}} is set, with "Failed to queue reinitialization
event for manual trigger (result: RETRY_LATER)" as the logged reason. The catch
dates from HDDS-1233; the same code is in 2.2.1.
h3. Trigger
Any full OM DB fetch whose result RocksDB cannot open, for example a snapshot
with a truncated or missing SST, or a disk that fills up while RocksDB opens
it. Only two cases were run: a snapshot whose largest SST is cut in half
({{Corruption: ... Failed to open}}) and a {{CURRENT}} file that names a
missing MANIFEST. On master a truncated SST can come from a retry after a
failed untar (HDDS-16880).
h3. Impact
* The log and the {{snapshotDownloadSuccess}} metric report a successful
update. The failure that is reported instead is the task reinitialization, and
its message blames the previous DB ("the on-disk OM DB may be incomplete or
corrupt").
* Recon keeps serving the previous OM DB handle. Reads still work from the open
files and writes are accepted, so the following delta updates are applied to
it, but a flush of that DB fails because its directory is gone (run with a
direct write and flush).
* Because the reinitialization failed, {{ReconTaskControllerImpl}} sets its
task failure flag, and {{consumeOMEvents}} stops passing delta events to the
Recon tasks until a reinitialization is queued (from code, not run). Each
following sync retries the reinitialization against the previous DB and fails
again. After at most six failed attempts it falls back to a full fetch, which
recovers (run: six sync cycles, about 30 seconds at the default 5 s
{{ozone.recon.om.snapshot.task.interval.delay}}). If the delta updates still
fail, for example with {{SequenceNumberNotFoundException}}, the next sync
fetches again right away (run).
* No effect on OM.
h3. Reproduction
Deterministic, unmodified source.
[^TestBugCR18ReconOmDbOpenFailureReportedAsSuccess.java]
({{hadoop-ozone/recon}}) runs Recon's own {{ReconOmMetadataManagerImpl}},
{{OzoneManagerServiceProviderImpl}} and {{ReconTaskControllerImpl}} against a
real OM RocksDB. Recon starts from a snapshot it fetched earlier; only the
download is replaced, and it returns a copy of an OM checkpoint whose largest
SST is cut in half. The tests assert the correct behaviour, so they fail while
the defect is present:
* {{updateReconOmDBWithNewSnapshot}} returns true, while Recon's store is still
the previous one, whose directory no longer exists.
* A full sync through {{syncDataFromOM}} (delta updates rejected with
{{SequenceNumberNotFoundException}}) queues the task reinitialization against
the previous DB.
With the patch both pass.
h3. Patch
[^CR-18-recon-report-failed-open-of-fetched-om-db.patch], against
a6b7bdb937109ba2893688b41a89470bc95c88cf. It also applies to master at
6818707698c.
{{initializeNewRdbStore}} no longer catches the exception. {{start}} keeps the
catch, so Recon still starts without an OM DB when its last known snapshot
cannot be opened, and then fetches a new one. {{updateOmDB}} now deletes the
snapshot directory that failed to open and rethrows, keeping the previous store
open, and {{updateReconOmDBWithNewSnapshot}} logs "Unable to refresh Recon OM
DB Snapshot." with the RocksDB error, counts {{snapshotDownloadFailures}} and
returns false. {{executeFullSnapshot}} then marks the run as failed without
queuing the reinitialization. This is how a failed download is already handled,
and as after a failed download no {{om.snapshot.db_<ts>}} is left on disk, so a
restart finds no OM DB snapshot and fetches a new one.
The deletion matters because the Recon tasks now keep consuming delta updates
after the failed open. If the open failure was transient (for example no file
descriptors or disk space left), a restart would otherwise open the left over
snapshot, the OM DB would be back behind the tasks, and the next delta updates
would reach the tasks a second time (from code, not run end to end).
Recovery is then also the same as after a failed download. If the delta updates
still fail, for example with {{SequenceNumberNotFoundException}}, the next sync
fetches again (run). If the full fetch came from failed task reinitializations,
the following syncs first retry the reinitialization up to six times, as today.
If the trigger was a transient delta failure, Recon goes on applying delta
updates to the previous handle, the Recon tasks keep receiving events, and
health shows {{GET_OM_DB_SNAPSHOT_FAILED}} until a full fetch succeeds. Today
the failing reinitialization forces that fetch within six cycles, about 30
seconds at the default interval. With the patch it happens at the next delta
failure, the first failed flush of the previous DB, or a restart, so on a
cluster with little OM write traffic there is no bound (run: no new fetch in
ten cycles). Until then the deleted files of the previous DB stay allocated.
That any failed full fetch leaves Recon on a DB whose directory is deleted is
not changed here.
Covered by {{testUpdateReconOmDBWithUnreadableSnapshot}} in the existing
{{TestOzoneManagerServiceProviderImpl}} and {{testUnreadableSnapshot}} in the
existing {{TestReconOmMetadataManagerImpl}} (which also checks that {{start}}
still skips an unreadable snapshot). Without the change they fail with
"expected: <false> but was: <true>" and "Expecting code to raise a throwable.".
Both also check that the snapshot directory that failed to open is deleted;
without the deletion they fail with "expected: <0> but was: <1>" and "Expecting
file: ... not to exist". With it, the Recon module unit tests pass (937 run;
the only failures are a Derby performance test,
{{TestUnhealthyContainersDerbyPerformance}}, that times out on the test machine
and fails the same way without the change), and checkstyle is clean.
Related: HDDS-16880 (a retry after a failed untar builds a snapshot with a
truncated SST), HDDS-9367 (open, RocksDB error handling in Recon in general),
HDDS-15995 (Recon SCM DB used after its directory was renamed).
Found by code review of the OM Ratis snapshot (checkpoint) install paths, as
part of the TLA+ verification effort under HDDS-15926, on commit
a6b7bdb937109ba2893688b41a89470bc95c88cf. 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]