Siyao Meng created HDDS-17372:
---------------------------------

             Summary: Datanode keeps writing to the open block file of a 
deleted container replica after the replica is recreated at the same path
                 Key: HDDS-17372
                 URL: https://issues.apache.org/jira/browse/HDDS-17372
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
CR-4-reopen-block-file-for-recreated-container-replica.patch, 
TestBugCR4StaleOpenBlockFileAfterContainerRecreate.java

h3. Mechanism
{{FilePerBlockStrategy.writeChunk}} gets its {{FileChannel}} from 
{{OpenFiles}}, a cache keyed by the block file path that keeps an entry for up 
to 10 minutes after its last access. Before it expires, an entry is closed only 
by PutBlock with end of block (also when sent inside a WriteChunk), 
PutSmallFile, FinalizeBlock and container reconciliation 
({{finishWriteChunks}}, {{finalizeWriteChunk}}). Container delete 
({{KeyValueHandler.deleteInternal}}), close, quasi close and import do not 
touch it. If a block received chunks but no final PutBlock and its replica is 
then deleted, the entry keeps the deleted file open. A new replica of the same 
container created on the same volume has the same block file paths, so its 
WriteChunks for that block hit the cached entry and go to the deleted file. The 
new replica's block file is never created.

h3. Trigger
* A replica of container C on datanode D receives chunks of block B without a 
final PutBlock, for example an EC reconstruction that fails midway through B.
* The replica is deleted (the EC coordinator force deletes its RECOVERING 
targets after a failure) and a new replica of C is created on the same volume 
of D, for example the next reconstruction attempt choosing D again.
* The new replica writes B within 10 minutes of the last write to B on the 
deleted replica. For EC reconstruction SCM sends the next attempt only after 
the pending operation expires ({{hdds.scm.replication.event.timeout}}, 12 
minutes by default), so the first attempt must have taken at least about 2 
minutes longer than the retry to reach B, counted from when SCM sent each 
command, for example because it waited in the datanode's queue (bounded by the 
datanode deadline, 6 minutes by default) or read from a slow source. Argued 
from the code, not run.
* Container reconciliation of a replica that was deleted and imported back onto 
the same volume can also write through the stale entry. Argued, not run.

h3. Impact
The new replica's chunks for B are written to the deleted file. For EC 
reconstruction the final PutBlock for B then fails with UNABLE_TO_FIND_CHUNK 
("Chunk file not found"), the datanode schedules a volume check 
({{StorageVolumeUtil.onFailure}}), and the attempt fails and cleans up its 
targets. The failing PutBlock closes the stale entry, so the next attempt is 
not affected; the cost is one more failed reconstruction and a later restored 
redundancy. For reconciliation the block file already exists in the imported 
replica, so the PutBlock check passes while the repaired chunks are missing 
from the file; the scan that follows reconciliation would find the checksum 
mismatch and mark the replica unhealthy (argued, not run). In both cases the 
data is not acknowledged as stored on the new replica and the source replicas 
keep it; I found no sequence that loses committed data.

The path keyed cache exists since HDDS-2717 (1.0.0), but I found no writer that 
reaches a recreated replica before EC reconstruction (1.3.0).

h3. Reproduction
[^TestBugCR4StaleOpenBlockFileAfterContainerRecreate.java] 
({{hadoop-hdds/container-service}}) uses one volume and one {{KeyValueHandler}} 
and only the calls the datanode makes: CreateContainer in RECOVERING state, 
WriteChunk without PutBlock, force {{deleteContainer}}, CreateContainer again 
(same path, asserted), WriteChunk, PutBlock. On unmodified source it fails with:
{noformat}
[PutBlock on the recreated replica: Chunk file not found: 
<tmp>/hdds/<clusterId>/current/containerDir0/1/chunks/1.block]
expected: SUCCESS
 but was: UNABLE_TO_FIND_CHUNK
{noformat}
What was run is this datanode level sequence, on macOS only. Not exercised: a 
mini cluster with real EC reconstruction, SCM's retry timing, and 
reconciliation.

h3. Suggested fix
Remember which replica opened each cached file and reopen the path when a 
different replica writes to it. Closing the container's cached files on delete 
was considered and not chosen: it needs a new {{ChunkManager}} method and hooks 
in every place that removes a replica, and still misses a write in flight 
during the delete. Checking the file's identity ({{fileKey}}) on every write 
costs a {{stat}} per WriteChunk.

h3. Patch
[^CR-4-reopen-block-file-for-recreated-container-replica.patch], against 
5fed99dab3c3204cd7bf1516074969a38d5eb00d. It also applies to master at 
76b5c6947a2. It overlaps textually with two open pull requests that change the 
same file: apache/ozone#11668 changes the line after the changed {{getChannel}} 
call in {{writeChunk}}, and apache/ozone#11678 adds imports to 
{{TestFilePerBlockStrategy}} in the same place. Whichever lands later needs a 
trivial rebase.

Each cached {{OpenFile}} now keeps the {{KeyValueContainerData}} of the replica 
that opened it. {{OpenFiles.getChannel}} receives the writing replica's data; 
if the cached entry belongs to another replica object, it removes that entry 
(the removal listener closes the old file) and opens the path again. A 
recreated replica always has a new {{KeyValueContainerData}}, while all writes 
of a live replica pass the same object, so an unchanged replica keeps reusing 
its channel. A write from a replica already in DELETED state (one that was in 
flight when its replica was deleted) is refused in that case instead of taking 
over the new replica's file, with the error a write to a DELETED container gets 
from {{KeyValueHandler}}. The cost is one reference comparison per WriteChunk.

Covered by 
{{TestFilePerBlockStrategy#testWriteChunkAfterContainerRecreatedOnSameVolume}}: 
write a chunk to a replica without PutBlock, delete the replica, create it 
again at the same path, write two chunks, finish the block, and expect the file 
to hold both chunks; then, with a new chunk open on the new replica, expect a 
write from the deleted replica to be refused and the file to be unchanged. 
Without the change it fails with "Chunk file not found". With the change 
{{TestFilePerBlockStrategy}}, {{TestFilePerChunkStrategy}}, 
{{TestChunkManagerDummyImpl}}, {{TestKeyValueHandler}}, 
{{TestKeyValueStreamDataChannel}}, {{TestBlockManagerImpl}}, 
{{TestKeyValueContainer}} and {{TestContainerReconciliationWithMockDatanodes}} 
pass (411 tests, 26 skipped by the suites' own parameters) and checkstyle is 
clean. The attached reproduction passes with the change.

Found by code review of the datanode memory mapped chunk read path, as part of 
the TLA+ verification effort under HDDS-15926, on commit 
5fed99dab3c3204cd7bf1516074969a38d5eb00d. 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