Skip to content

HDDS-12991. Support enabling Ratis DataStream on an existing cluster#10746

Open
yandrey321 wants to merge 3 commits into
apache:masterfrom
yandrey321:HDDS-12991
Open

HDDS-12991. Support enabling Ratis DataStream on an existing cluster#10746
yandrey321 wants to merge 3 commits into
apache:masterfrom
yandrey321:HDDS-12991

Conversation

@yandrey321

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Root cause

A datanode advertises its RATIS_DATASTREAM port in DatanodeDetails only when streaming is enabled at start-up, and a pipeline persists a snapshot of its members' DatanodeDetails. On a cluster that predates streaming:

  1. When a datanode re-registers after streaming is enabled, SCMNodeManager.register() refreshed the stored node record only on an IP/hostname or version change, so the newly-exposed RATIS_DATASTREAM port was dropped (HDDS-15799).
  2. DatanodeDetails.getPort(RATIS_DATASTREAM) silently falls back to the RATIS/gRPC port, so a streaming client could target the wrong port (HDDS-8687).
  3. A pipeline created before streaming keeps a portless member snapshot. Even after its datanodes restart with streaming enabled, that pipeline cannot serve a stream: its Ratis group's persisted configuration was built with a datastream address that resolves to the gRPC port, and the group configuration is not rebuilt on restart. Such a pipeline has to be recreated to become streaming-capable.

Changes

  1. SCM refreshes a datanode's ports on re-registration — SCMNodeManager. When a registered datanode re-registers with a changed port set (e.g. now exposing RATIS_DATASTREAM), SCM updates its stored node record, comparing ports as a name→value set. This reuses the existing NodeStateManager.updateNode path; node state is rebuilt from registrations, so there is no schema or upgrade impact.

  2. The streaming client degrades gracefully instead of failing — hadoop-hdds/client, hadoop-ozone/client, hadoop-ozone/common, ozonefs-common. A new StreamNotSupportedException is thrown by BlockDataStreamOutput.setupStream when a pipeline node lacks the RATIS_DATASTREAM port (using the explicit hasPort check, without changing getPort fallback semantics). KeyDataStreamOutput propagates it recognizably, and SelectorOutputStream recovers on the first flush by closing the streaming stream and replaying the buffered data to a non-streaming fallback stream, wired in BasicOzoneFileSystem/BasicRootedOzoneFileSystem. Writes to a not-yet-streaming pipeline therefore keep succeeding.

  3. SCM replaces pipelines that cannot stream — PipelineManager. A new closeNonStreamablePipelines(), run from the background pipeline scrubber, closes and deletes OPEN pipelines whose registered datanodes now expose a port the pipeline's stored snapshot lacks. BackgroundPipelineCreator then builds fresh, streaming-capable pipelines from the now-capable nodes. Closing (rather than editing the snapshot) is required because a pre-streaming Ratis group cannot be made streamable in place. It reuses the existing replicated closePipeline/deletePipeline operations, so no new HA/Ratis operation is introduced.

Net effect: after streaming is enabled and the datanodes restart, writes keep succeeding via the fallback while SCM replaces the legacy pipelines; once the new pipelines exist, streaming writes flow over them — no metadata reset required.

Co-authored with Cloude Code

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-12991
https://issues.apache.org/jira/browse/HDDS-15799
https://issues.apache.org/jira/browse/HDDS-8687

How was this patch tested?

  • Unit: TestSCMNodeManager (port refresh on re-registration; no refresh when unchanged); TestSelectorOutputStream (fallback on first flush for write(int) and write(byte[]), buffered-data replay, failing-close handling, no-fallback propagation, hasCapability); TestPipelineManagerImpl (closeNonStreamablePipelines closes a legacy pipeline while leaving streaming-capable, unregistered-node, and non-open pipelines untouched; the scrub-and-close background task). New non-test code has 100% line and branch coverage.
  • Integration: TestOzoneFileSystemWithStreamingDisabledDatanode (a streaming write falls back and round-trips over both o3fs and ofs); new TestOzoneFileSystemDataStreamEnablement: (a) after a rolling restart enables datastream, SCM refreshes the node ports and writes keep succeeding via fallback; (b) a pipeline that predates datastream is closed and a fresh one is created, over which a streaming write then succeeds end-to-end. Both integration tests use bounded @Timeouts.

@peterxcli

peterxcli commented Jul 14, 2026

Copy link
Copy Markdown
Member

@yandrey321

