Siyao Meng created HDDS-17012:
---------------------------------

             Summary: Container data scan cut short by a closed RocksDB is 
accepted as complete, so a partial checksum is reported and the replica stays 
CLOSED
                 Key: HDDS-17012
                 URL: https://issues.apache.org/jira/browse/HDDS-17012
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: MC-3-report-data-scan-cut-short-by-closed-db.patch, 
TestBugMC3ScanTruncatedByDbClose.java

h3. Mechanism
Since HDDS-14800, {{RDBStoreAbstractIterator.hasNext}} returns false once the 
RocksDB is closed, so a scanner stops without an exception. The datanode block 
iterator ({{AbstractDatanodeStore.KeyValueBlockIterator}}) passes that on, so 
{{KeyValueContainerCheck.scanData}} takes the end of iteration as the end of 
the container's blocks, and a scan whose per-disk DB is closed partway through 
ends with no error. {{ContainerScanHelper.scanData}} then treats it as a 
complete healthy scan: {{updateContainerChecksum}} writes the merkle tree built 
from the blocks scanned so far to the {{.tree}} file, sets the container data 
checksum from it and sends an ICR, the data scan timestamp moves forward, and 
the replica stays CLOSED.

A closed DB stays closed until the datanode restarts: {{DatanodeStoreCache}} 
keeps the closed store, and the volume check does not look at it 
({{HddsVolume.checkDbHealth}} also ignores NoSpace errors). The metadata 
scanner does not read the DB.

h3. Trigger
# A CLOSED container is being scanned by the background or on demand data 
scanner.
# The per-disk RocksDB of its volume is closed by RocksDB error handling 
({{RocksDatabase.closeOnError}}), for example when a write or flush for another 
container on the same disk fails with an IOError such as a full disk.
# The scanner's next {{hasNext}} returns false and the partial result is saved.

No client action is involved; the close comes from a disk or RocksDB error on 
the datanode.

h3. Impact
* Only the container being scanned on that disk when the DB closes is affected 
(normally one). Every other container on the disk is already marked UNHEALTHY 
with checksum 0 when the scanner or a failed request reaches it, because its 
scan finds the DB closed at the start.
* The affected replica stays CLOSED although its DB can no longer be read, and 
reports a data checksum computed from part of its blocks. That checksum is 
visible to operators ({{ozone admin container info}}) and disagrees with the 
other replicas.
* It is marked UNHEALTHY by the next scan of that container. A client request 
that needs the DB fails and triggers an on demand scan ({{HddsDispatcher}} does 
this for every failed request), but not within {{hdds.container.scrub.min.gap}} 
(15 minutes by default) of the truncated scan, since that scan moved the scan 
timestamp forward. A container that receives no requests waits for the next 
background data scan (the interval is 7 days by default). Argued from the code, 
not run end to end.
* No data is lost.

The code is in the released versions 2.2.0 and 2.2.1, which include HDDS-14800.

h3. Reproduction
PASS, deterministic, unmodified source, needs a non root user. 
[^TestBugMC3ScanTruncatedByDbClose.java] ({{hadoop-hdds/container-service}}) 
uses a schema V3 volume, a real {{KeyValueHandler}} and 
{{ContainerScanHelper}}. A closed container with 8 blocks is scanned once as a 
baseline. A second scan is paused by a throttler at the start of the third 
block; meanwhile a block is written for another open container on the same DB, 
the DB directory is made read only and the flush fails with an IOError, which 
closes the DB through {{closeOnError}}. After the scan resumes the container is 
still CLOSED, its checksum changed, the tree file holds 3 of 8 blocks, the last 
ICR carries the new checksum with state CLOSED and the scan timestamp moved 
forward. A passing test means the defect is present.

h3. Patch
[^MC-3-report-data-scan-cut-short-by-closed-db.patch], against 
cce1ca28944886218132f01c0e271c1fd0700078. It also applies to master at 
2536fe9cbc3.

When the underlying table iterator ends, {{KeyValueBlockIterator.hasNext}} now 
throws an {{IOException}} if the DB is closed, instead of reporting the end of 
the blocks. It reads the unsynchronized {{RDBStore.isClosed}}, so it cannot 
block against {{AbstractRDBStore.stop}}, which waits for open iterators. The 
shared {{RDBStoreAbstractIterator}} used by OM, SCM and Recon is not changed. 
Every caller of the block iterator already handles an {{IOException}} from it, 
since creating the iterator on a closed DB already throws one. Besides the 
scanner this covers {{KeyValueContainerUtil.noBlocksInContainer}} (the empty 
check before a container delete and after block deletion), which could 
otherwise answer "no blocks" if the DB closed during its iteration, and the 
block counting and metadata checksum paths.

{{KeyValueContainerCheck.scanData}} records the exception as 
{{INACCESSIBLE_DB}} and, when the block list could not be read in full, now 
returns an empty data tree, as it already does when the DB cannot be opened at 
the start of a scan ({{DataScanResult.unhealthyMetadata}} uses the same 
convention). So the replica is marked UNHEALTHY with checksum 0, the same as 
the other containers on that disk. A scan that finishes on an open DB and a 
scan of a container deleted during the scan behave as before.

Covered by {{testKeyValueContainerCheckDBClosedDuringScan}} in the existing 
{{TestKeyValueContainerCheck}}, which stops the store from another thread 
during the scan and expects one {{INACCESSIBLE_DB}} error and an empty tree 
with checksum 0. Without the change it fails for all container test versions 
with no scan error reported. With it the block iterator, scanner and 
reconciliation suites and the suites of the other block iterator callers (887 
tests) and the three container scanner integration suites on a mini cluster (21 
tests) pass, and checkstyle is clean. With the patch the reproduction fails 
because the replica is marked UNHEALTHY and reports checksum 0.

Not changed here: a per-disk DB closed by RocksDB does not fail its volume, so 
after a transient full disk every container on it ends up UNHEALTHY, even 
though a restart would reopen the DB. That is a separate follow up (related: 
HDDS-13994, opening the DB read only on a full disk).

Found by TLA+ model checking and code review of the datanode volume and 
per-disk RocksDB lifecycle paths under HDDS-15926, on commit 
cce1ca28944886218132f01c0e271c1fd0700078. 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