Siyao Meng created HDDS-17356:
---------------------------------

             Summary: Data scan marks a container UNHEALTHY when its DB reopen 
fails on the lock still held after a RocksDB error close
                 Key: HDDS-17356
                 URL: https://issues.apache.org/jira/browse/HDDS-17356
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-3-treat-db-lock-held-by-previous-instance-as-transient-scan-error.patch, 
TestBugMC3ContainerCacheReopenAfterErrorClose.java

h3. Mechanism
When a read or write on a container DB fails with {{Corruption}} or 
{{IOError}}, {{RocksDatabase.closeOnError}} marks the DB closed and starts a 
{{DBCloser}} thread. That thread frees the native DB, and releases its LOCK 
file, only when the last reference is gone; since HDDS-14800 an open iterator 
is such a reference. {{ContainerCache.getDB}} sees the closed store, removes it 
({{removeDB}} calls {{ReferenceCountedDB.cleanup}} and {{stop}}, whose 
{{RocksDatabase.close}} returns at once because the close is already in 
progress) and reopens the same path. RocksDB rejects the open with "lock hold 
by current process" while the old instance still exists.

{{BlockUtils.getDB}} turns that into a {{StorageContainerException}} with 
{{UNABLE_TO_READ_METADATA_DB}} and drops the cause. A data scan that gets this 
exception records {{INACCESSIBLE_DB}}, and {{ContainerScanHelper}} cannot 
recognise it as transient (the check added by HDDS-15150 walks the cause 
chain), so it marks the container UNHEALTHY and triggers a volume scan.

h3. Trigger
# A schema V1 or V2 container DB is open in {{ContainerCache}} and a thread 
iterates it (for example the container data scanner, which keeps its block 
iterator open while it scans a block).
# Another operation on the same DB fails with {{Corruption}} or {{IOError}} (I 
used real corruption of the SST files).
# A data scan of that container runs before the iterator is closed. This is 
likely: {{HddsDispatcher}} queues an on demand data scan after every failed 
request, including the original Corruption or IOError and each failed request 
in the window (subject to the minimum scan gap).

Schema V3 is not affected: {{DatanodeStoreCache}} does not reopen a closed 
store.

h3. Impact
* The data scan marks a replica UNHEALTHY although the reopen failed only 
because the old instance had not been freed yet; SCM then replaces the replica. 
After a Corruption the mark may be justified anyway, after a transient IOError 
it is not. Only data scans open the DB; the metadata scan does not.
* Client requests for the container fail with {{UNABLE_TO_READ_METADATA_DB}} 
until the iterator on the old instance is closed, and each schedules a volume 
check. The volume check does not open container DBs, so it does not fail a 
healthy volume. After the iterator is closed, {{getDB}} succeeds again, also on 
the same corrupted files. Clients retry on another replica.

h3. Reproduction
[^TestBugMC3ContainerCacheReopenAfterErrorClose.java] 
({{hadoop-hdds/container-service}}) builds a schema V2 container DB, opens it 
through {{BlockUtils.getDB}} with an iterator, overwrites the start of each SST 
file, and reads another table, which fails with Corruption. A second 
{{BlockUtils.getDB}} then fails with {{UNABLE_TO_READ_METADATA_DB}} ("lock hold 
by current process" in the log). That exception is given to 
{{ContainerScanHelper.scanData}} the way {{KeyValueContainerCheck.scanData}} 
reports it. After the iterator is closed, the same {{getDB}} succeeds. The test 
asserts the correct behaviour, so on unfixed code it fails at its last 
assertion: the container was marked UNHEALTHY. With the patch it passes.

h3. Patch
[^MC-3-treat-db-lock-held-by-previous-instance-as-transient-scan-error.patch], 
against bf3829e6c8ac69ae17585585703f91426dcdf6b3. It also applies to master at 
4fb68e96831. The open pull request for HDDS-17261 changes adjacent lines in 
{{BlockUtils.getDB}} and {{ContainerScanHelper.handleUnhealthyScanResult}}, so 
whichever lands second needs a small rebase.

{{BlockUtils.getDB}} keeps the original exception as the cause, and 
{{ScanTransientIOUtil}} also treats "lock hold by current process" in the cause 
chain as transient ({{scanErrorsAreOnlyTooManyOpenFiles}} becomes 
{{scanErrorsAreOnlyTransient}}). A data scan in the window is then logged as 
incomplete and retried on a later pass instead of marking the container 
UNHEALTHY. The match is narrow: a LOCK held by another process is still an 
error. Requests in the window still fail transiently as described above; making 
them wait for the old instance is not proposed, because {{ContainerCache}} 
closes it under its cache wide lock and the holder can be a scanner that keeps 
its iterator for a whole block. As a side effect of keeping the cause, the 
HDDS-15150 "too many open files" check also works when the DB open itself fails 
on file descriptor exhaustion (from the code, not run). The two scan log 
messages that named only file descriptor exhaustion are reworded.

Covered by {{testGetDBFailsWithLockCauseAfterErrorCloseWithOpenIterator}} in 
the existing {{TestContainerCache}} (real window, the failure must carry the 
RocksDB lock error, and {{getDB}} succeeds after the iterator is closed), 
{{testDataScanOnlyDbLockHeldByPreviousInstanceDoesNotMarkUnhealthy}} in 
{{TestBackgroundContainerDataScanner}}, and two cases in 
{{TestScanTransientIOUtil}} (lock held by this process is transient, lock held 
by another process is not). The first two fail without the change. With it the 
container cache, scanner, container check, dispatcher, handler, container and 
volume suites (420 tests) pass, checkstyle is clean.

Found by TLA+ model checking and code review of the RocksDB handle close and 
shutdown paths under HDDS-15926, on commit 
bf3829e6c8ac69ae17585585703f91426dcdf6b3. 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
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to