[
https://issues.apache.org/jira/browse/HDDS-17431?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-17431:
------------------------------
Affects Version/s: 2.3.0
> With PutBlock without Raft, a failed data stream close is reported as
> success, so OM commits a block that no replica or only one replica has
> --------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-17431
> URL: https://issues.apache.org/jira/browse/HDDS-17431
> Project: Apache Ozone
> Issue Type: Bug
> Affects Versions: 2.3.0
> Reporter: Siyao Meng
> Priority: Major
> Attachments: MC-1-fail-stream-close-on-rejected-putblock.patch,
> TestBugCR5StreamCloseFailureIgnored.java,
> TestBugMC1ClosedContainerStreamClose.java
>
>
> h3. Mechanism
> With {{ozone.client.datastream.putblock.without.raft.enabled=true}} (client)
> and {{hdds.container.ratis.datastream.enabled=true}} (datanode), both false
> by default, the final PutBlock of a streaming write is appended to the data
> stream and committed by each datanode when the stream closes
> ({{StreamInitWithPutBlock}}). The close reply from Ratis is the only signal
> that this commit happened, and two places lose it:
> * Datanode: {{ContainerStateMachine.streamPutBlock}} ignores the response of
> {{dispatchCommand}}. When the dispatcher rejects the PutBlock (for example
> {{CLOSED_CONTAINER_IO}}, "Requested operation not allowed as ContainerState
> is CLOSED"), {{KeyValueStreamDataChannel.closeWithStreamPutBlock}} still
> calls {{setLinked()}}, the channel close succeeds and Ratis replies success
> to the close. The mid stream PutBlock
> ({{ContainerStateMachine.streamCommand}}) already throws on a result other
> than SUCCESS; the close path does not.
> * Client: {{BlockDataStreamOutput.close()}} waits for the close reply but
> never checks {{isSuccess()}}. A close that fails on the pipeline (Ratis
> replies success=false when the local close throws or a remote close fails) is
> handled only by a {{whenComplete}} callback in {{executePutBlock}} that calls
> {{executePutBlock(true, false)}} again. That call cannot send anything:
> {{buffersForPutBlock}} is already null (NullPointerException) or the stream
> is already closed (IOException), and the exception is thrown inside a future
> nobody reads. So {{close()}} returns, {{KeyDataStreamOutput}} never retries,
> and OM commits the key on the original block.
> The Raft path (the default) is not affected in the same way, because there
> the separate Raft PutBlock is the commit and its failure is reported.
> h3. Trigger
> Datanode streaming enabled, a client that writes through the streaming API
> with PutBlock without Raft enabled, and all datanodes of the pipeline at the
> version that supports it. Then either:
> * the container is closed while the stream is open (container full, pipeline
> close, or an admin close), so every datanode rejects the close PutBlock, or
> * the followers of the stream pipeline fail after the last data packet was
> acknowledged and before the close, so the close fails on the pipeline while
> the primary has already committed the PutBlock locally.
> h3. Impact
> Reproduced on unmodified source with a 5 datanode mini cluster through the
> public client API:
> * *Container closed before the stream close, nothing committed yet.* 50 bytes
> written, container closed through SCM, then {{key.close()}}. The close
> returned normally, OM committed 50 bytes on the original block, the replicas
> logged {{op=PUT_BLOCK ... ret=FAILURE}} and have no such block, and reading
> the key fails with "Unable to find the block". With PutBlock through Raft the
> client rewrites the data on a new block instead.
> [^TestBugMC1ClosedContainerStreamClose.java]
> * *Container closed before the stream close, part of the block committed.*
> 500 bytes written with a PutBlock in the middle of the stream at 400 bytes,
> which all replicas committed. After the container close and {{key.close()}},
> OM has 500 bytes on the original block, every replica has 400, and reading
> the key fails with "Inconsistent read".
> [^TestBugMC1ClosedContainerStreamClose.java]
> * *Followers lost before the stream close.* 100 bytes written and
> acknowledged, both followers shut down, then {{key.close()}}. The client
> logged {{Failed executePutBlockClose, reply=...,FAILED}}, {{close()}}
> returned normally, OM committed the original block, the primary has it with
> length 100 and both followers still lack it after restart. The key is
> readable only while the primary lives.
> [^TestBugCR5StreamCloseFailureIgnored.java]
> In all cases the writer is told the data is stored. Nothing repairs it
> automatically: SCM replication does not see a difference between the
> replicas. Admin triggered container reconciliation was not tried.
> PutBlock without Raft is part of HDDS-15463 (Streaming Write Pipeline without
> Raft), is off by default and is not in any release yet. It came with the
> commits "Streaming Write also commit PutBlock at the time of closing"
> (HDDS-15792), "Commit PutBlock without Raft in Client" (HDDS-15758) and
> "PutBlocks from Flushes also go without Raft" (HDDS-16008). The callback that
> cannot retry dates from HDDS-6500 and is in released versions, but there it
> is harmless because the Raft PutBlock is the commit.
> h3. Reproduction
> The two attached classes are self contained JUnit tests for
> {{hadoop-ozone/integration-test}} (target path and Maven command in each
> class comment). Each starts its own mini cluster and uses only the public
> client API, {{OzoneTestHelper.waitForContainerClose}}, datanode shutdown and
> restart, and reads of the datanode's block metadata, with no injected state
> and no sleeps. They pass on unmodified source when the bug is present and
> fail with the patch.
> h3. Patch
> [^MC-1-fail-stream-close-on-rejected-putblock.patch], against
> 7212465c0bb19615708c144238317db51c2a696d. It also applies to master at
> ae71e2dd50c.
> * {{ContainerStateMachine.streamPutBlock}} throws a
> {{StorageContainerException}} when the result is not SUCCESS, as
> {{streamCommand}} does. {{closeWithStreamPutBlock}} then does not mark the
> channel linked, the close fails, Ratis replies success=false and the client
> does not send the link request.
> * With PutBlock without Raft, {{BlockDataStreamOutput}} handles the close
> reply like the reply to a mid stream PutBlock command: on success=false it
> fails the stream, so {{KeyDataStreamOutput}} excludes the pipeline and writes
> the unacknowledged bytes again on a new block; on success it registers the
> buffers the close PutBlock covered, so that they are released like those of
> the Raft PutBlock. {{executePutBlock}} no longer registers the callback that
> cannot retry on this path.
> The release is needed for the retry to be correct. All blocks of a key share
> one buffer list, and before this patch a successful close without Raft never
> released its buffers. A retry on a later block then wrote the previous
> block's tail into the new block as well, and the key commit failed with an
> {{IllegalArgumentException}} ({{BlockDataStreamOutputEntryPool.commitKey}},
> offset does not match the block lengths). That already happened before this
> patch when a PutBlock in the middle of a later block failed; with the close
> failures now surfaced it would also hit a key whose last close fails,
> including a key whose data was all hsynced before its container closed. With
> the patch these keys commit and read back correctly (checked on a mini
> cluster with 1000 byte blocks: a failed close of the second block, a failed
> mid stream PutBlock of the second block, and a hsynced second block whose
> container closes before the close).
> Both datanode and client changes are needed for the container close case:
> with only the client change the close reply is still success, with only the
> datanode change the client ignores the failed reply (both checked with the
> new integration test). The followers case needs only the client change; there
> the rewrite, like the one after a failed Raft PutBlock, succeeds or fails
> loudly depending on whether SCM still hands out pipelines with a stopped
> datanode, and while it does the client can keep retrying for minutes (about
> two minutes in one run); that retry behaviour predates this patch. Nothing
> changes when PutBlock goes through Raft, and there is no protocol or
> configuration change; a client and its datanodes both need the patch for the
> container close case.
> Tests:
> *
> {{TestBlockDataStreamOutput#closeFailsIfStreamCloseCommittingPutBlockFails}}
> and {{#closeReleasesBuffersIfStreamCloseCommittingPutBlockSucceeds}} (mocked
> pipeline, close replied with success=false or success).
> *
> {{TestKeyDataStreamOutput#failedStreamCloseOfSecondBlockRewritesOnlyItsData}}
> (mocked pipelines, two block key whose second close fails).
> * {{ContainerStateMachineTests#testStreamPutBlockFailsIfDispatchFails}} and
> {{#testStreamPutBlockSucceedsIfDispatchSucceeds}}.
> *
> {{TestContainerStateMachineStream#testContainerClosedBeforeStreamCloseWithPutBlockWithoutRaft}}
> (shared non HA cluster: write, close the container, close the key, read it
> back).
> All of them except {{#testStreamPutBlockSucceedsIfDispatchSucceeds}} fail
> without the change; that one guards the success path. With the change they
> pass, as do {{TestBlockDataStreamOutput}}, {{TestClientThreadShutdown}},
> {{TestContainerStateMachineLeader}}, {{TestContainerStateMachineFollower}},
> {{TestKeyValueStreamDataChannel}}, {{TestKeyDataStreamOutput}},
> {{TestContainerStateMachineStream}}, the integration
> {{TestBlockDataStreamOutput}} and {{TestOzoneFileSystemWithStreaming}}, and
> checkstyle is clean.
> 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. Related but not
> duplicates: HDDS-16265 (the same callback, described as an unbounded retry
> whose failure is non critical because the Raft PutBlock does the commit;
> without Raft the close PutBlock is the commit, and on either path the
> re-entry fails before sending anything, so after this patch HDDS-16265
> concerns only the Raft path), HDDS-16828 (a stream PutBlock accepted on a
> finalized block), HDDS-16822 (hsync with PutBlock through Raft) and PR #11384
> (HDDS-16191, refactors the same method). This should be fixed before PutBlock
> without Raft (HDDS-15463) is enabled. 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]