Copy link
Copy Markdown
Contributor Author

might relate to https://github.com/apache/ozone/pull/10743/changes#r3573417136

this one would be the separate work item.

@jojochuang

Copy link
Copy Markdown
Contributor

@ss77892

@szetszwo

Copy link
Copy Markdown
Contributor

@yandrey321 , thanks for working on this!

This AI generated PR has the usual problems of AI generated PRs:

Could you clean them up.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables turning on Ratis DataStream in an existing Ozone cluster without requiring metadata resets by (1) ensuring SCM refreshes datanode port changes on re-registration, (2) making the client detect non-streamable pipelines and fall back to non-streaming writes, and (3) having SCM proactively replace legacy pipelines whose persisted member snapshots cannot support streaming.

Changes:

  • Refresh SCM’s stored datanode port set on re-registration when ports (eg RATIS_DATASTREAM) change.
  • Introduce StreamNotSupportedException and a client-side streaming→non-streaming fallback path (SelectorOutputStream + OzoneFS wiring).
  • Close/delete legacy OPEN pipelines that can’t be made streamable in place, plus add unit/integration coverage for end-to-end enablement.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java Wires a non-streaming fallback selector into FS create output stream.
hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicOzoneFileSystem.java Same fallback wiring for o3fs scheme.
hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFileSystemWithStreamingDisabledDatanode.java Updates integration test to validate fallback + correct round-trip instead of fail-fast.
hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFileSystemDataStreamEnablement.java New end-to-end tests for port refresh + legacy pipeline replacement + eventual streaming success.
hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/client/io/TestSelectorOutputStream.java Adds unit tests for StreamNotSupportedException fallback behavior and capability delegation.
hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/client/io/SelectorOutputStream.java Adds fallback selector support and first-write recovery on StreamNotSupportedException.
hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/KeyDataStreamOutput.java Propagates StreamNotSupportedException cleanly to enable callers to fall back.
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/pipeline/TestPipelineManagerImpl.java Unit tests for closing non-streamable pipelines and scrubber wiring.
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/pipeline/MockPipelineManager.java Implements new PipelineManager API for tests.
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestSCMNodeManager.java Unit tests validating SCM port refresh on re-registration.
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/pipeline/PipelineManagerImpl.java Adds background scrubber hook + logic to close/delete pipelines that can’t stream.
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/pipeline/PipelineManager.java Adds closeNonStreamablePipelines() to the PipelineManager interface.
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeManager.java Adds port-set change detection and triggers nodeStateManager update when ports change.
hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamNotSupportedException.java New exception type to distinguish “pipeline cannot stream” from generic IO failures.
hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java Throws StreamNotSupportedException when a pipeline lacks RATIS_DATASTREAM ports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +158 to +164
LOG.warn("Streaming output is not supported for this write; "
+ "falling back to the non-streaming output stream.", e);
try {
out.close();
} catch (IOException suppressed) {
e.addSuppressed(suppressed);
}
Comment on lines +614 to +621
private static boolean exposesNewPorts(DatanodeDetails stored,
DatanodeDetails current) {
final Set<DatanodeDetails.Port.Name> storedNames = stored.getPorts().stream()
.map(DatanodeDetails.Port::getName).collect(Collectors.toSet());
return current.getPorts().stream()
.map(DatanodeDetails.Port::getName)
.anyMatch(name -> !storedNames.contains(name));
}
@amaliujia
amaliujia self-requested a review July 21, 2026 08:36

@amaliujia amaliujia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the code and I had the same feeling above: it might be much better if this PR can be split. Smaller PRs can be used to divide and conquer the problems.

I personal found it is hard for verify if the testing is complete or sufficient for a specific fix because there are many test code involved.

*/
private static boolean portsChanged(DatanodeDetails oldNode,
DatanodeDetails newNode) {
return !portValues(oldNode).equals(portValues(newNode));

@amaliujia amaliujia Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious that if we know that we are fixing for DatanodeDetails.Port.Name.RATIS_DATASTREAM missing issue, can we only look for the case in which the newNode has the data stream port while the old node does not, thus we need to do an update?

It does not look wrong that we compare entire port set, but the side effect is I do not know if there is any port that do not need to be updated.

@peterxcli
peterxcli self-requested a review July 21, 2026 09:26
@yandrey321

Copy link
Copy Markdown
Contributor Author

@szetszwo @jojochuang @amaliujia Please review the first subset of these changes: #10828

I'll add another PR for the server side part of this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants