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 11e9e1f9d RATIS-2559. Add linearizable check for streaming read 
request (#1490)
11e9e1f9d is described below

commit 11e9e1f9d576f5abcc4d7972b860209a6bf9662a
Author: Peter Lee <[email protected]>
AuthorDate: Mon Jul 6 17:23:20 2026 +0800

    RATIS-2559. Add linearizable check for streaming read request (#1490)
---
 dev-support/checkstyle.xml                         |   4 +
 .../apache/ratis/protocol/RaftClientRequest.java   |  68 +++++++++++-
 .../ratis/netty/server/ReadStreamManagement.java   |  54 +++++++--
 ratis-proto/src/main/proto/Raft.proto              |   1 +
 .../apache/ratis/server/impl/RaftServerImpl.java   |  31 +++++-
 .../client/impl/TestDataStreamClientImpl.java      |  17 ---
 .../ratis/datastream/DataStreamClusterTests.java   |   3 -
 .../netty/server/TestDataStreamManagement.java     | 122 ++++++++++++++++++++-
 8 files changed, 256 insertions(+), 44 deletions(-)

diff --git a/dev-support/checkstyle.xml b/dev-support/checkstyle.xml
index db4954fb4..16dd31ad7 100644
--- a/dev-support/checkstyle.xml
+++ b/dev-support/checkstyle.xml
@@ -60,6 +60,10 @@
     </module>
 
     <module name="SuppressWarningsFilter"/>
+    <module name="SuppressionSingleFilter">
+        <property name="checks" value="FileLength"/>
+        <property name="files" 
value="[/\\]ratis-server[/\\]src[/\\]main[/\\]java[/\\]org[/\\]apache[/\\]ratis[/\\]server[/\\]impl[/\\]RaftServerImpl\.java"/>
+    </module>
 
     <!-- Checks that a package-info.java file exists for each package.     -->
     <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->
diff --git 
a/ratis-common/src/main/java/org/apache/ratis/protocol/RaftClientRequest.java 
b/ratis-common/src/main/java/org/apache/ratis/protocol/RaftClientRequest.java
index b04402fe1..ca7b6cc88 100644
--- 
a/ratis-common/src/main/java/org/apache/ratis/protocol/RaftClientRequest.java
+++ 
b/ratis-common/src/main/java/org/apache/ratis/protocol/RaftClientRequest.java
@@ -46,11 +46,11 @@ public class RaftClientRequest extends RaftClientMessage {
   private static final Type WATCH_DEFAULT = new Type(
       
WatchRequestTypeProto.newBuilder().setIndex(0L).setReplication(ReplicationLevel.MAJORITY).build());
 
-  private static final Type READ_AFTER_WRITE_CONSISTENT_DEFAULT
-      = new 
Type(ReadRequestTypeProto.newBuilder().setReadAfterWriteConsistent(true).build());
-  private static final Type READ_DEFAULT = new 
Type(ReadRequestTypeProto.getDefaultInstance());
-  private static final Type READ_NONLINEARIZABLE_DEFAULT
-      = new 
Type(ReadRequestTypeProto.newBuilder().setPreferNonLinearizable(true).build());
+  private static final ReadTypes READ_TYPES = new ReadTypes();
+  private static final Type READ_DEFAULT = readRequestType(false, false, 
false);
+  private static final Type READ_NONLINEARIZABLE_DEFAULT = 
readRequestType(true, false, false);
+  private static final Type READ_AFTER_WRITE_CONSISTENT_DEFAULT = 
readRequestType(false, true, false);
+
   private static final Type STALE_READ_DEFAULT = new 
Type(StaleReadRequestTypeProto.getDefaultInstance());
 
   private static final Map<ReplicationLevel, Type> WRITE_REQUEST_TYPES;
@@ -71,6 +71,44 @@ public class RaftClientRequest extends RaftClientMessage {
     return WRITE_REQUEST_TYPES.get(replication);
   }
 
+  private static final class ReadTypes {
+    private final Type[] array = new Type[8];
+
+    private ReadTypes() {
+      for (int i = 0; i < array.length; i++) {
+        array[i] = new Type(ReadRequestTypeProto.newBuilder()
+            .setPreferNonLinearizable((i & 1) != 0)
+            .setReadAfterWriteConsistent((i & 2) != 0)
+            .setDummy((i & 4) != 0)
+            .build());
+      }
+
+      assertArray();
+    }
+
+    private Type getImpl(boolean nonLinearizable, boolean 
readAfterWriteConsistent, boolean dummy) {
+      final int i = (nonLinearizable ? 1 : 0)
+          | (readAfterWriteConsistent ? 2 : 0)
+          | (dummy ? 4 : 0);
+      return array[i];
+    }
+
+    Type get(boolean nonLinearizable, boolean readAfterWriteConsistent, 
boolean dummy) {
+      if (nonLinearizable && readAfterWriteConsistent) {
+        throw new IllegalArgumentException("Cannot be both nonLinearizable and 
readAfterWriteConsistent");
+      }
+      return getImpl(nonLinearizable, readAfterWriteConsistent, dummy);
+    }
+
+    private void assertArray() {
+      for (final Type type : array) {
+        final ReadRequestTypeProto read = type.getRead();
+        final Type got = getImpl(read.getPreferNonLinearizable(), 
read.getReadAfterWriteConsistent(), read.getDummy());
+        Preconditions.assertSame(type, got, "type");
+      }
+    }
+  }
+
   public static Type writeRequestType() {
     return writeRequestType(ReplicationLevel.MAJORITY);
   }
@@ -91,6 +129,10 @@ public class RaftClientRequest extends RaftClientMessage {
         .build());
   }
 
+  public static Type readRequestType(boolean nonLinearizable, boolean 
readAfterWriteConsistent, boolean dummy) {
+    return READ_TYPES.get(nonLinearizable, readAfterWriteConsistent, dummy);
+  }
+
   public static Type readAfterWriteConsistentRequestType() {
     return READ_AFTER_WRITE_CONSISTENT_DEFAULT;
   }
@@ -312,6 +354,22 @@ public class RaftClientRequest extends RaftClientMessage {
       return new RaftClientRequest(this);
     }
 
+    public Builder set(RaftClientRequest request) {
+      this.clientId = request.getClientId();
+      this.serverId = request.getServerId();
+      this.groupId = request.getRaftGroupId();
+      this.callId = request.getCallId();
+      this.toLeader = request.isToLeader();
+      this.repliedCallIds = request.getRepliedCallIds();
+      this.message = request.getMessage();
+      this.type = request.getType();
+      this.slidingWindowEntry = request.getSlidingWindowEntry();
+      this.routingTable = request.getRoutingTable();
+      this.timeoutMs = request.getTimeoutMs();
+      this.spanContext = request.getSpanContext();
+      return this;
+    }
+
     public Builder setClientId(ClientId clientId) {
       this.clientId = clientId;
       return this;
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 5336760a0..46d169bbf 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,9 +17,11 @@
  */
 package org.apache.ratis.netty.server;
 
+import org.apache.ratis.client.impl.OrderedAsync;
 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;
 import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
 import org.apache.ratis.proto.RaftProtos.RaftClientRequestProto;
 import org.apache.ratis.proto.RaftProtos.RaftClientRequestProto.TypeCase;
@@ -47,6 +49,7 @@ 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.newDataStreamReplyByteBuffer;
 import static 
org.apache.ratis.netty.server.DataStreamManagement.replyDataStreamException;
 
 public class ReadStreamManagement {
@@ -60,23 +63,24 @@ public class ReadStreamManagement {
     private final DataStreamReplyByteBuffer terminalReply;
     private long streamOffset;
 
-    ReadStream(RaftClientRequest request, long streamId, ChannelHandlerContext 
ctx) {
+    ReadStream(RaftClientRequest request, long streamId, ChannelHandlerContext 
ctx, RaftClientReply terminalReply) {
       this.clientId = request.getClientId();
       this.streamId = streamId;
       this.ctx = ctx;
 
-      final RaftClientReply reply = RaftClientReply.newBuilder()
-          .setRequest(request)
-          .setSuccess()
-          .build();
-      this.terminalReply = DataStreamReplyByteBuffer.newBuilder()
+      this.terminalReply = newReadStreamTerminalReply(clientId, streamId, 
terminalReply);
+    }
+
+    private static DataStreamReplyByteBuffer newReadStreamTerminalReply(
+        ClientId clientId, long streamId, RaftClientReply reply) {
+      return DataStreamReplyByteBuffer.newBuilder()
           .setClientId(clientId)
           .setType(Type.STREAM_HEADER)
           .setStreamId(streamId)
           .setStreamOffset(0)
           
.setBuffer(toRaftClientReplyProto(reply).toByteString().asReadOnlyByteBuffer())
-          .setSuccess(true)
-          .setBytesWritten(0)
+          .setSuccess(reply.isSuccess())
+          .setCommitInfos(reply.getCommitInfos())
           .build();
     }
 
@@ -186,17 +190,45 @@ public class ReadStreamManagement {
       return true;
     }
 
-    final ReadStream stream = new ReadStream(request, 
requestBuf.getStreamId(), ctx);
-    requestExecutor.execute(() -> {
+    final CompletableFuture<RaftClientReply> readCheck;
+    try {
+      readCheck = 
server.submitClientRequestAsync(newDummyReadRequest(request));
+    } catch (IOException e) {
+      replyDataStreamException(server, e, request, requestBuf, ctx);
+      return true;
+    }
+
+    readCheck.whenCompleteAsync((readCheckReply, exception) -> {
+      if (exception != null) {
+        replyDataStreamException(server, exception, request, requestBuf, ctx);
+        return;
+      }
+
+      if (!readCheckReply.isSuccess()) {
+        ctx.writeAndFlush(newDataStreamReplyByteBuffer(requestBuf, 
readCheckReply));
+        return;
+      }
+
+      final ReadStream stream = new ReadStream(request, 
requestBuf.getStreamId(), ctx, readCheckReply);
       try {
         division.getStateMachine().data().query(request.getMessage(), stream);
       } catch (Throwable t) {
         LOG.error("{}: Failed read-only data stream query for {}", this, 
request, t);
       }
-    });
+    }, requestExecutor);
     return true;
   }
 
+  private static RaftClientRequest newDummyReadRequest(RaftClientRequest 
request) {
+    final RaftProtos.ReadRequestTypeProto original = 
request.getType().getRead();
+    return RaftClientRequest.newBuilder()
+        .set(request)
+        .setMessage(OrderedAsync.DUMMY)
+        .setType(RaftClientRequest.readRequestType(
+            original.getPreferNonLinearizable(), 
original.getReadAfterWriteConsistent(), true))
+        .build();
+  }
+
   @Override
   public String toString() {
     return name;
diff --git a/ratis-proto/src/main/proto/Raft.proto 
b/ratis-proto/src/main/proto/Raft.proto
index eba5de3b7..c1bb9d64e 100644
--- a/ratis-proto/src/main/proto/Raft.proto
+++ b/ratis-proto/src/main/proto/Raft.proto
@@ -302,6 +302,7 @@ message ForwardRequestTypeProto {
 message ReadRequestTypeProto {
   bool preferNonLinearizable = 1;
   bool readAfterWriteConsistent = 2;
+  bool dummy = 3;
 }
 
 message StaleReadRequestTypeProto {
diff --git 
a/ratis-server/src/main/java/org/apache/ratis/server/impl/RaftServerImpl.java 
b/ratis-server/src/main/java/org/apache/ratis/server/impl/RaftServerImpl.java
index f758fd0ed..f4e740407 100644
--- 
a/ratis-server/src/main/java/org/apache/ratis/server/impl/RaftServerImpl.java
+++ 
b/ratis-server/src/main/java/org/apache/ratis/server/impl/RaftServerImpl.java
@@ -43,6 +43,7 @@ import 
org.apache.ratis.proto.RaftProtos.RequestVoteRequestProto;
 import org.apache.ratis.proto.RaftProtos.RoleInfoProto;
 import org.apache.ratis.proto.RaftProtos.StartLeaderElectionReplyProto;
 import org.apache.ratis.proto.RaftProtos.StartLeaderElectionRequestProto;
+import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.protocol.ClientInvocationId;
 import org.apache.ratis.protocol.GroupInfoReply;
 import org.apache.ratis.protocol.GroupInfoRequest;
@@ -260,6 +261,8 @@ class RaftServerImpl implements RaftServer.Division,
   private final ExecutorService clientExecutor;
   private final ThreadGroup threadGroup;
 
+  private final CompletableFuture<RaftClientReply> dummySuccessReply;
+
   RaftServerImpl(RaftGroup group, StateMachine stateMachine, RaftServerProxy 
proxy, RaftStorage.StartupOption option)
       throws IOException {
     final RaftPeerId id = proxy.getId();
@@ -303,6 +306,13 @@ class RaftServerImpl implements RaftServer.Division,
         RaftServerConfigKeys.ThreadPool.clientSize(properties),
         id + "-client");
     this.threadGroup = new ThreadGroup(proxy.getThreadGroup(), 
getMemberId().toString());
+
+    this.dummySuccessReply = 
CompletableFuture.completedFuture(RaftClientReply.newBuilder()
+        .setClientId(ClientId.emptyClientId())
+        .setServerId(id)
+        .setGroupId(group.getGroupId())
+        .setSuccess()
+        .build());
   }
 
   private long getCommitIndex(RaftPeerId id) {
@@ -1113,11 +1123,12 @@ class RaftServerImpl implements RaftServer.Division,
     if (request.getType().getRead().getPreferNonLinearizable()
         || readOption == RaftServerConfigKeys.Read.Option.DEFAULT) {
       final CompletableFuture<RaftClientReply> reply = 
checkLeaderState(request);
-       if (reply != null) {
-         return reply;
-       }
-       return queryStateMachine(request);
-    } else if (readOption == RaftServerConfigKeys.Read.Option.LINEARIZABLE){
+      if (reply != null) {
+        return reply;
+      }
+      return isDummyRead(request) ? 
CompletableFuture.completedFuture(newSuccessReply(request))
+          : queryStateMachine(request);
+    } else if (readOption == RaftServerConfigKeys.Read.Option.LINEARIZABLE) {
       final LeaderStateImpl leader = role.getLeaderState().orElse(null);
       final CompletableFuture<Long> replyFuture;
       if (leader != null) {
@@ -1136,12 +1147,17 @@ class RaftServerImpl implements RaftServer.Division,
       return replyFuture
           .thenCompose(readIndex -> 
getState().getReadRequests().waitToAdvance(readIndex,
               () -> getReadException("add", 
snapshotInstallationHandler.getInProgressInstallSnapshotIndex(), false)))
-          .thenCompose(readIndex -> queryStateMachine(request))
+          .thenCompose(readIndex -> isDummyRead(request)
+              ? CompletableFuture.completedFuture(newSuccessReply(request)) : 
queryStateMachine(request))
           .exceptionally(e -> readException2Reply(request, e));
     } else {
       throw new IllegalStateException("Unexpected read option: " + readOption);
     }
   }
+  private static boolean isDummyRead(RaftClientRequest request) {
+    return request.getMessage() != null && 
OrderedAsync.DUMMY.getContent().equals(request.getMessage().getContent());
+  }
+
   private RaftClientReply readException2Reply(RaftClientRequest request, 
Throwable e) {
     e = JavaUtils.unwrapCompletionException(e);
     if (e instanceof StateMachineException ) {
@@ -1183,6 +1199,9 @@ class RaftServerImpl implements RaftServer.Division,
   }
 
   CompletableFuture<RaftClientReply> queryStateMachine(RaftClientRequest 
request) {
+    if (request.getType().getRead().getDummy()) {
+      return dummySuccessReply;
+    }
     return processQueryFuture(stateMachine.query(request.getMessage()), 
request);
   }
 
diff --git 
a/ratis-test/src/test/java/org/apache/ratis/client/impl/TestDataStreamClientImpl.java
 
b/ratis-test/src/test/java/org/apache/ratis/client/impl/TestDataStreamClientImpl.java
index 523c49f05..a85a9f483 100644
--- 
a/ratis-test/src/test/java/org/apache/ratis/client/impl/TestDataStreamClientImpl.java
+++ 
b/ratis-test/src/test/java/org/apache/ratis/client/impl/TestDataStreamClientImpl.java
@@ -22,9 +22,7 @@ import org.apache.ratis.client.DataStreamClientRpc;
 import org.apache.ratis.client.api.DataStreamInput;
 import org.apache.ratis.conf.RaftProperties;
 import org.apache.ratis.datastream.DataStreamObserver;
-import org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuffer;
-import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
 import org.apache.ratis.proto.RaftProtos.RaftClientRequestProto;
 import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.protocol.DataStreamReply;
@@ -32,7 +30,6 @@ import org.apache.ratis.protocol.DataStreamRequest;
 import org.apache.ratis.protocol.RaftClientRequest;
 import org.apache.ratis.protocol.RaftGroupId;
 import org.apache.ratis.protocol.RaftPeer;
-import org.apache.ratis.thirdparty.io.netty.buffer.Unpooled;
 import org.apache.ratis.util.ReferenceCountedObject;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -69,20 +66,6 @@ public class TestDataStreamClientImpl {
       return future;
     }
 
-    RaftClientRequest getRequest() {
-      return request.get();
-    }
-
-    void receive(DataStreamReplyByteBuf reply) {
-      final ReferenceCountedObject<DataStreamReply> ref = 
DataStreamReplyByteBuf.asReferenceCounted(reply);
-      ref.retain();
-      try {
-        replyHandler.get().onNext(ref);
-      } finally {
-        ref.release();
-      }
-    }
-
     void complete() {
       replyHandler.get().onCompleted();
       replyFuture.get().complete(null);
diff --git 
a/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamClusterTests.java
 
b/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamClusterTests.java
index 95e9bef49..7478c3628 100644
--- 
a/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamClusterTests.java
+++ 
b/ratis-test/src/test/java/org/apache/ratis/datastream/DataStreamClusterTests.java
@@ -32,9 +32,6 @@ import 
org.apache.ratis.datastream.impl.DataStreamReplyByteBuf;
 import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
 import org.apache.ratis.proto.RaftProtos.ReplicationLevel;
 import org.apache.ratis.protocol.DataStreamReply;
-import org.apache.ratis.protocol.DataStreamRequest;
-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.retry.RetryPolicies;
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 188f119fc..56ecd166b 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
@@ -19,6 +19,7 @@ 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.client.impl.OrderedAsync;
 import org.apache.ratis.conf.RaftProperties;
 import org.apache.ratis.datastream.impl.DataStreamReplyByteBuffer;
 import org.apache.ratis.datastream.impl.DataStreamRequestByteBuf;
@@ -28,10 +29,12 @@ 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.RaftClientReply;
 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.protocol.exceptions.ReadIndexException;
 import org.apache.ratis.server.RaftServer;
 import org.apache.ratis.statemachine.StateMachine;
 import org.apache.ratis.statemachine.StateMachine.DataApi;
@@ -59,14 +62,17 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
 
 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.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class TestDataStreamManagement {
@@ -96,8 +102,9 @@ class TestDataStreamManagement {
       assertTrue(management.process(readOnlyRequest.request, 
embeddedChannel.pipeline().firstContext()));
       assertEquals(0, readOnlyRequest.headerBuf.refCnt());
 
+      JavaUtils.attempt(() -> assertNotNull(streamRef.get()), 10,
+          TimeDuration.valueOf(100, TimeUnit.MILLISECONDS), "read-only 
stream", null);
       final WritableByteChannel stream = streamRef.get();
-      assertNotNull(stream);
       stream.write(response.asReadOnlyByteBuffer());
       stream.close();
 
@@ -116,6 +123,99 @@ class TestDataStreamManagement {
       
assertTrue(ClientProtoUtils.getRaftClientReply(replies.get(1)).isSuccess());
     } finally {
       embeddedChannel.finishAndReleaseAll();
+      management.shutdown();
+    }
+  }
+
+  @Test
+  void readOnlyRequestWaitsForLinearizableCheck() 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 CompletableFuture<RaftClientReply> readOnlyCheck = new 
CompletableFuture<>();
+    final AtomicReference<RaftClientRequest> submittedReadOnlyCheck = new 
AtomicReference<>();
+    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 ReadStreamManagement management = newReadStreamManagement(serverId, 
groupId, dataApi, request -> {
+      submittedReadOnlyCheck.set(request);
+      return readOnlyCheck;
+    });
+    final EmbeddedChannel embeddedChannel = new EmbeddedChannel(new 
ChannelInboundHandlerAdapter());
+    final ReadOnlyRequest readOnlyRequest = newReadOnlyRequest(clientId, 
serverId, groupId, 1L, query);
+
+    try {
+      assertTrue(management.process(readOnlyRequest.request, 
embeddedChannel.pipeline().firstContext()));
+      assertEquals(0, readOnlyRequest.headerBuf.refCnt());
+
+      final RaftClientRequest checkRequest = submittedReadOnlyCheck.get();
+      assertNotNull(checkRequest);
+      assertEquals(OrderedAsync.DUMMY.getContent(), 
checkRequest.getMessage().getContent());
+      assertNull(streamRef.get(), "state machine query should wait for the 
read-only check");
+
+      
readOnlyCheck.complete(RaftClientReply.newBuilder().setRequest(checkRequest).setSuccess().build());
+      JavaUtils.attempt(() -> assertNotNull(streamRef.get()), 10,
+          TimeDuration.valueOf(100, TimeUnit.MILLISECONDS), "linearizable 
read-only stream", null);
+      assertEquals(query, messageRef.get().getContent());
+    } finally {
+      embeddedChannel.finishAndReleaseAll();
+      management.shutdown();
+    }
+  }
+
+  @Test
+  void readOnlyCheckFailureSkipsStateMachineQuery() 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 AtomicBoolean queryCalled = new AtomicBoolean();
+
+    final DataApi dataApi = new DataApi() {
+      @Override
+      public void query(Message request, WritableByteChannel stream) {
+        queryCalled.set(true);
+      }
+    };
+    final ReadStreamManagement management = newReadStreamManagement(serverId, 
groupId, dataApi, request ->
+        CompletableFuture.completedFuture(RaftClientReply.newBuilder()
+            .setRequest(request)
+            .setException(new ReadIndexException("read index failed"))
+            .build()));
+    final EmbeddedChannel embeddedChannel = new EmbeddedChannel(new 
ChannelInboundHandlerAdapter());
+    final ReadOnlyRequest readOnlyRequest = newReadOnlyRequest(clientId, 
serverId, groupId, 1L, query);
+
+    try {
+      assertTrue(management.process(readOnlyRequest.request, 
embeddedChannel.pipeline().firstContext()));
+      assertEquals(0, readOnlyRequest.headerBuf.refCnt());
+
+      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 
check failure reply", null);
+
+      assertFalse(queryCalled.get(), "state machine query should not run when 
the read-only check fails");
+      final DataStreamReply reply = replies.get(0);
+      assertEquals(Type.STREAM_HEADER, reply.getType());
+      assertFalse(reply.isSuccess());
+      final RaftClientReply clientReply = 
ClientProtoUtils.getRaftClientReply(reply);
+      assertFalse(clientReply.isSuccess());
+      assertNotNull(clientReply.getReadIndexException());
+      assertEquals(serverId, clientReply.getServerId());
+    } finally {
+      embeddedChannel.finishAndReleaseAll();
+      management.shutdown();
     }
   }
 
@@ -231,13 +331,19 @@ class TestDataStreamManagement {
 
   private static ReadStreamManagement newReadStreamManagement(
       RaftPeerId serverId, RaftGroupId groupId, DataApi dataApi) {
+    return newReadStreamManagement(serverId, groupId, dataApi, 
TestDataStreamManagement::successReply);
+  }
+
+  private static ReadStreamManagement newReadStreamManagement(RaftPeerId 
serverId, RaftGroupId groupId,
+      DataApi dataApi, Function<RaftClientRequest, 
CompletableFuture<RaftClientReply>> submitClientRequestAsync) {
     final StateMachine stateMachine = new BaseStateMachine() {
       @Override
       public DataApi data() {
         return dataApi;
       }
     };
-    final RaftServer server = newRaftServer(serverId, new RaftProperties(), 
groupId, newDivision(stateMachine));
+    final RaftServer server = newRaftServer(serverId, new RaftProperties(), 
groupId, newDivision(stateMachine),
+        submitClientRequestAsync);
     return new ReadStreamManagement(server);
   }
 
@@ -275,6 +381,16 @@ class TestDataStreamManagement {
 
   private static RaftServer newRaftServer(RaftPeerId serverId, RaftProperties 
properties,
       RaftGroupId groupId, RaftServer.Division division) {
+    return newRaftServer(serverId, properties, groupId, division, 
TestDataStreamManagement::successReply);
+  }
+
+  private static CompletableFuture<RaftClientReply> 
successReply(RaftClientRequest request) {
+    return 
CompletableFuture.completedFuture(RaftClientReply.newBuilder().setRequest(request).setSuccess().build());
+  }
+
+  private static RaftServer newRaftServer(RaftPeerId serverId, RaftProperties 
properties,
+      RaftGroupId groupId, RaftServer.Division division,
+      Function<RaftClientRequest, CompletableFuture<RaftClientReply>> 
submitClientRequestAsync) {
     return (RaftServer) 
Proxy.newProxyInstance(RaftServer.class.getClassLoader(), new 
Class<?>[]{RaftServer.class},
         (proxy, method, args) -> {
           switch (method.getName()) {
@@ -287,6 +403,8 @@ class TestDataStreamManagement {
               return division;
             }
             throw new IOException("Division not found: " + args[0]);
+          case "submitClientRequestAsync":
+            return submitClientRequestAsync.apply((RaftClientRequest) args[0]);
           case "close":
             return null;
           case "toString":

Reply via email to