This is an automated email from the ASF dual-hosted git repository.

szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git


The following commit(s) were added to refs/heads/master by this push:
     new fd2dde635 RATIS-2564. Add an executor and terminalReply to stream read 
(#1486)
fd2dde635 is described below

commit fd2dde635c02ffc697eb7e782a2cbd4d10234778
Author: Peter Lee <[email protected]>
AuthorDate: Fri Jun 19 02:09:10 2026 +0800

    RATIS-2564. Add an executor and terminalReply to stream read (#1486)
---
 .../apache/ratis/client/impl/ClientProtoUtils.java | 11 ++--
 .../ratis/client/impl/DataStreamClientImpl.java    | 23 ++++----
 .../datastream/impl/DataStreamReplyByteBuf.java    | 24 +++++++-
 .../apache/ratis/protocol/ClientInvocationId.java  |  4 ++
 .../java/org/apache/ratis/protocol/Message.java    |  5 ++
 .../java/org/apache/ratis/util/NettyUtils.java     |  7 +++
 .../apache/ratis/netty/NettyDataStreamUtils.java   | 30 +++++-----
 .../ratis/netty/client/NettyClientReplies.java     | 14 ++---
 .../ratis/netty/client/NettyClientStreamRpc.java   | 16 +++--
 .../ratis/netty/server/DataStreamManagement.java   |  2 +-
 .../ratis/netty/server/NettyServerStreamRpc.java   | 22 +++++--
 .../ratis/netty/server/ReadStreamManagement.java   | 68 ++++++++++++++++++----
 .../netty/server/TestDataStreamManagement.java     |  4 +-
 13 files changed, 170 insertions(+), 60 deletions(-)

diff --git 
a/ratis-client/src/main/java/org/apache/ratis/client/impl/ClientProtoUtils.java 
b/ratis-client/src/main/java/org/apache/ratis/client/impl/ClientProtoUtils.java
index d2146a521..1126453c2 100644
--- 
a/ratis-client/src/main/java/org/apache/ratis/client/impl/ClientProtoUtils.java
+++ 
b/ratis-client/src/main/java/org/apache/ratis/client/impl/ClientProtoUtils.java
@@ -18,6 +18,7 @@
 package org.apache.ratis.client.impl;
 
 import org.apache.ratis.datastream.impl.DataStreamReplyByteBuffer;
+import org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.proto.RaftProtos.AlreadyClosedExceptionProto;
 import org.apache.ratis.proto.RaftProtos.ClientMessageEntryProto;
 import org.apache.ratis.proto.RaftProtos.GroupAddRequestProto;
@@ -379,11 +380,13 @@ public interface ClientProtoUtils {
   }
 
   static RaftClientReply getRaftClientReply(DataStreamReply reply) {
-    if (!(reply instanceof DataStreamReplyByteBuffer)) {
-      throw new IllegalStateException("Unexpected " + reply.getClass() + ": 
reply is " + reply);
-    }
     try {
-      return toRaftClientReply(((DataStreamReplyByteBuffer) reply).slice());
+      if (reply instanceof DataStreamReplyByteBuffer) {
+        return toRaftClientReply(((DataStreamReplyByteBuffer) reply).slice());
+      } else if (reply instanceof DataStreamReplyByteBuf) {
+        return toRaftClientReply(((DataStreamReplyByteBuf) 
reply).slice().nioBuffer());
+      }
+      throw new IllegalStateException("Unexpected " + reply.getClass() + ": 
reply is " + reply);
     } catch (InvalidProtocolBufferException e) {
       throw new IllegalStateException("Failed to getRaftClientReply from " + 
reply, e);
     }
diff --git 
a/ratis-client/src/main/java/org/apache/ratis/client/impl/DataStreamClientImpl.java
 
b/ratis-client/src/main/java/org/apache/ratis/client/impl/DataStreamClientImpl.java
index 313131cbd..3c055009f 100644
--- 
a/ratis-client/src/main/java/org/apache/ratis/client/impl/DataStreamClientImpl.java
+++ 
b/ratis-client/src/main/java/org/apache/ratis/client/impl/DataStreamClientImpl.java
@@ -34,16 +34,16 @@ import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.protocol.ClientInvocationId;
 import org.apache.ratis.protocol.DataStreamReply;
 import org.apache.ratis.protocol.DataStreamRequestHeader;
+import org.apache.ratis.protocol.Message;
 import org.apache.ratis.protocol.RaftClientReply;
 import org.apache.ratis.protocol.RaftClientRequest;
 import org.apache.ratis.protocol.RaftGroupId;
 import org.apache.ratis.protocol.RaftPeer;
+import org.apache.ratis.protocol.RoutingTable;
 import org.apache.ratis.protocol.exceptions.AlreadyClosedException;
 import org.apache.ratis.rpc.CallId;
 import org.apache.ratis.thirdparty.io.netty.buffer.ByteBuf;
 import org.apache.ratis.util.IOUtils;
-import org.apache.ratis.protocol.*;
-import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.apache.ratis.util.JavaUtils;
 import org.apache.ratis.util.MemoizedSupplier;
 import org.apache.ratis.util.Preconditions;
@@ -57,7 +57,6 @@ import java.nio.channels.WritableByteChannel;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -260,18 +259,20 @@ public class DataStreamClientImpl implements 
DataStreamClient {
           () -> "Primary peer mismatched: the routing table has " + 
routingTable.getPrimary()
               + " but the client has " + dataStreamServer.getId());
     }
-    final Message message =
-        
Optional.ofNullable(headerMessage).map(ByteString::copyFrom).map(Message::valueOf).orElse(null);
-    RaftClientRequest request = RaftClientRequest.newBuilder()
+    return new DataStreamOutputImpl(newBuilder(headerMessage)
+        .setType(RaftClientRequest.dataStreamRequestType())
+        .setRoutingTable(routingTable)
+        .build());
+  }
+
+  private RaftClientRequest.Builder newBuilder(ByteBuffer headerMessage) {
+    final Message message = headerMessage == null ? null : 
Message.valueOf(headerMessage);
+    return RaftClientRequest.newBuilder()
         .setClientId(clientId)
         .setServerId(dataStreamServer.getId())
         .setGroupId(groupId)
         .setCallId(CallId.getAndIncrement())
-        .setMessage(message)
-        .setType(RaftClientRequest.dataStreamRequestType())
-        .setRoutingTable(routingTable)
-        .build();
-    return new DataStreamOutputImpl(request);
+        .setMessage(message);
   }
 
   @Override
diff --git 
a/ratis-common/src/main/java/org/apache/ratis/datastream/impl/DataStreamReplyByteBuf.java
 
b/ratis-common/src/main/java/org/apache/ratis/datastream/impl/DataStreamReplyByteBuf.java
index bef58563f..2477b089b 100644
--- 
a/ratis-common/src/main/java/org/apache/ratis/datastream/impl/DataStreamReplyByteBuf.java
+++ 
b/ratis-common/src/main/java/org/apache/ratis/datastream/impl/DataStreamReplyByteBuf.java
@@ -22,6 +22,7 @@ import org.apache.ratis.protocol.DataStreamReply;
 import org.apache.ratis.protocol.DataStreamReplyHeader;
 import org.apache.ratis.thirdparty.io.netty.buffer.ByteBuf;
 
+import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -30,7 +31,7 @@ import java.util.Collections;
  * <p>
  * This class is immutable.
  */
-public final class DataStreamReplyByteBuf extends DataStreamPacketByteBuf 
implements DataStreamReply {
+public final class DataStreamReplyByteBuf extends DataStreamPacketByteBuf 
implements DataStreamReply, AutoCloseable {
   public static final class Builder extends DataStreamReplyBuilder<Builder> {
     private ByteBuf buf;
 
@@ -90,10 +91,31 @@ public final class DataStreamReplyByteBuf extends 
DataStreamPacketByteBuf implem
     return Collections.unmodifiableCollection(commitInfos);
   }
 
+  @Override
+  public void close() {
+    release();
+  }
+
   @Override
   public String toString() {
     return super.toString()
         + "," + (success ? "SUCCESS" : "FAILED")
         + ",bytesWritten=" + bytesWritten;
   }
+
+  public DataStreamReplyByteBuffer copy() {
+    return DataStreamReplyByteBuffer.newBuilder()
+        .setDataStreamPacket(this)
+        .setBuffer(copy(slice()))
+        .setSuccess(isSuccess())
+        .setBytesWritten(getBytesWritten())
+        .setCommitInfos(getCommitInfos())
+        .build();
+  }
+
+  static ByteBuffer copy(ByteBuf buf) {
+    final byte[] bytes = new byte[buf.readableBytes()];
+    buf.readBytes(bytes);
+    return ByteBuffer.wrap(bytes);
+  }
 }
diff --git 
a/ratis-common/src/main/java/org/apache/ratis/protocol/ClientInvocationId.java 
b/ratis-common/src/main/java/org/apache/ratis/protocol/ClientInvocationId.java
index 24e491e6e..0d05b4c92 100644
--- 
a/ratis-common/src/main/java/org/apache/ratis/protocol/ClientInvocationId.java
+++ 
b/ratis-common/src/main/java/org/apache/ratis/protocol/ClientInvocationId.java
@@ -37,6 +37,10 @@ public final class ClientInvocationId {
     return valueOf(message.getClientId(), message.getCallId());
   }
 
+  public static ClientInvocationId valueOf(DataStreamPacket packet) {
+    return valueOf(packet.getClientId(), packet.getStreamId());
+  }
+
   public static ClientInvocationId valueOf(StateMachineLogEntryProto proto) {
     return valueOf(ClientId.valueOf(proto.getClientId()), proto.getCallId());
   }
diff --git a/ratis-common/src/main/java/org/apache/ratis/protocol/Message.java 
b/ratis-common/src/main/java/org/apache/ratis/protocol/Message.java
index 55fcd064d..b773826fb 100644
--- a/ratis-common/src/main/java/org/apache/ratis/protocol/Message.java
+++ b/ratis-common/src/main/java/org/apache/ratis/protocol/Message.java
@@ -23,6 +23,7 @@ import 
org.apache.ratis.thirdparty.com.google.protobuf.TextFormat;
 import org.apache.ratis.util.MemoizedSupplier;
 import org.apache.ratis.util.StringUtils;
 
+import java.nio.ByteBuffer;
 import java.util.Optional;
 import java.util.function.Supplier;
 
@@ -55,6 +56,10 @@ public interface Message {
     return valueOf(bytes, () -> "Message:" + 
StringUtils.bytes2ShortString(bytes));
   }
 
+  static Message valueOf(ByteBuffer bytes) {
+    return valueOf(ByteString.copyFrom(bytes));
+  }
+
   static Message valueOf(String string) {
     return valueOf(ByteString.copyFromUtf8(string), () -> "Message:" + string);
   }
diff --git a/ratis-common/src/main/java/org/apache/ratis/util/NettyUtils.java 
b/ratis-common/src/main/java/org/apache/ratis/util/NettyUtils.java
index 163c3d869..819c93db9 100644
--- a/ratis-common/src/main/java/org/apache/ratis/util/NettyUtils.java
+++ b/ratis-common/src/main/java/org/apache/ratis/util/NettyUtils.java
@@ -37,6 +37,7 @@ import 
org.apache.ratis.thirdparty.io.netty.channel.socket.nio.NioSocketChannel;
 import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContext;
 import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContextBuilder;
 import org.apache.ratis.thirdparty.io.netty.util.concurrent.Future;
+import org.apache.ratis.thirdparty.io.netty.util.concurrent.ScheduledFuture;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -230,4 +231,10 @@ public interface NettyUtils {
       LOG.warn("closeChannel {} is not yet completed in {}", name, 
CLOSE_TIMEOUT);
     }
   }
+
+  static void cancel(ScheduledFuture<?> future) {
+    if (future != null) {
+      future.cancel(true);
+    }
+  }
 }
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/NettyDataStreamUtils.java 
b/ratis-netty/src/main/java/org/apache/ratis/netty/NettyDataStreamUtils.java
index 583d6e3e9..2c5015b12 100644
--- a/ratis-netty/src/main/java/org/apache/ratis/netty/NettyDataStreamUtils.java
+++ b/ratis-netty/src/main/java/org/apache/ratis/netty/NettyDataStreamUtils.java
@@ -18,6 +18,7 @@
 package org.apache.ratis.netty;
 
 import org.apache.ratis.datastream.impl.DataStreamReplyByteBuffer;
+import org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuffer;
 import org.apache.ratis.datastream.impl.DataStreamRequestFilePositionCount;
 import org.apache.ratis.io.FilePositionCount;
@@ -30,6 +31,7 @@ import 
org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto;
 import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.protocol.DataStreamPacketHeader;
 import org.apache.ratis.protocol.DataStreamReplyHeader;
+import org.apache.ratis.protocol.DataStreamReply;
 import org.apache.ratis.protocol.DataStreamRequest;
 import org.apache.ratis.protocol.DataStreamRequestHeader;
 import 
org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferException;
@@ -80,7 +82,7 @@ public interface NettyDataStreamUtils {
         .asReadOnlyByteBuffer();
   }
 
-  static ByteBuffer 
getDataStreamReplyHeaderProtoByteBuf(DataStreamReplyByteBuffer reply) {
+  static ByteBuffer getDataStreamReplyHeaderProtoByteBuf(DataStreamReply 
reply) {
     DataStreamPacketHeaderProto.Builder b = DataStreamPacketHeaderProto
         .newBuilder()
         .setClientId(reply.getClientId().toByteString())
@@ -149,14 +151,22 @@ public interface NettyDataStreamUtils {
     out.accept(new DefaultFileRegion(f.getFile(), f.getPosition(), 
f.getCount()));
   }
 
-  static void encodeDataStreamReplyByteBuffer(DataStreamReplyByteBuffer reply, 
Consumer<ByteBuf> out,
+  static void encodeDataStreamReply(DataStreamReply reply, ByteBuf body, 
Consumer<Object> out,
       ByteBufAllocator allocator) {
-    ByteBuffer headerBuf = getDataStreamReplyHeaderProtoByteBuf(reply);
+    final ByteBuffer headerBuf = getDataStreamReplyHeaderProtoByteBuf(reply);
     final ByteBuf headerLenBuf = 
allocator.ioBuffer(DataStreamPacketHeader.getSizeOfHeaderLen());
     headerLenBuf.writeInt(headerBuf.remaining());
     out.accept(headerLenBuf);
     out.accept(Unpooled.wrappedBuffer(headerBuf));
-    out.accept(Unpooled.wrappedBuffer(reply.slice()));
+    encodeByteBuf(body, out);
+  }
+
+  static void encodeDataStreamReply(DataStreamReplyByteBuffer reply, 
Consumer<Object> out, ByteBufAllocator allocator) {
+    encodeDataStreamReply(reply, Unpooled.wrappedBuffer(reply.slice()), out, 
allocator);
+  }
+
+  static void encodeDataStreamReply(DataStreamReplyByteBuf reply, 
Consumer<Object> out, ByteBufAllocator allocator) {
+    encodeDataStreamReply(reply, reply.slice(), out, allocator);
   }
 
   static DataStreamRequestByteBuf decodeDataStreamRequestByteBuf(ByteBuf buf) {
@@ -208,18 +218,12 @@ public interface NettyDataStreamUtils {
     }
   }
 
-  static ByteBuffer copy(ByteBuf buf) {
-    final byte[] bytes = new byte[buf.readableBytes()];
-    buf.readBytes(bytes);
-    return ByteBuffer.wrap(bytes);
-  }
-
-  static DataStreamReplyByteBuffer decodeDataStreamReplyByteBuffer(ByteBuf 
buf) {
+  static DataStreamReplyByteBuf decodeDataStreamReplyByteBuf(ByteBuf buf) {
     return Optional.ofNullable(decodeDataStreamReplyHeader(buf))
         .map(header -> checkHeader(header, buf))
-        .map(header -> DataStreamReplyByteBuffer.newBuilder()
+        .map(header -> DataStreamReplyByteBuf.newBuilder()
             .setDataStreamReplyHeader(header)
-            .setBuffer(decodeData(buf, header, NettyDataStreamUtils::copy))
+            .setBuf(decodeData(buf, header, ByteBuf::retainedSlice))
             .build())
         .orElse(null);
   }
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientReplies.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientReplies.java
index 695177262..121ef3c0f 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientReplies.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientReplies.java
@@ -18,12 +18,14 @@
 
 package org.apache.ratis.netty.client;
 
+import org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
 import org.apache.ratis.protocol.ClientInvocationId;
 import org.apache.ratis.protocol.DataStreamPacket;
 import org.apache.ratis.protocol.DataStreamReply;
 import org.apache.ratis.thirdparty.io.netty.util.concurrent.ScheduledFuture;
 import org.apache.ratis.util.MemoizedSupplier;
+import org.apache.ratis.util.NettyUtils;
 import org.apache.ratis.util.Preconditions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -63,7 +65,7 @@ public class NettyClientReplies {
       return map.computeIfAbsent(requestEntry, r -> new ReplyEntry(isClose, 
f));
     }
 
-    void receiveReply(DataStreamReply reply) {
+    void receiveReply(DataStreamReplyByteBuf reply) {
       final RequestEntry requestEntry = new RequestEntry(reply);
       final ReplyEntry replyEntry = map.remove(requestEntry);
       LOG.debug("remove: {}; replyEntry: {}; reply: {}", requestEntry, 
replyEntry, reply);
@@ -162,21 +164,15 @@ public class NettyClientReplies {
     }
 
     synchronized void complete(DataStreamReply reply) {
-      cancel(timeoutFuture);
+      NettyUtils.cancel(timeoutFuture);
       replyFuture.complete(reply);
     }
 
     synchronized void completeExceptionally(Throwable t) {
-      cancel(timeoutFuture);
+      NettyUtils.cancel(timeoutFuture);
       replyFuture.completeExceptionally(t);
     }
 
-    static void cancel(ScheduledFuture<?> future) {
-      if (future != null) {
-        future.cancel(true);
-      }
-    }
-
     synchronized void scheduleTimeout(Supplier<ScheduledFuture<?>> 
scheduleMethod) {
       if (!replyFuture.isDone()) {
         timeoutFuture = scheduleMethod.get();
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
index 5b673d518..ccb0b15da 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
@@ -23,6 +23,7 @@ import org.apache.ratis.client.RaftClientConfigKeys;
 import org.apache.ratis.conf.RaftProperties;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuf;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuffer;
+import org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.datastream.impl.DataStreamRequestFilePositionCount;
 import org.apache.ratis.io.StandardWriteOption;
 import org.apache.ratis.io.WriteOption;
@@ -353,11 +354,16 @@ public class NettyClientStreamRpc implements 
DataStreamClientRpc {
 
       @Override
       public void channelRead(ChannelHandlerContext ctx, Object msg) {
-        if (!(msg instanceof DataStreamReply)) {
+        if (!(msg instanceof DataStreamReplyByteBuf)) {
           LOG.error("{}: unexpected message {}", name, msg.getClass());
           return;
         }
-        final DataStreamReply reply = (DataStreamReply) msg;
+        try (DataStreamReplyByteBuf reply = (DataStreamReplyByteBuf) msg) {
+          process(reply);
+        }
+      }
+
+      private void process(DataStreamReplyByteBuf reply) {
         LOG.debug("{}: read {}", name, reply);
         final ClientInvocationId clientInvocationId = 
ClientInvocationId.valueOf(
             reply.getClientId(), reply.getStreamId());
@@ -370,7 +376,7 @@ public class NettyClientStreamRpc implements 
DataStreamClientRpc {
         try {
           replyMap.receiveReply(reply);
         } catch (Throwable cause) {
-          LOG.warn("{} : channelRead error:", name, cause);
+          LOG.warn("{} : channelRead error for {}", name, reply, cause);
           replyMap.completeExceptionally(cause);
         }
       }
@@ -456,7 +462,7 @@ public class NettyClientStreamRpc implements 
DataStreamClientRpc {
 
       @Override
       protected void decode(ChannelHandlerContext context, ByteBuf buf, 
List<Object> out) {
-        
Optional.ofNullable(NettyDataStreamUtils.decodeDataStreamReplyByteBuffer(buf)).ifPresent(out::add);
+        
Optional.ofNullable(NettyDataStreamUtils.decodeDataStreamReplyByteBuf(buf)).ifPresent(out::add);
       }
     };
   }
@@ -464,7 +470,7 @@ public class NettyClientStreamRpc implements 
DataStreamClientRpc {
   @Override
   public CompletableFuture<DataStreamReply> streamAsync(DataStreamRequest 
request) {
     final CompletableFuture<DataStreamReply> f = new CompletableFuture<>();
-    ClientInvocationId clientInvocationId = 
ClientInvocationId.valueOf(request.getClientId(), request.getStreamId());
+    final ClientInvocationId clientInvocationId = 
ClientInvocationId.valueOf(request);
     final boolean isClose = 
request.getWriteOptionList().contains(StandardWriteOption.CLOSE);
 
     final NettyClientReplies.ReplyMap replyMap = 
replies.getOrCreateReplyMap(clientInvocationId);
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
index 4e337e06b..005e0b8e6 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
@@ -424,7 +424,7 @@ public class DataStreamManagement {
   void read(DataStreamRequestByteBuf request, ChannelHandlerContext ctx,
       CheckedBiFunction<RaftClientRequest, Set<RaftPeer>, 
Set<DataStreamOutputImpl>, IOException> getStreams) {
     LOG.debug("{}: read {}", this, request);
-    final ClientInvocationId key = 
ClientInvocationId.valueOf(request.getClientId(), request.getStreamId());
+    final ClientInvocationId key = ClientInvocationId.valueOf(request);
     final ChannelId channelId = ctx.channel().id();
     try {
       readImpl(request, ctx, getStreams, key, channelId);
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/NettyServerStreamRpc.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/NettyServerStreamRpc.java
index 643ed15a0..d7cdeb056 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/NettyServerStreamRpc.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/NettyServerStreamRpc.java
@@ -23,6 +23,7 @@ import org.apache.ratis.client.DataStreamOutputRpc;
 import org.apache.ratis.client.impl.DataStreamClientImpl.DataStreamOutputImpl;
 import org.apache.ratis.conf.Parameters;
 import org.apache.ratis.conf.RaftProperties;
+import org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.datastream.impl.DataStreamReplyByteBuffer;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuf;
 import org.apache.ratis.netty.NettyConfigKeys;
@@ -31,6 +32,7 @@ import org.apache.ratis.util.NettyUtils;
 import org.apache.ratis.netty.metrics.NettyServerStreamRpcMetrics;
 import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.protocol.DataStreamPacket;
+import org.apache.ratis.protocol.DataStreamReply;
 import org.apache.ratis.protocol.RaftClientRequest;
 import org.apache.ratis.protocol.RaftPeer;
 import org.apache.ratis.security.TlsConf;
@@ -286,13 +288,19 @@ public class NettyServerStreamRpc implements 
DataStreamServerRpc {
     };
   }
 
-  static final MessageToMessageEncoder<DataStreamReplyByteBuffer> ENCODER = 
new Encoder();
+  static final MessageToMessageEncoder<DataStreamReply> ENCODER = new 
Encoder();
 
   @ChannelHandler.Sharable
-  static class Encoder extends 
MessageToMessageEncoder<DataStreamReplyByteBuffer> {
+  static class Encoder extends MessageToMessageEncoder<DataStreamReply> {
     @Override
-    protected void encode(ChannelHandlerContext context, 
DataStreamReplyByteBuffer reply, List<Object> out) {
-      NettyDataStreamUtils.encodeDataStreamReplyByteBuffer(reply, out::add, 
context.alloc());
+    protected void encode(ChannelHandlerContext context, DataStreamReply 
reply, List<Object> out) {
+      if (reply instanceof DataStreamReplyByteBuffer) {
+        NettyDataStreamUtils.encodeDataStreamReply((DataStreamReplyByteBuffer) 
reply, out::add, context.alloc());
+      } else if (reply instanceof DataStreamReplyByteBuf) {
+        NettyDataStreamUtils.encodeDataStreamReply((DataStreamReplyByteBuf) 
reply, out::add, context.alloc());
+      } else {
+        throw new IllegalArgumentException("Unexpected DataStreamReply class " 
+ reply.getClass());
+      }
     }
   }
 
@@ -321,6 +329,12 @@ public class NettyServerStreamRpc implements 
DataStreamServerRpc {
       LOG.error(this + ": Failed to shutdown request service.", e);
     }
 
+    try {
+      reads.shutdown();
+    } catch (Exception e) {
+      LOG.error(this + ": Failed to shutdown read service.", e);
+    }
+
     try {
       channelFuture.channel().close().sync();
       bossGroup.shutdownGracefully(0, 100, TimeUnit.MILLISECONDS);
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/ReadStreamManagement.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/ReadStreamManagement.java
index bcdced1ea..5336760a0 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/ReadStreamManagement.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/ReadStreamManagement.java
@@ -17,19 +17,24 @@
  */
 package org.apache.ratis.netty.server;
 
+import org.apache.ratis.conf.RaftProperties;
 import org.apache.ratis.datastream.impl.DataStreamReplyByteBuffer;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuf;
 import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
 import org.apache.ratis.proto.RaftProtos.RaftClientRequestProto;
 import org.apache.ratis.proto.RaftProtos.RaftClientRequestProto.TypeCase;
 import org.apache.ratis.protocol.ClientId;
+import org.apache.ratis.protocol.RaftClientReply;
 import org.apache.ratis.protocol.RaftClientRequest;
 import org.apache.ratis.protocol.exceptions.AlreadyClosedException;
 import org.apache.ratis.server.RaftServer;
+import org.apache.ratis.server.RaftServerConfigKeys;
 import 
org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferException;
 import org.apache.ratis.thirdparty.io.netty.channel.ChannelFuture;
 import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext;
+import org.apache.ratis.util.ConcurrentUtils;
 import org.apache.ratis.util.JavaUtils;
+import org.apache.ratis.util.TimeDuration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,8 +43,10 @@ import java.io.InterruptedIOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.WritableByteChannel;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
 
 import static 
org.apache.ratis.client.impl.ClientProtoUtils.toRaftClientRequest;
+import static 
org.apache.ratis.client.impl.ClientProtoUtils.toRaftClientReplyProto;
 import static 
org.apache.ratis.netty.server.DataStreamManagement.replyDataStreamException;
 
 public class ReadStreamManagement {
@@ -50,12 +57,27 @@ public class ReadStreamManagement {
     private final long streamId;
     private final ChannelHandlerContext ctx;
     private final CompletableFuture<Void> closed = new CompletableFuture<>();
+    private final DataStreamReplyByteBuffer terminalReply;
     private long streamOffset;
 
-    ReadStream(DataStreamRequestByteBuf request, ChannelHandlerContext ctx) {
+    ReadStream(RaftClientRequest request, long streamId, ChannelHandlerContext 
ctx) {
       this.clientId = request.getClientId();
-      this.streamId = request.getStreamId();
+      this.streamId = streamId;
       this.ctx = ctx;
+
+      final RaftClientReply reply = RaftClientReply.newBuilder()
+          .setRequest(request)
+          .setSuccess()
+          .build();
+      this.terminalReply = DataStreamReplyByteBuffer.newBuilder()
+          .setClientId(clientId)
+          .setType(Type.STREAM_HEADER)
+          .setStreamId(streamId)
+          .setStreamOffset(0)
+          
.setBuffer(toRaftClientReplyProto(reply).toByteString().asReadOnlyByteBuffer())
+          .setSuccess(true)
+          .setBytesWritten(0)
+          .build();
     }
 
     @Override
@@ -64,8 +86,10 @@ public class ReadStreamManagement {
     }
 
     @Override
-    public void close() {
-      closed.complete(null);
+    public synchronized void close() throws IOException {
+      if (closed.complete(null)) {
+        writeAndFlush(terminalReply, 0);
+      }
     }
 
     @Override
@@ -76,19 +100,23 @@ public class ReadStreamManagement {
       buffer = buffer.asReadOnlyBuffer();
       final int length = buffer.remaining();
       final DataStreamReplyByteBuffer reply = newReply(buffer);
+      writeAndFlush(reply, length);
+      streamOffset += length;
+      return length;
+    }
+
+    private synchronized void writeAndFlush(DataStreamReplyByteBuffer reply, 
int length) throws IOException {
+      final long offset = reply.getStreamOffset();
       final ChannelFuture future = ctx.writeAndFlush(reply);
       try {
         future.await();
       } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
-        throw new InterruptedIOException(
-            "Interrupted while writing " + length + " bytes at offset " + 
streamOffset);
+        throw new InterruptedIOException("Interrupted while writing " + length 
+ " bytes at offset " + offset);
       }
       if (!future.isSuccess()) {
-        throw new IOException("Failed to write " + length + " bytes at offset 
" + streamOffset, future.cause());
+        throw new IOException("Failed to write " + length + " bytes at offset 
" + offset, future.cause());
       }
-      streamOffset += length;
-      return length;
     }
 
     private synchronized DataStreamReplyByteBuffer newReply(ByteBuffer buffer) 
{
@@ -106,10 +134,22 @@ public class ReadStreamManagement {
 
   private final RaftServer server;
   private final String name;
+  private final ExecutorService requestExecutor;
 
   ReadStreamManagement(RaftServer server) {
     this.server = server;
     this.name = server.getId() + "-" + 
JavaUtils.getClassSimpleName(getClass());
+
+    final RaftProperties properties = server.getProperties();
+    this.requestExecutor = ConcurrentUtils.newThreadPoolWithMax(
+        
RaftServerConfigKeys.DataStream.asyncRequestThreadPoolCached(properties),
+        RaftServerConfigKeys.DataStream.asyncRequestThreadPoolSize(properties),
+        name + "-request-");
+  }
+
+  void shutdown() {
+    ConcurrentUtils.shutdownAndWait(TimeDuration.ONE_SECOND, requestExecutor,
+        timeout -> LOG.warn("{}: requestExecutor shutdown timeout in {}", 
this, timeout));
   }
 
   boolean process(DataStreamRequestByteBuf requestBuf, ChannelHandlerContext 
ctx) {
@@ -146,8 +186,14 @@ public class ReadStreamManagement {
       return true;
     }
 
-    final ReadStream stream = new ReadStream(requestBuf, ctx);
-    division.getStateMachine().data().query(request.getMessage(), stream);
+    final ReadStream stream = new ReadStream(request, 
requestBuf.getStreamId(), ctx);
+    requestExecutor.execute(() -> {
+      try {
+        division.getStateMachine().data().query(request.getMessage(), stream);
+      } catch (Throwable t) {
+        LOG.error("{}: Failed read-only data stream query for {}", this, 
request, t);
+      }
+    });
     return true;
   }
 
diff --git 
a/ratis-test/src/test/java/org/apache/ratis/netty/server/TestDataStreamManagement.java
 
b/ratis-test/src/test/java/org/apache/ratis/netty/server/TestDataStreamManagement.java
index 1573a2a28..516f8c171 100644
--- 
a/ratis-test/src/test/java/org/apache/ratis/netty/server/TestDataStreamManagement.java
+++ 
b/ratis-test/src/test/java/org/apache/ratis/netty/server/TestDataStreamManagement.java
@@ -124,12 +124,14 @@ class TestDataStreamManagement {
         for (Object outbound; (outbound = embeddedChannel.readOutbound()) != 
null;) {
           replies.add((DataStreamReply) outbound);
         }
-        assertEquals(1, replies.size());
+        assertEquals(2, replies.size());
       }, 10, TimeDuration.valueOf(100, TimeUnit.MILLISECONDS), "read-only 
replies", null);
 
       assertEquals(query, messageRef.get().getContent());
       assertFalse(streamRef.get().isOpen(), "state machine should close the 
streaming query channel");
       assertSuccessReply(Type.STREAM_DATA, response.size(), replies.get(0));
+      assertSuccessReply(Type.STREAM_HEADER, 0, replies.get(1));
+      
assertTrue(ClientProtoUtils.getRaftClientReply(replies.get(1)).isSuccess());
     } finally {
       embeddedChannel.finishAndReleaseAll();
     }

Reply via email to