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 0355d33e0 RATIS-2511. Follower should throw ReadException if it is
installing snapshot (#1444)
0355d33e0 is described below
commit 0355d33e06c98d8410260bb2c36feba0661af16f
Author: Ivan Andika <[email protected]>
AuthorDate: Wed May 20 00:55:56 2026 +0800
RATIS-2511. Follower should throw ReadException if it is installing
snapshot (#1444)
---
.../apache/ratis/server/impl/RaftServerImpl.java | 24 +++----
.../org/apache/ratis/server/impl/ReadRequests.java | 26 ++++++--
.../server/impl/SnapshotInstallationHandler.java | 3 +-
.../org/apache/ratis/LinearizableReadTests.java | 7 +-
.../org/apache/ratis/ReadOnlyRequestTests.java | 74 ++++++++++++++++++++++
5 files changed, 113 insertions(+), 21 deletions(-)
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 a35d7e0f2..f758fd0ed 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
@@ -1086,12 +1086,15 @@ class RaftServerImpl implements RaftServer.Division,
}
return processQueryFuture(stateMachine.queryStale(request.getMessage(),
minIndex), request);
}
-
- ReadRequests getReadRequests() {
- return getState().getReadRequests();
+ ReadException getReadException(String op, long installSnapshot, boolean
started) {
+ return installSnapshot == RaftLog.INVALID_LOG_INDEX ? null : new
ReadException(getMemberId() + ": Failed to " + op
+ + " readIndex as snapshot (" + installSnapshot + ") installation is "
+ (started ? "started" : "in progress"));
}
-
private CompletableFuture<ReadIndexReplyProto>
sendReadIndexAsync(RaftClientRequest clientRequest) {
+ final long installSnapshot =
snapshotInstallationHandler.getInProgressInstallSnapshotIndex();
+ if (installSnapshot != RaftLog.INVALID_LOG_INDEX) {
+ return JavaUtils.completeExceptionally(getReadException("get",
installSnapshot, false));
+ }
final RaftPeerId leaderId = getInfo().getLeaderId();
if (leaderId == null) {
return JavaUtils.completeExceptionally(new
ReadIndexException(getMemberId() + ": Leader is unknown."));
@@ -1103,11 +1106,9 @@ class RaftServerImpl implements RaftServer.Division,
return JavaUtils.completeExceptionally(e);
}
}
-
private CompletableFuture<Long> getReadIndex(RaftClientRequest request,
LeaderStateImpl leader) {
return
writeIndexCache.getWriteIndexFuture(request).thenCompose(leader::getReadIndex);
}
-
private CompletableFuture<RaftClientReply> readAsync(RaftClientRequest
request) {
if (request.getType().getRead().getPreferNonLinearizable()
|| readOption == RaftServerConfigKeys.Read.Option.DEFAULT) {
@@ -1117,14 +1118,7 @@ class RaftServerImpl implements RaftServer.Division,
}
return queryStateMachine(request);
} else if (readOption == RaftServerConfigKeys.Read.Option.LINEARIZABLE){
- /*
- Linearizable read using ReadIndex. See Raft paper section 6.4.
- 1. First obtain readIndex from Leader.
- 2. Then waits for statemachine to advance at least as far as readIndex.
- 3. Finally, query the statemachine and return the result.
- */
final LeaderStateImpl leader = role.getLeaderState().orElse(null);
-
final CompletableFuture<Long> replyFuture;
if (leader != null) {
replyFuture = getReadIndex(request, leader);
@@ -1140,14 +1134,14 @@ class RaftServerImpl implements RaftServer.Division,
}
return replyFuture
- .thenCompose(readIndex -> getReadRequests().waitToAdvance(readIndex))
+ .thenCompose(readIndex ->
getState().getReadRequests().waitToAdvance(readIndex,
+ () -> getReadException("add",
snapshotInstallationHandler.getInProgressInstallSnapshotIndex(), false)))
.thenCompose(readIndex -> queryStateMachine(request))
.exceptionally(e -> readException2Reply(request, e));
} else {
throw new IllegalStateException("Unexpected read option: " + readOption);
}
}
-
private RaftClientReply readException2Reply(RaftClientRequest request,
Throwable e) {
e = JavaUtils.unwrapCompletionException(e);
if (e instanceof StateMachineException ) {
diff --git
a/ratis-server/src/main/java/org/apache/ratis/server/impl/ReadRequests.java
b/ratis-server/src/main/java/org/apache/ratis/server/impl/ReadRequests.java
index 6112a4600..df6f3d93f 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/ReadRequests.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/ReadRequests.java
@@ -20,16 +20,19 @@ package org.apache.ratis.server.impl;
import org.apache.ratis.conf.RaftProperties;
import org.apache.ratis.protocol.exceptions.ReadException;
import org.apache.ratis.server.RaftServerConfigKeys;
+import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.Preconditions;
import org.apache.ratis.util.TimeDuration;
import org.apache.ratis.util.TimeoutExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Collection;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.function.LongConsumer;
+import java.util.function.Supplier;
/** For supporting linearizable read. */
class ReadRequests {
@@ -43,7 +46,7 @@ class ReadRequests {
* Map : readIndex -> appliedIndexFuture (when completes, readIndex
<= appliedIndex).
* Invariant: all keys > lastAppliedIndex.
*/
- private final NavigableMap<Long, CompletableFuture<Long>> sorted = new
TreeMap<>();
+ private NavigableMap<Long, CompletableFuture<Long>> sorted = new
TreeMap<>();
private final TimeDuration readTimeout;
@@ -52,10 +55,14 @@ class ReadRequests {
this.readTimeout = readTimeout;
}
- CompletableFuture<Long> add(long readIndex) {
+ CompletableFuture<Long> add(long readIndex, Supplier<ReadException>
checkReadException) {
final CompletableFuture<Long> returned;
final boolean create;
synchronized (this) {
+ final ReadException exception = checkReadException.get();
+ if (exception != null) {
+ return JavaUtils.completeExceptionally(exception);
+ }
if (readIndex <= lastAppliedIndex) {
return CompletableFuture.completedFuture(lastAppliedIndex);
}
@@ -88,6 +95,11 @@ class ReadRequests {
removed.completeExceptionally(new ReadException("Read timeout " +
readTimeout + " for index " + readIndex));
}
+ synchronized Collection<CompletableFuture<Long>> clear() {
+ final Collection<CompletableFuture<Long>> futures = sorted.values();
+ sorted = new TreeMap<>();
+ return futures;
+ }
/** Complete all the entries less than or equal to the given applied
index. */
synchronized void complete(long appliedIndex) {
@@ -119,7 +131,13 @@ class ReadRequests {
return readIndexQueue::complete;
}
- CompletableFuture<Long> waitToAdvance(long readIndex) {
- return readIndexQueue.add(readIndex);
+ CompletableFuture<Long> waitToAdvance(long readIndex,
Supplier<ReadException> checkReadException) {
+ return readIndexQueue.add(readIndex, checkReadException);
+ }
+
+ void fail(Throwable cause) {
+ for (CompletableFuture<Long> f : readIndexQueue.clear()) {
+ f.completeExceptionally(cause);
+ }
}
}
diff --git
a/ratis-server/src/main/java/org/apache/ratis/server/impl/SnapshotInstallationHandler.java
b/ratis-server/src/main/java/org/apache/ratis/server/impl/SnapshotInstallationHandler.java
index 46b6aaf87..870625662 100644
---
a/ratis-server/src/main/java/org/apache/ratis/server/impl/SnapshotInstallationHandler.java
+++
b/ratis-server/src/main/java/org/apache/ratis/server/impl/SnapshotInstallationHandler.java
@@ -276,6 +276,7 @@ class SnapshotInstallationHandler {
InstallSnapshotResult.ALREADY_INSTALLED, snapshotIndex);
return future.thenApply(dummy -> reply);
}
+ server.getState().getReadRequests().fail(server.getReadException("wait
for", firstAvailableLogIndex, true));
final RaftPeerProto leaderProto;
if (!request.hasLastRaftConfigurationLogEntryProto()) {
@@ -401,4 +402,4 @@ class SnapshotInstallationHandler {
.setFollowerInfo(followerInfo)
.build();
}
-}
\ No newline at end of file
+}
diff --git
a/ratis-server/src/test/java/org/apache/ratis/LinearizableReadTests.java
b/ratis-server/src/test/java/org/apache/ratis/LinearizableReadTests.java
index 09781b546..832457e9d 100644
--- a/ratis-server/src/test/java/org/apache/ratis/LinearizableReadTests.java
+++ b/ratis-server/src/test/java/org/apache/ratis/LinearizableReadTests.java
@@ -169,6 +169,11 @@ public abstract class LinearizableReadTests<CLUSTER
extends MiniRaftCluster>
}
}
+ @Test
+ public void testFollowerLinearizableReadFailsWhenInstallingSnapshot() throws
Exception {
+
runWithNewCluster(ReadOnlyRequestTests::runTestFollowerLinearizableReadFailsWhenInstallingSnapshot);
+ }
+
@Test
public void testFollowerLinearizableReadParallel() throws Exception {
runWithNewCluster(LinearizableReadTests::runTestFollowerReadOnlyParallel);
@@ -285,4 +290,4 @@ public abstract class LinearizableReadTests<CLUSTER extends
MiniRaftCluster>
assertReplyAtLeast(2, asyncReply.join());
}
}
-}
\ No newline at end of file
+}
diff --git
a/ratis-server/src/test/java/org/apache/ratis/ReadOnlyRequestTests.java
b/ratis-server/src/test/java/org/apache/ratis/ReadOnlyRequestTests.java
index 94e9433b1..d88780764 100644
--- a/ratis-server/src/test/java/org/apache/ratis/ReadOnlyRequestTests.java
+++ b/ratis-server/src/test/java/org/apache/ratis/ReadOnlyRequestTests.java
@@ -40,8 +40,12 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.event.Level;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicLong;
public abstract class ReadOnlyRequestTests<CLUSTER extends MiniRaftCluster>
@@ -139,6 +143,76 @@ public abstract class ReadOnlyRequestTests<CLUSTER extends
MiniRaftCluster>
}
}
+ private static void setInProgressInstallSnapshotIndex(RaftServer.Division
server, long index) throws Exception {
+ final Field snapshotInstallationHandler =
server.getClass().getDeclaredField("snapshotInstallationHandler");
+ snapshotInstallationHandler.setAccessible(true);
+ final Object handler = snapshotInstallationHandler.get(server);
+ final Field inProgressInstallSnapshotIndex = handler.getClass()
+ .getDeclaredField("inProgressInstallSnapshotIndex");
+ inProgressInstallSnapshotIndex.setAccessible(true);
+ ((AtomicLong) inProgressInstallSnapshotIndex.get(handler)).set(index);
+ }
+
+ private static void startSnapshotInstallation(RaftServer.Division server,
long index) throws Exception {
+ setInProgressInstallSnapshotIndex(server, index);
+ final Method getState = server.getClass().getDeclaredMethod("getState");
+ getState.setAccessible(true);
+ final Object state = getState.invoke(server);
+ final Method getReadRequests =
state.getClass().getDeclaredMethod("getReadRequests");
+ getReadRequests.setAccessible(true);
+ final Object readRequests = getReadRequests.invoke(state);
+ final Method fail = readRequests.getClass().getDeclaredMethod("fail",
Throwable.class);
+ fail.setAccessible(true);
+ fail.invoke(readRequests, new ReadException(server.getMemberId()
+ + ": Failed to wait for readIndex as snapshot (" + index + ")
installation is started"));
+ }
+
+ static void assertSnapshotInstallationReadException(Throwable exception) {
+ final Throwable cause = exception instanceof CompletionException &&
exception.getCause() != null
+ ? exception.getCause() : exception;
+ Assertions.assertInstanceOf(ReadException.class, cause);
+ Assertions.assertTrue(cause.getMessage().contains("snapshot (1)
installation is"),
+ () -> "Unexpected exception: " + exception);
+ }
+
+ static <C extends MiniRaftCluster> void
runTestFollowerLinearizableReadFailsWhenInstallingSnapshot(C cluster)
+ throws Exception {
+ final RaftPeerId leaderId = RaftTestUtil.waitForLeader(cluster).getId();
+
+ final List<RaftServer.Division> followers = cluster.getFollowers();
+ Assertions.assertEquals(2, followers.size());
+
+ final RaftServer.Division follower = followers.get(0);
+ final RaftPeerId followerId = follower.getId();
+
+ try (RaftClient leaderClient = cluster.createClient(leaderId);
+ RaftClient followerClient = cluster.createClient(followerId,
RetryPolicies.noRetry())) {
+ assertReplyExact(1, leaderClient.io().send(INCREMENT));
+ assertReplyExact(1, followerClient.io().sendReadOnly(QUERY, followerId));
+
+ final CompletableFuture<RaftClientReply> writeReply =
leaderClient.async().send(WAIT_AND_INCREMENT);
+ Thread.sleep(100);
+ final CompletableFuture<RaftClientReply> pendingRead =
followerClient.async().sendReadOnly(QUERY, followerId);
+ Assertions.assertFalse(pendingRead.isDone(), () -> "pendingRead=" +
pendingRead);
+
+ startSnapshotInstallation(follower, 1);
+ try {
+ final CompletionException pendingException =
Assertions.assertThrows(CompletionException.class,
+ pendingRead::join);
+ assertSnapshotInstallationReadException(pendingException);
+
+ final ReadException readException =
Assertions.assertThrows(ReadException.class,
+ () -> followerClient.io().sendReadOnly(QUERY, followerId));
+ assertSnapshotInstallationReadException(readException);
+ } finally {
+ setInProgressInstallSnapshotIndex(follower, -1);
+ }
+
+ assertReplyExact(2, writeReply.join());
+ assertReplyExact(2, followerClient.io().sendReadOnly(QUERY, followerId));
+ }
+ }
+
static int retrieve(RaftClientReply reply) {
Assertions.assertTrue(reply.isSuccess());
return
Integer.parseInt(reply.getMessage().getContent().toString(StandardCharsets.UTF_8));