Skip to content

HDDS-12991 recreate pipelines after enabling ratis write streaming#10842

Open
yandrey321 wants to merge 1 commit into
apache:masterfrom
yandrey321:HDDS-12991-part-2
Open

HDDS-12991 recreate pipelines after enabling ratis write streaming#10842
yandrey321 wants to merge 1 commit into
apache:masterfrom
yandrey321:HDDS-12991-part-2

Conversation

@yandrey321

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

SCM refreshes a datanode's ports on re-registration (SCMNodeManager)

  1. When a datanode re-registers with a changed port set — e.g. it restarted with DataStream enabled and now advertises the RATIS_DATASTREAM port — SCM updates its stored NodeManager record instead of keeping the stale one. Previously a port-only change (same IP/hostname/version) was ignored, so SCM never learned the new port and every new pipeline stayed port-less.

  2. New PipelineManager.closeNonStreamablePipelines(): closes+deletes OPEN pipelines whose persisted node snapshot lacks a port the currently-registered nodes now expose BackgroundPipelineCreator then builds fresh, streaming-capable replacements. Runs automatically as part of the pipeline scrubber: scrubAndCloseNonStreamablePipelines() (scrubs, then closes non-streamable) is wired into the BackgroundPipelineScrubber.

Why close-and-recreate rather than heal in place: a pipeline created before DataStream persists a stale dataStreamAddress (the gRPC port) in its Raft group configuration. ServerState.setRaftConf() only re-adds peers to the gRPC RPC, not the datastream RPC, so the group can never stream in place — the group must be recreated.

What is the link to the Apache JIRA

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

How was this patch tested?

TestSCMNodeManager: testRegisterRefreshesPortsOnPortChange (ports changed → node record updated) and testRegisterKeepsPortsWhenUnchanged (unchanged → no update).
TestPipelineManagerImpl: coverage for closeNonStreamablePipelines() — closes a portless OPEN pipeline once nodes expose the new port, and is a no-op when port sets already match.
Integration — TestOzoneFileSystemDataStreamEnablement (MiniOzoneCluster, datanodes start with DataStream disabled, client with streaming enabled):

testDataStreamFallbackAndPortRefresh: writes fall back gracefully while a pipeline is port-less; SCM refreshes the datanodes' ports on re-registration (validates piece 1).
testCloseNonStreamablePipelineThenStream: a portless legacy pipeline survives a rolling restart + SCM restart; closeNonStreamablePipelines() retires it; a fresh streaming-capable pipeline forms and a streaming write succeeds end-to-end (validates piece 2).
testBatchWritesAcrossStreamingEnablement: full lifecycle over a batch of files — several files written while disabled (all succeed via fallback), enable + close legacy pipeline, then several more files that all stream over a RATIS_DATASTREAM-capable pipeline; asserts no write fails and the post-enablement writes take the streaming path.

when part 1 (#10828) is missing:

mvn -pl hadoop-ozone/integration-test test \
  -Dtest='TestOzoneFileSystemDataStreamEnablement#testBatchWritesAcrossStreamingEnablement' \
  -DskipShade -DskipRecon \
  -Ddevelocity.cache.local.enabled=false -Ddevelocity.cache.remote.enabled=false -Dscan=false

Tests run: 1, Failures: 0, Errors: 1  <<< FAILURE!

java.io.IOException: RATIS_DATASTREAM port is missing for datanode <dn>
  in pipeline <id>; datastream is disabled for this pipeline
    at org.apache.hadoop.ozone.client.io.KeyDataStreamOutput.handleWrite(KeyDataStreamOutput.java:212)
    ...
Caused by: java.io.IOException: RATIS_DATASTREAM port is missing ...
    at org.apache.hadoop.hdds.scm.storage.BlockDataStreamOutput.setupStream(BlockDataStreamOutput.java:200)
    at org.apache.hadoop.hdds.scm.storage.BlockDataStreamOutput.<init>(BlockDataStreamOutput.java:173)

@yandrey321

Copy link
Copy Markdown
Contributor Author

@szetszwo

@chihsuan chihsuan 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.

Thanks for working on this! I left a few comments around the pipeline retirement condition but the overall approach looks reasonable.

Also, the three integration tests each start their own MiniOzoneCluster, which probably would add noticeable CI cost. Would you consider combining these into one end-to-end lifecycle test?

.map(DatanodeDetails.Port::getName).collect(Collectors.toSet());
return current.getPorts().stream()
.map(DatanodeDetails.Port::getName)
.anyMatch(name -> !storedNames.contains(name));

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.

SCMNodeManager.portsChanged handles name=value changes, while this check only compares names. Should we also compare the RATIS_DATASTREAM port value here?

return false;
}

