Siyao Meng created HDDS-17418:
---------------------------------

             Summary: Pipeline write version uses the first datanode's version, 
so writes on looked up pipelines can carry a version above the lowest member
                 Key: HDDS-17418
                 URL: https://issues.apache.org/jira/browse/HDDS-17418
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng


h3. Mechanism
{{Pipeline.getWriteVersion}} returns the serialized current version of the 
first member. Its Javadoc assumes all members share one version. That holds for 
pipelines from block allocation, where 
{{ScmBlockLocationProtocolServerSideTranslatorPB.allocateScmBlock}} sets the 
minimum member version on every member. It does not hold for pipelines from a 
container lookup: 
{{StorageContainerLocationProtocolServerSideTranslatorPB.getContainerWithPipeline}}
 (and the batch variants) set each member's own live version since HDDS-16044. 
{{ContainerProtocolCalls}} copies {{getWriteVersion}} into 
{{writePipelineVersion}} for FinalizeBlock, PutBlock, WriteChunk and 
PutSmallFile, and {{BlockDataStreamOutput}} does the same for StreamInit.

h3. Trigger
# During a rolling upgrade, a Ratis pipeline has members at versions 101, 100 
and 100, with the upgraded datanode listed first.
# Lease recovery runs on a hsync'ed key. 
{{OMRecoverLeaseRequest.updateBlockInfo}} replaces the last block's pipeline 
with the result of {{getContainerWithPipeline}}.
# The file system client calls {{finalizeBlock}} 
({{BasicRootedOzoneClientAdapterImpl}} or {{BasicOzoneClientAdapterImpl}}), 
which sends FinalizeBlock with {{writePipelineVersion}} 101.

Argued, not run: {{XceiverClientManager}} caches the client by pipeline ID and 
type (plus the user in secure mode). If the process had no write client cached 
for that pipeline yet, the client created for FinalizeBlock keeps the looked up 
pipeline, and later WriteChunk and PutBlock calls from the same process to that 
pipeline take the version from it. If a write client was already cached, 
FinalizeBlock reuses it and its allocation pipeline, and sends the right value.

h3. Impact
None observable at this commit. The datanode stores the value in 
{{DispatcherContext}} ({{KeyValueHandler}}, {{ContainerStateMachine}}), but no 
main code calls {{DispatcherContext.getWriteVersion}}. The client 
PutBlock-without-Raft check 
({{BlockDataStreamOutput.allDatanodesSupportStreamPutBlock}}) checks each 
member itself, and FinalizeBlock has no version gate. It breaks the rule that a 
write pipeline never uses a version above its lowest member, and any datanode 
side gate that starts reading the write version would let such writes through.

h3. Reproduction
PASS on unmodified source. [^TestBugCR3WriteVersionFirstNode.java] 
({{hadoop-hdds/common}}) serializes one pipeline through both SCM paths, parses 
it as the client does, builds a FinalizeBlock request with 
{{ContainerProtocolCalls.finalizeBlock}} (only the transport is mocked), and 
derives the datanode write version with 
{{ClientCommandsUtils.getWritePipelineVersion}}. Output: allocation pipeline 
100; looked up pipeline 101 with lowest member 100; FinalizeBlock 
{{writePipelineVersion}} 101, datanode write version 
{{STREAM_PUTBLOCK_WITHOUT_RAFT}} (101). A passing test means the defect is 
present. No cluster was run with datanodes on two builds.

h3. Patch
[^CR-3-use-lowest-member-version-as-pipeline-write-version.patch], against 
5f7111a27ea07ce6b7bf5284a4213ebacb65f2ae. It also applies to master at 
e03ba37c41d.

{{getWriteVersion}} returns the lowest serialized member version. Negative 
values (unknown future versions, as in {{ComponentVersion.isSupportedBy}}) rank 
above all known versions, so a plain integer minimum is not used. The raw 
serialized value is still forwarded. Allocation pipelines get the same value as 
before.

New tests in the existing {{TestPipeline}} cover a newest first member, members 
with unknown versions, and a pipeline whose members are all {{UNKNOWN_VERSION}} 
(unchanged result). Without the change both fail ({{expected: <100> but was: 
<101>}} and {{expected: <100> but was: <-1>}}). 
{{TestContainerProtocolCalls.writeVersionPreservesOriginalSerializedValueFromWire}}
 relied on first member selection and now sets the undeserializable version on 
every member.

With the change, {{TestPipeline}}, {{TestContainerProtocolCalls}}, 
{{TestClientCommandsUtils}}, {{TestDatanodeDetails}}, the client 
{{TestBlockDataStreamOutput}}, {{TestKeyValueHandler}} and 
{{TestDispatcherContext}} pass, as do {{TestDatanodeCurrentVersionEndToEnd}} 
and {{TestBlockDataStreamOutput}} on a mini cluster. Checkstyle is clean. With 
the patch, the reproduction fails because the request carries 100.

Found by code review of the zero downtime upgrade version gating in SCM, OM and 
the datanode write path, as part of the TLA+ verification effort under 
HDDS-15926, on commit 5f7111a27ea07ce6b7bf5284a4213ebacb65f2ae. 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