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 31bfd3887 RATIS-1240. Add input stream to DataStreamApi for read 
operations in Server (#1469)
31bfd3887 is described below

commit 31bfd388748551f7d5a71e705e07b76b4a194f64
Author: Peter Lee <[email protected]>
AuthorDate: Sat May 30 10:34:03 2026 +0800

    RATIS-1240. Add input stream to DataStreamApi for read operations in Server 
(#1469)
---
 .../ratis/netty/server/DataStreamManagement.java   |   1 +
 .../ratis/netty/server/NettyServerStreamRpc.java   |   6 +
 .../ratis/netty/server/ReadStreamManagement.java   | 158 ++++++++++++++++++++
 .../apache/ratis/statemachine/StateMachine.java    |  11 ++
 .../ratis/datastream/DataStreamTestUtils.java      |  36 +++++
 .../netty/server/TestDataStreamManagement.java     | 159 ++++++++++++++++++---
 6 files changed, 353 insertions(+), 18 deletions(-)

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 f3b98054d..4e337e06b 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
@@ -352,6 +352,7 @@ public class DataStreamManagement {
         .setDataStreamPacket(request)
         .setBuffer(buffer)
         .setSuccess(reply.isSuccess())
+        .setCommitInfos(reply.getCommitInfos())
         .build();
   }
 
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 451040bb6..24303d867 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
@@ -152,6 +152,7 @@ public class NettyServerStreamRpc implements 
DataStreamServerRpc {
   private final ChannelFuture channelFuture;
 
   private final DataStreamManagement requests;
+  private final ReadStreamManagement reads;
   private final ProxiesPool proxies;
 
   private final NettyServerStreamRpcMetrics metrics;
@@ -162,6 +163,7 @@ public class NettyServerStreamRpc implements 
DataStreamServerRpc {
     this.name = server.getId() + "-" + 
JavaUtils.getClassSimpleName(getClass());
     this.metrics = new NettyServerStreamRpcMetrics(this.name);
     this.requests = new DataStreamManagement(server, metrics);
+    this.reads = new ReadStreamManagement(server);
 
     final RaftProperties properties = server.getProperties();
 
@@ -235,6 +237,9 @@ public class NettyServerStreamRpc implements 
DataStreamServerRpc {
 
         final DataStreamRequestByteBuf request = (DataStreamRequestByteBuf)msg;
         try(UncheckedAutoCloseable autoReset = requestRef.set(request)) {
+          if (reads.process(request, ctx)) {
+            return;
+          }
           requests.read(request, ctx, 
proxies.get(request)::getDataStreamOutput);
         }
       }
@@ -248,6 +253,7 @@ public class NettyServerStreamRpc implements 
DataStreamServerRpc {
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable 
throwable) {
         Optional.ofNullable(requestRef.getAndSetNull())
             .ifPresent(request -> requests.replyDataStreamException(throwable, 
request, ctx));
+        ctx.close();
       }
     };
   }
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
new file mode 100644
index 000000000..bcdced1ea
--- /dev/null
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/ReadStreamManagement.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ratis.netty.server;
+
+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.RaftClientRequest;
+import org.apache.ratis.protocol.exceptions.AlreadyClosedException;
+import org.apache.ratis.server.RaftServer;
+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.JavaUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.CompletableFuture;
+
+import static 
org.apache.ratis.client.impl.ClientProtoUtils.toRaftClientRequest;
+import static 
org.apache.ratis.netty.server.DataStreamManagement.replyDataStreamException;
+
+public class ReadStreamManagement {
+  public static final Logger LOG = 
LoggerFactory.getLogger(ReadStreamManagement.class);
+
+  static class ReadStream implements WritableByteChannel {
+    private final ClientId clientId;
+    private final long streamId;
+    private final ChannelHandlerContext ctx;
+    private final CompletableFuture<Void> closed = new CompletableFuture<>();
+    private long streamOffset;
+
+    ReadStream(DataStreamRequestByteBuf request, ChannelHandlerContext ctx) {
+      this.clientId = request.getClientId();
+      this.streamId = request.getStreamId();
+      this.ctx = ctx;
+    }
+
+    @Override
+    public boolean isOpen() {
+      return !closed.isDone();
+    }
+
+    @Override
+    public void close() {
+      closed.complete(null);
+    }
+
+    @Override
+    public synchronized int write(ByteBuffer buffer) throws IOException {
+      if (!isOpen()) {
+        throw new AlreadyClosedException("Channel closed at offset " + 
streamOffset);
+      }
+      buffer = buffer.asReadOnlyBuffer();
+      final int length = buffer.remaining();
+      final DataStreamReplyByteBuffer reply = newReply(buffer);
+      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);
+      }
+      if (!future.isSuccess()) {
+        throw new IOException("Failed to write " + length + " bytes at offset 
" + streamOffset, future.cause());
+      }
+      streamOffset += length;
+      return length;
+    }
+
+    private synchronized DataStreamReplyByteBuffer newReply(ByteBuffer buffer) 
{
+      return DataStreamReplyByteBuffer.newBuilder()
+          .setClientId(clientId)
+          .setType(Type.STREAM_DATA)
+          .setStreamId(streamId)
+          .setStreamOffset(streamOffset)
+          .setBuffer(buffer)
+          .setSuccess(true)
+          .setBytesWritten(buffer.remaining())
+          .build();
+    }
+  }
+
+  private final RaftServer server;
+  private final String name;
+
+  ReadStreamManagement(RaftServer server) {
+    this.server = server;
+    this.name = server.getId() + "-" + 
JavaUtils.getClassSimpleName(getClass());
+  }
+
+  boolean process(DataStreamRequestByteBuf requestBuf, ChannelHandlerContext 
ctx) {
+    boolean processed = false;
+    try {
+      processed = processImpl(requestBuf, ctx);
+    } catch (Throwable e) {
+      LOG.error("Failed to process {}", requestBuf, e);
+      processed = true;
+    } finally {
+      if (processed) {
+        requestBuf.release();
+      }
+    }
+    return processed;
+  }
+
+  private boolean processImpl(DataStreamRequestByteBuf requestBuf, 
ChannelHandlerContext ctx)
+      throws InvalidProtocolBufferException {
+    if (requestBuf.getType() != Type.STREAM_HEADER) {
+      return false;
+    }
+    final RaftClientRequest request = toRaftClientRequest(
+        RaftClientRequestProto.parseFrom(requestBuf.slice().nioBuffer()));
+    if (!request.is(TypeCase.READ)) {
+      return false;
+    }
+
+    final RaftServer.Division division;
+    try {
+      division = server.getDivision(request.getRaftGroupId());
+    } catch (IOException e) {
+      replyDataStreamException(server, e, request, requestBuf, ctx);
+      return true;
+    }
+
+    final ReadStream stream = new ReadStream(requestBuf, ctx);
+    division.getStateMachine().data().query(request.getMessage(), stream);
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+}
diff --git 
a/ratis-server-api/src/main/java/org/apache/ratis/statemachine/StateMachine.java
 
b/ratis-server-api/src/main/java/org/apache/ratis/statemachine/StateMachine.java
index 98d453784..61e708feb 100644
--- 
a/ratis-server-api/src/main/java/org/apache/ratis/statemachine/StateMachine.java
+++ 
b/ratis-server-api/src/main/java/org/apache/ratis/statemachine/StateMachine.java
@@ -116,6 +116,17 @@ public interface StateMachine extends Closeable {
       return CompletableFuture.completedFuture(null);
     }
 
+    /**
+     * Similar to {@link #query(Message)} except that
+     * {@link #query(Message)} returns the result in a future
+     * while this method sends the result using the given stream.
+     *
+     * @param request the client request
+     * @param stream the output stream to send the results
+     */
+    default void query(Message request, WritableByteChannel stream) {
+    }
+
     /**
      * Link asynchronously the given stream with the given log entry.
      * The given stream can be null if it is unavailable due to errors.
diff --git 
a/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamTestUtils.java 
b/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamTestUtils.java
index 989b6cd2b..fe9c3f9ea 100644
--- 
a/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamTestUtils.java
+++ 
b/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamTestUtils.java
@@ -57,9 +57,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -147,8 +149,14 @@ public interface DataStreamTestUtils {
   }
 
   class MultiDataStreamStateMachine extends BaseStateMachine {
+    static final int READ_ONLY_STREAM_CHUNKS = 3;
+
     private final ConcurrentMap<ClientInvocationId, SingleDataStream> streams 
= new ConcurrentHashMap<>();
 
+    static ByteString getReadOnlyStreamChunk(ByteString query, int index) {
+      return query.concat(ByteString.copyFromUtf8("-chunk-" + index));
+    }
+
     @Override
     public CompletableFuture<DataStream> stream(RaftClientRequest request) {
       final SingleDataStream s = new SingleDataStream(request);
@@ -176,6 +184,34 @@ public interface DataStreamTestUtils {
       return CompletableFuture.completedFuture(() -> bytesWritten);
     }
 
+    @Override
+    public CompletableFuture<Message> query(Message request) {
+      return CompletableFuture.completedFuture(request);
+    }
+
+    @Override
+    public void query(Message request, WritableByteChannel stream) {
+      CompletableFuture.supplyAsync(() -> {
+        try {
+          streamReadOnlyImpl(request, stream);
+        } catch (IOException e) {
+          throw new CompletionException("Failed to streamReadOnly for " + 
request, e);
+        }
+        return null;
+      });
+    }
+
+    private void streamReadOnlyImpl(Message request, WritableByteChannel 
stream) throws IOException {
+      try {
+        for (int i = 0; i < READ_ONLY_STREAM_CHUNKS; i++) {
+          final ByteString chunk = 
getReadOnlyStreamChunk(request.getContent(), i);
+          stream.write(chunk.asReadOnlyByteBuffer());
+        }
+      } finally {
+        stream.close();
+      }
+    }
+
     SingleDataStream getSingleDataStream(RaftClientRequest request) {
       return getSingleDataStream(ClientInvocationId.valueOf(request));
     }
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 5c06ddd31..1573a2a28 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
@@ -17,34 +17,124 @@
  */
 package org.apache.ratis.netty.server;
 
+import org.apache.ratis.client.impl.ClientProtoUtils;
 import org.apache.ratis.client.impl.DataStreamClientImpl.DataStreamOutputImpl;
 import org.apache.ratis.conf.RaftProperties;
+import org.apache.ratis.datastream.impl.DataStreamReplyByteBuffer;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuf;
 import org.apache.ratis.io.StandardWriteOption;
 import org.apache.ratis.netty.metrics.NettyServerStreamRpcMetrics;
 import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
 import org.apache.ratis.protocol.ClientId;
+import org.apache.ratis.protocol.DataStreamReply;
+import org.apache.ratis.protocol.Message;
 import org.apache.ratis.protocol.RaftClientRequest;
+import org.apache.ratis.protocol.RaftGroupId;
 import org.apache.ratis.protocol.RaftPeer;
 import org.apache.ratis.protocol.RaftPeerId;
 import org.apache.ratis.server.RaftServer;
+import org.apache.ratis.statemachine.StateMachine;
+import org.apache.ratis.statemachine.StateMachine.DataApi;
+import org.apache.ratis.statemachine.impl.BaseStateMachine;
+import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+import org.apache.ratis.thirdparty.io.netty.buffer.ByteBuf;
 import org.apache.ratis.thirdparty.io.netty.buffer.Unpooled;
 import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext;
 import org.apache.ratis.thirdparty.io.netty.channel.ChannelId;
 import 
org.apache.ratis.thirdparty.io.netty.channel.ChannelInboundHandlerAdapter;
 import org.apache.ratis.thirdparty.io.netty.channel.embedded.EmbeddedChannel;
+import org.apache.ratis.util.JavaUtils;
+import org.apache.ratis.util.TimeDuration;
 import org.apache.ratis.util.function.CheckedBiFunction;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
 import java.lang.reflect.Proxy;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class TestDataStreamManagement {
+  @Test
+  void readOnlyRequestInvokesReadStreamManagement() throws Exception {
+    final RaftPeerId serverId = RaftPeerId.valueOf("s1");
+    final ClientId clientId = ClientId.randomId();
+    final RaftGroupId groupId = RaftGroupId.randomId();
+    final ByteString query = ByteString.copyFromUtf8("query");
+    final ByteString response = ByteString.copyFromUtf8("response");
+
+    final AtomicReference<Message> messageRef = new AtomicReference<>();
+    final AtomicReference<WritableByteChannel> streamRef = new 
AtomicReference<>();
+    final DataApi dataApi = new DataApi() {
+      @Override
+      public void query(Message request, WritableByteChannel stream) {
+        messageRef.set(request);
+        streamRef.set(stream);
+      }
+    };
+    final StateMachine stateMachine = new BaseStateMachine() {
+      @Override
+      public DataApi data() {
+        return dataApi;
+      }
+    };
+    final RaftServer server = newRaftServer(serverId, new RaftProperties(), 
groupId, newDivision(stateMachine));
+    final ReadStreamManagement management = new ReadStreamManagement(server);
+    final EmbeddedChannel embeddedChannel = new EmbeddedChannel(new 
ChannelInboundHandlerAdapter());
+
+    final RaftClientRequest raftClientRequest = RaftClientRequest.newBuilder()
+        .setClientId(clientId)
+        .setServerId(serverId)
+        .setGroupId(groupId)
+        .setCallId(1L)
+        .setMessage(Message.valueOf(query))
+        .setType(RaftClientRequest.readRequestType())
+        .build();
+    final ByteBuffer header = 
ClientProtoUtils.toRaftClientRequestProtoByteBuffer(raftClientRequest);
+    final ByteBuf headerBuf = Unpooled.wrappedBuffer(header);
+    final DataStreamRequestByteBuf request = new DataStreamRequestByteBuf(
+        clientId,
+        Type.STREAM_HEADER,
+        raftClientRequest.getCallId(),
+        0L,
+        Collections.singletonList(StandardWriteOption.FLUSH),
+        headerBuf);
+
+    try {
+      assertTrue(management.process(request, 
embeddedChannel.pipeline().firstContext()));
+      assertEquals(0, headerBuf.refCnt());
+
+      final WritableByteChannel stream = streamRef.get();
+      assertNotNull(stream);
+      stream.write(response.asReadOnlyByteBuffer());
+      stream.close();
+
+      final List<DataStreamReply> replies = new ArrayList<>();
+      JavaUtils.attempt(() -> {
+        for (Object outbound; (outbound = embeddedChannel.readOutbound()) != 
null;) {
+          replies.add((DataStreamReply) outbound);
+        }
+        assertEquals(1, 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));
+    } finally {
+      embeddedChannel.finishAndReleaseAll();
+    }
+  }
+
   @Test
   void readCleansChannelMapOnEarlyException() throws Exception {
     // Scenario: STREAM_DATA arrives without prior STREAM_HEADER, so readImpl 
fails early.
@@ -85,29 +175,62 @@ class TestDataStreamManagement {
     }
   }
 
+  private static void assertSuccessReply(Type expectedType, long 
expectedBytesWritten, DataStreamReply reply) {
+    assertEquals(expectedType, reply.getType());
+    assertTrue(reply.isSuccess());
+    assertEquals(expectedBytesWritten, reply.getBytesWritten());
+    assertTrue(reply instanceof DataStreamReplyByteBuffer);
+  }
+
   private static RaftServer newRaftServer(RaftPeerId serverId, RaftProperties 
properties) {
-    return (RaftServer) 
Proxy.newProxyInstance(TestDataStreamManagement.class.getClassLoader(),
-        new Class<?>[]{RaftServer.class},
+    return newRaftServer(serverId, properties, null, null);
+  }
+
+  private static RaftServer newRaftServer(RaftPeerId serverId, RaftProperties 
properties,
+      RaftGroupId groupId, RaftServer.Division division) {
+    return (RaftServer) 
Proxy.newProxyInstance(RaftServer.class.getClassLoader(), new 
Class<?>[]{RaftServer.class},
         (proxy, method, args) -> {
-          if (method.getDeclaringClass() == Object.class) {
-            switch (method.getName()) {
-              case "toString":
-                return "RaftServerProxy(" + serverId + ")";
-              case "hashCode":
-                return System.identityHashCode(proxy);
-              case "equals":
-                return proxy == args[0];
-              default:
-                return null;
+          switch (method.getName()) {
+          case "getId":
+            return serverId;
+          case "getProperties":
+            return properties;
+          case "getDivision":
+            if (groupId != null && groupId.equals(args[0])) {
+              return division;
             }
+            throw new IOException("Division not found: " + args[0]);
+          case "close":
+            return null;
+          case "toString":
+            return serverId.toString();
+          case "hashCode":
+            return System.identityHashCode(proxy);
+          case "equals":
+            return proxy == args[0];
+          default:
+            throw new UnsupportedOperationException(method.toString());
           }
+        });
+  }
+
+  private static RaftServer.Division newDivision(StateMachine stateMachine) {
+    return (RaftServer.Division) 
Proxy.newProxyInstance(RaftServer.Division.class.getClassLoader(),
+        new Class<?>[]{RaftServer.Division.class},
+        (proxy, method, args) -> {
           switch (method.getName()) {
-            case "getId":
-              return serverId;
-            case "getProperties":
-              return properties;
-            default:
-              throw new UnsupportedOperationException("Unexpected RaftServer 
call: " + method);
+          case "getStateMachine":
+            return stateMachine;
+          case "close":
+            return null;
+          case "toString":
+            return stateMachine.toString();
+          case "hashCode":
+            return System.identityHashCode(proxy);
+          case "equals":
+            return proxy == args[0];
+          default:
+            throw new UnsupportedOperationException(method.toString());
           }
         });
   }

Reply via email to