[ 
https://issues.apache.org/jira/browse/HDDS-17689?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Tsz-wo Sze updated HDDS-17689:
------------------------------
    Description: 
When a pipeline is created, do datanodes have the ReplicationType of the 
pipeline?  If yes, the CHAINED pipelines can create only CHAINED containers.  
For stream init, datanode can recognises it by the pipeline ReplicationType.  
Is it correct?
----
Claude:
The datanode is sent the pipeline's ReplicationType, but today it throws it 
away. Your approach works once the datanode stores the type for each Raft group 
and still has it after a restart.

What happens today
- SCM sends CreatePipelineCommandProto \{ pipelineID, type, factor, datanode, 
... }, so each datanode does get CHAINED.
- CreatePipelineCommandHandler reads createCommand.getReplicationType() but 
only logs it. XceiverServerRatis#addGroup(pipelineId, peers, priorityList) has 
no type parameter, and ActivePipelineContext has no type field.
- The datanode that handles the command first creates the group on its peers 
through getGroupManagementApi(peer).add(group). When a peer's own command 
arrives, server.isExist(...) is already true, so the handler skips that whole 
block (CreatePipelineCommandHandler:122). A peer can therefore have a CHAINED 
group without ever recording its type, even if we add a type parameter to 
addGroup.
- On restart, Ratis reloads groups from its storage. That storage has no type, 
and no new command is sent for those groups.

What makes the approach correct
1. Record the type for each group. XceiverServerRatis keeps RaftGroupId → 
ReplicationType. CreatePipelineCommandHandler sets it on every command, 
including the case where the group already exists.
2. Persist it. Write a small marker (for example a type file) in the group's 
state machine storage directory, and reload it with the group on restart. This 
adds new on-disk datanode metadata, so I'd like your sign-off before putting it 
in the RFC (CLAUDE.md's "ask first" rule). The other option, SCM re-sending the 
type, would need a new command or a change to the heartbeat response, which is 
a proto change.
3. Look the type up at stream init, not when the state machine is built. The 
state machine can exist before the type is known (the peer case above). Stream 
requests already carry getGroupId() (ContainerStateMachine:518,555), so 
stream() can look up the type when the request arrives. If it is still unknown, 
stream init should fail with a retriable error so the client retries or 
excludes that datanode. This is a small window, because SCM opens a pipeline as 
soon as every datanode reports the group.

What follows from it
- Containers. A CHAINED pipeline creates only CHAINED containers, as you said. 
If the group is CHAINED, stream init skips container2BCSIDMap.putIfAbsent. That 
one decision keeps CHAINED containers away from the notifyGroupRemove 
quasi-close and the UNHEALTHY marking on restart.
- Close. If the pipeline still exists and its group is CHAINED, the datanode 
closes the container directly. If the pipeline is gone, the close is already 
force=true and closes directly. So the close path doesn't need the container's 
own type persisted. This replaces the "force and not in container2BCSIDMap" 
heuristic now in §4.2.
- No proto change. The stream header doesn't change; option (b) in open 
question 8 isn't needed.

----
For "still has it after a restart", we can close all the CHAINED pipelines if a 
datanode restart.
----
Claude:
Yes, that works. Closing every CHAINED pipeline on a restarted datanode means 
the type only has to live in memory, so nothing new goes on disk. Two pieces 
are missing, though.

1. Today nothing triggers the close.
- When a datanode restarts it registers again. For a node SCM already knows, 
SCMNodeManager#register (the else branch, around line 470) only updates the 
address, version or ports. It fires no event and doesn't touch pipelines.
- StaleNodeHandler closes a node's pipelines only after the stale interval 
(ozone.scm.stale.node.interval), so a fast restart never reaches it.
- So the RFC needs one SCM change: when a known datanode re-registers, close 
the CHAINED pipelines that contain it. RATIS pipelines are left alone. Closing 
goes through the normal path: the containers get force=true closes and the 
datanodes get ClosePipelineCommand.

2. There is a short window right after restart.
- VersionEndpointTask:90 calls ozoneContainer.start(clusterId), which starts 
the Ratis and DataStream servers. That happens before registration, and 
ClosePipelineCommand only arrives later in a heartbeat response.
- In between, the datanode has reloaded the Raft group, doesn't know it's 
CHAINED, and can still accept streams. That can come from a client that got a 
block on this pipeline before SCM closed it.
- If the datanode treats such a stream as RATIS, it adds a newly created 
container to container2BCSIDMap. When the group is removed, notifyGroupRemove 
then quasi-closes that replica, which breaks the "never QUASI_CLOSED" rule.

Ways to close the window:
- (a) Reject unknown-type groups. For any group reloaded at startup whose type 
was never recorded, the datanode rejects StreamInitWithPutBlock with a 
retriable error. The client excludes the pipeline and gets a new block. The 
cost is that RATIS pipelines using 
ozone.client.datastream.putblock.without.raft.enabled are also refused after a 
restart, until those pipelines are replaced. Plain V2 StreamInit is not 
affected.
- (b) Never use the map for this request type. StreamInitWithPutBlock never 
adds to container2BCSIDMap when the group's type is unknown, which makes the 
window harmless for CHAINED. The cost is that RATIS containers written that way 
after a restart are left out of the bcsId restart check. That check means 
little for them anyway, because they also have no Raft write entries.


  was:
When a pipeline is created, do datanodes have the ReplicationType of the 
pipeline?  If yes, the CHAINED pipelines can create only CHAINED containers.  
For stream init, datanode can recognises it by the pipeline ReplicationType.  
Is it correct?
----
Claude:
The datanode is sent the pipeline's ReplicationType, but today it throws it 
away. Your approach works once the datanode stores the type for each Raft group 
and still has it after a restart.

What happens today
- SCM sends CreatePipelineCommandProto \{ pipelineID, type, factor, datanode, 
... }, so each datanode does get CHAINED.
- CreatePipelineCommandHandler reads createCommand.getReplicationType() but 
only logs it. XceiverServerRatis#addGroup(pipelineId, peers, priorityList) has 
no type parameter, and ActivePipelineContext has no type field.
- The datanode that handles the command first creates the group on its peers 
through getGroupManagementApi(peer).add(group). When a peer's own command 
arrives, server.isExist(...) is already true, so the handler skips that whole 
block (CreatePipelineCommandHandler:122). A peer can therefore have a CHAINED 
group without ever recording its type, even if we add a type parameter to 
addGroup.
- On restart, Ratis reloads groups from its storage. That storage has no type, 
and no new command is sent for those groups.

What makes the approach correct
1. Record the type for each group. XceiverServerRatis keeps RaftGroupId → 
ReplicationType. CreatePipelineCommandHandler sets it on every command, 
including the case where the group already exists.
2. Persist it. Write a small marker (for example a type file) in the group's 
state machine storage directory, and reload it with the group on restart. This 
adds new on-disk datanode metadata, so I'd like your sign-off before putting it 
in the RFC (CLAUDE.md's "ask first" rule). The other option, SCM re-sending the 
type, would need a new command or a change to the heartbeat response, which is 
a proto change.
3. Look the type up at stream init, not when the state machine is built. The 
state machine can exist before the type is known (the peer case above). Stream 
requests already carry getGroupId() (ContainerStateMachine:518,555), so 
stream() can look up the type when the request arrives. If it is still unknown, 
stream init should fail with a retriable error so the client retries or 
excludes that datanode. This is a small window, because SCM opens a pipeline as 
soon as every datanode reports the group.

What follows from it
- Containers. A CHAINED pipeline creates only CHAINED containers, as you said. 
If the group is CHAINED, stream init skips container2BCSIDMap.putIfAbsent. That 
one decision keeps CHAINED containers away from the notifyGroupRemove 
quasi-close and the UNHEALTHY marking on restart.
- Close. If the pipeline still exists and its group is CHAINED, the datanode 
closes the container directly. If the pipeline is gone, the close is already 
force=true and closes directly. So the close path doesn't need the container's 
own type persisted. This replaces the "force and not in container2BCSIDMap" 
heuristic now in §4.2.
- No proto change. The stream header doesn't change; option (b) in open 
question 8 isn't needed.
----
For "still has it after a restart", we can close all the CHAINED pipelines if a 
datanode restart.
----
Claude:
Yes, that works. Closing every CHAINED pipeline on a restarted datanode means 
the type only has to live in memory, so nothing new goes on disk. Two pieces 
are missing, though.

1. Today nothing triggers the close.
- When a datanode restarts it registers again. For a node SCM already knows, 
SCMNodeManager#register (the else branch, around line 470) only updates the 
address, version or ports. It fires no event and doesn't touch pipelines.
- StaleNodeHandler closes a node's pipelines only after the stale interval 
(ozone.scm.stale.node.interval), so a fast restart never reaches it.
- So the RFC needs one SCM change: when a known datanode re-registers, close 
the CHAINED pipelines that contain it. RATIS pipelines are left alone. Closing 
goes through the normal path: the containers get force=true closes and the 
datanodes get ClosePipelineCommand.

2. There is a short window right after restart.
- VersionEndpointTask:90 calls ozoneContainer.start(clusterId), which starts 
the Ratis and DataStream servers. That happens before registration, and 
ClosePipelineCommand only arrives later in a heartbeat response.
- In between, the datanode has reloaded the Raft group, doesn't know it's 
CHAINED, and can still accept streams. That can come from a client that got a 
block on this pipeline before SCM closed it.
- If the datanode treats such a stream as RATIS, it adds a newly created 
container to container2BCSIDMap. When the group is removed, notifyGroupRemove 
then quasi-closes that replica, which breaks the "never QUASI_CLOSED" rule.

Ways to close the window:
- (a) Reject unknown-type groups. For any group reloaded at startup whose type 
was never recorded, the datanode rejects StreamInitWithPutBlock with a 
retriable error. The client excludes the pipeline and gets a new block. The 
cost is that RATIS pipelines using 
ozone.client.datastream.putblock.without.raft.enabled are also refused after a 
restart, until those pipelines are replaced. Plain V2 StreamInit is not 
affected.
- (b) Never use the map for this request type. StreamInitWithPutBlock never 
adds to container2BCSIDMap when the group's type is unknown, which makes the 
window harmless for CHAINED. The cost is that RATIS containers written that way 
after a restart are left out of the bcsId restart check. That check means 
little for them anyway, because they also have no Raft write entries.



> Support CHAINED pipeline in datadnodes
> --------------------------------------
>
>                 Key: HDDS-17689
>                 URL: https://issues.apache.org/jira/browse/HDDS-17689
>             Project: Apache Ozone
>          Issue Type: Sub-task
>          Components: Ozone Datanode
>            Reporter: Tsz-wo Sze
>            Priority: Major
>
> When a pipeline is created, do datanodes have the ReplicationType of the 
> pipeline?  If yes, the CHAINED pipelines can create only CHAINED containers.  
> For stream init, datanode can recognises it by the pipeline ReplicationType.  
> Is it correct?
> ----
> Claude:
> The datanode is sent the pipeline's ReplicationType, but today it throws it 
> away. Your approach works once the datanode stores the type for each Raft 
> group and still has it after a restart.
> What happens today
> - SCM sends CreatePipelineCommandProto \{ pipelineID, type, factor, datanode, 
> ... }, so each datanode does get CHAINED.
> - CreatePipelineCommandHandler reads createCommand.getReplicationType() but 
> only logs it. XceiverServerRatis#addGroup(pipelineId, peers, priorityList) 
> has no type parameter, and ActivePipelineContext has no type field.
> - The datanode that handles the command first creates the group on its peers 
> through getGroupManagementApi(peer).add(group). When a peer's own command 
> arrives, server.isExist(...) is already true, so the handler skips that whole 
> block (CreatePipelineCommandHandler:122). A peer can therefore have a CHAINED 
> group without ever recording its type, even if we add a type parameter to 
> addGroup.
> - On restart, Ratis reloads groups from its storage. That storage has no 
> type, and no new command is sent for those groups.
> What makes the approach correct
> 1. Record the type for each group. XceiverServerRatis keeps RaftGroupId → 
> ReplicationType. CreatePipelineCommandHandler sets it on every command, 
> including the case where the group already exists.
> 2. Persist it. Write a small marker (for example a type file) in the group's 
> state machine storage directory, and reload it with the group on restart. 
> This adds new on-disk datanode metadata, so I'd like your sign-off before 
> putting it in the RFC (CLAUDE.md's "ask first" rule). The other option, SCM 
> re-sending the type, would need a new command or a change to the heartbeat 
> response, which is a proto change.
> 3. Look the type up at stream init, not when the state machine is built. The 
> state machine can exist before the type is known (the peer case above). 
> Stream requests already carry getGroupId() (ContainerStateMachine:518,555), 
> so stream() can look up the type when the request arrives. If it is still 
> unknown, stream init should fail with a retriable error so the client retries 
> or excludes that datanode. This is a small window, because SCM opens a 
> pipeline as soon as every datanode reports the group.
> What follows from it
> - Containers. A CHAINED pipeline creates only CHAINED containers, as you 
> said. If the group is CHAINED, stream init skips 
> container2BCSIDMap.putIfAbsent. That one decision keeps CHAINED containers 
> away from the notifyGroupRemove quasi-close and the UNHEALTHY marking on 
> restart.
> - Close. If the pipeline still exists and its group is CHAINED, the datanode 
> closes the container directly. If the pipeline is gone, the close is already 
> force=true and closes directly. So the close path doesn't need the 
> container's own type persisted. This replaces the "force and not in 
> container2BCSIDMap" heuristic now in §4.2.
> - No proto change. The stream header doesn't change; option (b) in open 
> question 8 isn't needed.
> ----
> For "still has it after a restart", we can close all the CHAINED pipelines if 
> a datanode restart.
> ----
> Claude:
> Yes, that works. Closing every CHAINED pipeline on a restarted datanode means 
> the type only has to live in memory, so nothing new goes on disk. Two pieces 
> are missing, though.
> 1. Today nothing triggers the close.
> - When a datanode restarts it registers again. For a node SCM already knows, 
> SCMNodeManager#register (the else branch, around line 470) only updates the 
> address, version or ports. It fires no event and doesn't touch pipelines.
> - StaleNodeHandler closes a node's pipelines only after the stale interval 
> (ozone.scm.stale.node.interval), so a fast restart never reaches it.
> - So the RFC needs one SCM change: when a known datanode re-registers, close 
> the CHAINED pipelines that contain it. RATIS pipelines are left alone. 
> Closing goes through the normal path: the containers get force=true closes 
> and the datanodes get ClosePipelineCommand.
> 2. There is a short window right after restart.
> - VersionEndpointTask:90 calls ozoneContainer.start(clusterId), which starts 
> the Ratis and DataStream servers. That happens before registration, and 
> ClosePipelineCommand only arrives later in a heartbeat response.
> - In between, the datanode has reloaded the Raft group, doesn't know it's 
> CHAINED, and can still accept streams. That can come from a client that got a 
> block on this pipeline before SCM closed it.
> - If the datanode treats such a stream as RATIS, it adds a newly created 
> container to container2BCSIDMap. When the group is removed, notifyGroupRemove 
> then quasi-closes that replica, which breaks the "never QUASI_CLOSED" rule.
> Ways to close the window:
> - (a) Reject unknown-type groups. For any group reloaded at startup whose 
> type was never recorded, the datanode rejects StreamInitWithPutBlock with a 
> retriable error. The client excludes the pipeline and gets a new block. The 
> cost is that RATIS pipelines using 
> ozone.client.datastream.putblock.without.raft.enabled are also refused after 
> a restart, until those pipelines are replaced. Plain V2 StreamInit is not 
> affected.
> - (b) Never use the map for this request type. StreamInitWithPutBlock never 
> adds to container2BCSIDMap when the group's type is unknown, which makes the 
> window harmless for CHAINED. The cost is that RATIS containers written that 
> way after a restart are left out of the bcsId restart check. That check means 
> little for them anyway, because they also have no Raft write entries.



--
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