Siyao Meng created HDDS-17432:
---------------------------------

             Summary: Stream PutBlock that passed the open check can commit 
into a container after it is closed and its checksum and close report were taken
                 Key: HDDS-17432
                 URL: https://issues.apache.org/jira/browse/HDDS-17432
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-2-recheck-container-state-under-lock-for-stream-putblock.patch, 
TestBugMC2StreamPutBlockCommittedAfterClose.java

h3. Mechanism
With {{ozone.client.datastream.putblock.without.raft.enabled}}, PutBlocks reach 
the datanode through the data stream instead of the Raft log: as a stream 
command in the middle of the stream ({{ContainerStateMachine.streamCommand}}) 
and when the stream closes ({{ContainerStateMachine.streamPutBlock}}). They are 
not ordered with the Raft-applied CloseContainer. 
{{KeyValueHandler.handlePutBlock}} calls {{checkContainerOpen}} (which accepts 
OPEN and CLOSING) and later commits the block through 
{{BlockManagerImpl.persistPutBlock}} without holding the container lock. 
{{KeyValueHandler.closeContainer}} holds the container write lock only while it 
syncs the DB and moves the replica to CLOSED; it then builds the data checksum 
from the block table and sends the close ICR.

A stream PutBlock that passed the check before the close can therefore commit 
after all of that. The CLOSED replica gains a block row and its block count 
changes, while the close ICR and the stored data checksum describe the replica 
without it. For the mid stream PutBlock, 
{{KeyValueContainer.addToPendingPutBlockCache}} then fails because the close 
cleared the pending block set, and the handler returns 
{{CONTAINER_INTERNAL_ERROR}}. {{HddsDispatcher}} treats that as a replica 
failure on a container that is no longer open and throws 
{{IllegalStateException}}, so the stream command fails for a PutBlock that was 
committed. The stream close PutBlock gets {{SUCCESS}}.

h3. Trigger
# Datanodes with {{hdds.container.ratis.datastream.enabled=true}} and a client 
with {{ozone.client.datastream.putblock.without.raft.enabled=true}} (both 
default false) write through the data stream.
# SCM closes the container while a stream PutBlock for it is between the open 
check and the commit on one replica.

No fault is needed. The window is narrow (from the open check to the RocksDB 
batch commit). Without a timing hook a tight loop hit it rarely (1 in 3000 
attempts); the stale close report and checksum were only shown with a timing 
hook.

h3. Impact
* The close ICR reports a key count and data checksum that do not include the 
block, and the stale checksum is kept across a restart. The next full container 
report refreshes the key count and the data scanner rebuilds the checksum 
(every 7 days by default).
* Mid stream PutBlock: if the other replicas rejected it, the late replica 
keeps a block row that no key references, because the client wrote the data 
again to a new block. Until the scanner runs the replicas report the same 
checksum; afterwards Recon reports a replica mismatch, and an admin reconcile 
copies the row to the peers. This is the same kind of leftover an abandoned 
write leaves through the Raft path. (Argued from the code, not run on a 
cluster.)
* Stream close PutBlock: if the other replicas rejected it, the late replica is 
the only one that holds the block, and the client is told the write succeeded 
only because of HDDS-17431, which ignores the rejection on the other replicas.
* I found no wrong result visible to a client or OM from this race alone. After 
the failed mid stream command the client excludes the pipeline and writes the 
data to a new block.
* PutBlocks over gRPC (EC) can race a direct close from 
{{CloseContainerCommandHandler}} in the same way, with the same transient 
effect on the close ICR and checksum. This patch does not change that path. 
(Argued from the code, not run.)

h3. Reproduction
PASS, deterministic, unmodified source. 
[^TestBugMC2StreamPutBlockCommittedAfterClose.java] 
({{hadoop-hdds/container-service}}) drives a real {{KeyValueHandler}} and 
container. The PutBlock enters {{handlePutBlock}} with the op 
{{ContainerStateMachine}} uses for each path, and the close goes through 
{{KeyValueHandler.closeContainer}}. The only hook is a Mockito spy on the real 
{{BlockManager}} that starts the close when the PutBlock reaches the commit and 
lets the commit run once the close has finished. For both paths it shows a 
CLOSED replica holding the block while its close ICR says keyCount 0 and its 
stored checksum is that of an empty container. A passing test means the defect 
is present. It calls the handler directly, so for the mid stream PutBlock it 
sees {{CONTAINER_INTERNAL_ERROR}} rather than the {{IllegalStateException}} 
that {{HddsDispatcher}} turns it into.

h3. Patch
[^MC-2-recheck-container-state-under-lock-for-stream-putblock.patch], against 
7212465c0bb19615708c144238317db51c2a696d. It also applies to master at 
ae71e2dd50c.

For stream PutBlocks only, {{handlePutBlock}} repeats {{checkContainerOpen}} 
under the container read lock and commits while holding it. Close, quasi close 
and mark unhealthy change the state under the container write lock, so a stream 
PutBlock is now either committed before the close takes its checksum and ICR, 
or rejected with {{CLOSED_CONTAINER_IO}} without writing anything. Raft-applied 
PutBlocks and the gRPC path take no new lock; the block file is still closed 
outside the lock.

The mid stream part does not depend on anything else: {{streamCommand}} already 
fails on a non success result, and the patch turns today's 
{{IllegalStateException}} and leftover row into a plain {{CLOSED_CONTAINER_IO}} 
with no row. The stream close part depends on HDDS-17431: 
{{ContainerStateMachine.streamPutBlock}} ignores the PutBlock result, so a late 
stream close PutBlock that this patch now rejects would leave the block off 
that replica while the client is told the write succeeded. Land this patch with 
or after the fix for HDDS-17431.

HDDS-16828's proposed patch takes the container read lock at the same place in 
{{handlePutBlock}} for its finalized block check. Whichever lands second should 
put both checks in one locked section; with HDDS-16828 in, this fix reduces to 
repeating {{checkContainerOpen}} inside it.

Covered by two tests in the existing {{TestKeyValueHandler}}, each for both 
stream paths. {{testStreamPutBlockRacingCloseIsNotCommittedAfterClose}} runs 
the close while the PutBlock is at the commit and asserts that the PutBlock 
succeeds and the checksum taken at close covers its block; without the change 
it fails for both paths. 
{{testStreamPutBlockRejectedWhenContainerClosesBeforeCommit}} closes the 
container after the first open check and before the lock is taken, and asserts 
{{CLOSED_CONTAINER_IO}} and no block; with the re-check under the lock removed 
it fails for both paths. With the change {{TestKeyValueHandler}}, 
{{TestKeyValueHandlerWithUnhealthyContainer}}, {{TestKeyValueContainer}}, 
{{TestBlockManagerImpl}}, {{TestHddsDispatcher}}, 
{{TestKeyValueStreamDataChannel}}, {{TestCloseContainerCommandHandler}} and the 
container state machine leader and follower tests pass, 
{{TestBlockDataStreamOutput}} and {{TestContainerStateMachineStream}} pass on a 
mini cluster (with and without the stream PutBlock), and checkstyle is clean. 
With the patch the reproduction fails, because the PutBlock is committed before 
the close and the close ICR counts it.

Found by TLA+ model checking and code review of the datanode streaming PutBlock 
paths under HDDS-15926, on commit 7212465c0bb19615708c144238317db51c2a696d. 
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