private static boolean exposesNewPorts(DatanodeDetails stored,

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.

I wonder if we could narrow this check to RATIS pipelines and the RATIS_DATASTREAM port specifically. Older pipeline snapshots may legitimately lack ports added by later versions, such as HTTP, HTTPS, or CLIENT_RPC. In that case, checking any new port could retire unrelated pipelines, including EC pipelines.

private boolean nodesExposeNewPorts(Pipeline pipeline) {
for (DatanodeDetails stored : pipeline.getNodes()) {
final DatanodeDetails current = nodeManager.getNode(stored.getID());
if (current != null && exposesNewPorts(stored, current)) {

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.

One concern during a rolling restart: this may close the pipeline as soon as the first member advertises the new port. A replacement could still include datanodes that have not restarted yet, leading to another close-and-recreate cycle later. Would it make sense if we defer retirement until SCM can ensure the replacement uses only DataStream-capable nodes?

* succeeds over the new pipeline (HDDS-12991).
*/
@Test
@Timeout(value = 70, unit = TimeUnit.SECONDS)

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.

Would it make sense to rely on the repository’s five-minute default timeout here? This test waits up to 30 seconds for port registration both before and after the SCM restart, then up to 60 seconds for a replacement pipeline.

With cluster startup and restarts as well, the 70-second outer timeout may fire first and hide which operation was actually slow.

The same concern also applies to lines 225 and 318.

@szetszwo szetszwo 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.

@yandrey321 , thanks for working on this!

Please review the AI generated code carefully before submitting.

  • The code is inconsistent -- streaming specific vs any new ports
  • The javadocs/comments are very long and keep repeating.

It is wasting the time of the reviewers of this PR.

Comment on lines +614 to +620
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));

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.

Move it to DatanodeDetails and make it non-static.

* created in their place. A pre-datastream Raft group cannot be made
* streamable in place, so it must be recreated (HDDS-12991).
*/
void closeNonStreamablePipelines();

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.

The implementation is closing all the pipelines with new port but specific to Streaming. Please update the name and javadoc.

}

@Override
public void closeNonStreamablePipelines() {

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.

The implementation is closing all the pipelines with new port but specific to Streaming. Please change the name to reflect it.

Comment on lines +588 to +589
LOG.info("Closing non-streamable pipeline {} so a streaming-capable "
+ "pipeline can replace it", id);

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.

Again, the implementation is closing all the pipelines with new port. The log message is very misleading.

Comment on lines +561 to +570
/**
* Close (and delete) OPEN pipelines that predate a datanode capability the
* registered node now advertises but the pipeline's stored node snapshot
* lacks — in practice the RATIS_DATASTREAM port after Ratis DataStream was
* enabled. Such pipelines cannot serve streaming even after the datanodes
* restart, because the Raft group's persisted configuration still carries the
* stale datastream address; only a freshly created pipeline is
* streaming-capable. BackgroundPipelineCreator recreates replacements from
* the now-capable nodes (HDDS-12991).
*/

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.

This kind of long javadoc repeated multiple times talking about the same thing through out this PR. It will be very maintain it for the future changes. Please review and revise the AI generated javadocs/comments before submitting a 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.

3 participants