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 978065786 RATIS-2506. Delay consecutive GrpcLogAppender restart. 
(#1450)
978065786 is described below

commit 97806578670d868fe6526fc393f37b588e5a7cb5
Author: Tsz-Wo Nicholas Sze <[email protected]>
AuthorDate: Thu May 14 09:59:57 2026 -0700

    RATIS-2506. Delay consecutive GrpcLogAppender restart. (#1450)
---
 .../apache/ratis/examples/filestore/FileInfo.java  |  2 +-
 .../ratis/examples/filestore/FileStoreClient.java  | 11 ++++++++
 .../examples/filestore/FileStoreStateMachine.java  | 31 +++++++++++++++++++++
 .../examples/filestore/FileStoreBaseTest.java      | 14 ++++++++++
 .../ratis/examples/filestore/FileStoreWriter.java  |  4 +--
 .../apache/ratis/grpc/server/GrpcLogAppender.java  | 32 ++++++++++------------
 .../apache/ratis/server/leader/FollowerInfo.java   | 30 ++++++++++++++++++++
 .../apache/ratis/server/impl/FollowerInfoImpl.java | 32 ++++++++++++++++++++++
 8 files changed, 136 insertions(+), 20 deletions(-)

diff --git 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileInfo.java
 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileInfo.java
index c7d8cb7cd..c3ec21f64 100644
--- 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileInfo.java
+++ 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileInfo.java
@@ -276,7 +276,7 @@ abstract class FileInfo {
       final WriteInfo info = writeInfos.get(index);
       if (info == null) {
         return JavaUtils.completeExceptionally(
-            new IOException(name.get() + " is already committed."));
+            new IOException(name.get() + " not found."));
       }
 
       final CheckedSupplier<Integer, IOException> task = 
LogUtils.newCheckedSupplier(LOG, () -> {
diff --git 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreClient.java
 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreClient.java
index c223b100b..fb0ee49dc 100644
--- 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreClient.java
+++ 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreClient.java
@@ -171,6 +171,17 @@ public class FileStoreClient implements Closeable {
 
   public long write(String path, long offset, boolean close, ByteBuffer 
buffer, boolean sync)
       throws IOException {
+    LOG.trace("write {}, offset={}, length={}, close? {}", path, offset, 
buffer.remaining(), close);
+    try {
+      return writeImpl(path, offset, close, buffer, sync);
+    } catch (IOException e) {
+      LOG.error("Failed to write {}, offset {}, close? {}, sync? {}", path, 
offset, close, sync, e);
+      throw e;
+    }
+  }
+
+  private long writeImpl(String path, long offset, boolean close, ByteBuffer 
buffer, boolean sync)
+      throws IOException {
     final int chunkSize = FileStoreCommon.getChunkSize(buffer.remaining());
     buffer.limit(chunkSize);
     final ByteString reply = writeImpl(this::send, path, offset, close, 
buffer, sync);
diff --git 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreStateMachine.java
 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreStateMachine.java
index 5f258ee3b..d9a1463b9 100644
--- 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreStateMachine.java
+++ 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStoreStateMachine.java
@@ -31,6 +31,7 @@ import org.apache.ratis.proto.RaftProtos.LogEntryProto;
 import org.apache.ratis.protocol.Message;
 import org.apache.ratis.protocol.RaftClientRequest;
 import org.apache.ratis.protocol.RaftGroupId;
+import org.apache.ratis.protocol.RaftPeerId;
 import org.apache.ratis.server.RaftServer;
 import org.apache.ratis.server.storage.RaftStorage;
 import org.apache.ratis.statemachine.StateMachineStorage;
@@ -44,8 +45,23 @@ import org.apache.ratis.util.FileUtils;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
 
 public class FileStoreStateMachine extends BaseStateMachine {
+  static class SimulateFailure {
+    static final String PATH_TO_FAIL = "path-to-fail";
+    private static final AtomicReference<RaftPeerId> CHOSEN = new 
AtomicReference<>();
+
+    static boolean chooseServer(RaftPeerId serverId) {
+      if (SimulateFailure.CHOSEN.compareAndSet(null, serverId)) {
+        LOG.info("Server {} is chosen", serverId);
+        return true;
+      } else {
+        return serverId.equals(SimulateFailure.CHOSEN.get());
+      }
+    }
+  }
+
   private final SimpleStateMachineStorage storage = new 
SimpleStateMachineStorage();
 
   private final FileStore files;
@@ -120,6 +136,15 @@ public class FileStoreStateMachine extends 
BaseStateMachine {
         .build();
   }
 
+  void simulateFailure(String path) throws Exception {
+    final RaftServer.Division division = 
getServer().get().getDivision(getGroupId());
+    if (path.equals(SimulateFailure.PATH_TO_FAIL)
+        && division.getInfo().isFollower()
+        && SimulateFailure.chooseServer(division.getId())) {
+      throw new IOException(getId() + ": Simulated failure for path " + path);
+    }
+  }
+
   @Override
   public CompletableFuture<Integer> write(LogEntryProto entry, 
TransactionContext context) {
     final FileStoreRequestProto proto = getProto(context, entry);
@@ -128,6 +153,12 @@ public class FileStoreStateMachine extends 
BaseStateMachine {
     }
 
     final WriteRequestHeaderProto h = proto.getWriteHeader();
+    try {
+      simulateFailure(h.getPath().toStringUtf8());
+    } catch (Exception e) {
+      return FileStoreCommon.completeExceptionally(entry.getIndex(), getId() + 
": Failed simulateFailure", e);
+    }
+
     final CompletableFuture<Integer> f = files.write(entry.getIndex(),
         h.getPath().toStringUtf8(), h.getClose(),  h.getSync(), h.getOffset(),
         
entry.getStateMachineLogEntry().getStateMachineEntry().getStateMachineData());
diff --git 
a/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreBaseTest.java
 
b/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreBaseTest.java
index 07668e0f2..e363a0f70 100644
--- 
a/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreBaseTest.java
+++ 
b/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreBaseTest.java
@@ -47,6 +47,8 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import static 
org.apache.ratis.examples.filestore.FileStoreStateMachine.SimulateFailure.PATH_TO_FAIL;
+
 public abstract class FileStoreBaseTest<CLUSTER extends MiniRaftCluster>
     extends BaseTest
     implements MiniRaftCluster.Factory.Get<CLUSTER> {
@@ -144,6 +146,18 @@ public abstract class FileStoreBaseTest<CLUSTER extends 
MiniRaftCluster>
     cluster.shutdown();
   }
 
+  @Test
+  public void testWriteStateMachineDataFailure() throws Exception {
+    final CLUSTER cluster = newCluster(NUM_PEERS);
+    cluster.start();
+    RaftTestUtil.waitForLeader(cluster);
+
+    final CheckedSupplier<FileStoreClient, IOException> newClient = () -> 
newFileStoreClient(cluster);
+    testSingleFile(PATH_TO_FAIL, SizeInBytes.valueOf("100k"), newClient);
+    testSingleFile("bar", SizeInBytes.valueOf("2k"), newClient);
+    cluster.shutdown();
+  }
+
   private static FileStoreWriter writeSingleFile(
       String path, SizeInBytes fileLength, CheckedSupplier<FileStoreClient, 
IOException> newClient)
       throws Exception {
diff --git 
a/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreWriter.java
 
b/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreWriter.java
index c0f7d0809..480fe40d1 100644
--- 
a/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreWriter.java
+++ 
b/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreWriter.java
@@ -126,8 +126,6 @@ final class FileStoreWriter implements Closeable {
 
       final ByteBuffer b = randomBytes(length, r);
 
-      LOG.trace("write {}, offset={}, length={}, close? {}",
-          fileName, offset, length, close);
       final long written = client.write(fileName, offset, close, b, sync);
       Assertions.assertEquals(length, written);
       offset += length;
@@ -221,6 +219,8 @@ final class FileStoreWriter implements Closeable {
       verify(read, offset, n, expected);
       offset += n;
     }
+
+    LOG.info("Verify successful: {}", fileName);
     return this;
   }
 
diff --git 
a/ratis-grpc/src/main/java/org/apache/ratis/grpc/server/GrpcLogAppender.java 
b/ratis-grpc/src/main/java/org/apache/ratis/grpc/server/GrpcLogAppender.java
index 053cc5c0f..29867b544 100644
--- a/ratis-grpc/src/main/java/org/apache/ratis/grpc/server/GrpcLogAppender.java
+++ b/ratis-grpc/src/main/java/org/apache/ratis/grpc/server/GrpcLogAppender.java
@@ -129,18 +129,13 @@ public class GrpcLogAppender extends LogAppenderBase {
     }
   }
 
-  static class ReplyState {
+  class ReplyState {
     private boolean firstReplyReceived = false;
-    private int errorCount = 0;
 
     synchronized boolean isFirstReplyReceived() {
       return firstReplyReceived;
     }
 
-    synchronized int getErrorCount() {
-      return errorCount;
-    }
-
     int process(AppendResult result) {
       return process(result == AppendResult.INCONSISTENCY? 
Event.APPEND_ENTRIES_INCONSISTENCY_REPLY
           : Event.APPEND_ENTRIES_REPLY);
@@ -148,12 +143,7 @@ public class GrpcLogAppender extends LogAppenderBase {
 
     synchronized int process(Event event) {
       firstReplyReceived = event.updateFirstReplyReceived(firstReplyReceived);
-      if (event.isError()) {
-        errorCount++;
-      } else {
-        errorCount = 0;
-      }
-      return errorCount;
+      return getFollower().getErrorState().updateErrorCount(event.isError());
     }
   }
 
@@ -300,17 +290,25 @@ public class GrpcLogAppender extends LogAppenderBase {
   private void mayWait() {
     // use lastSend time instead of lastResponse time
     try {
-      getEventAwaitForSignal().await(getWaitTimeMs() + errorWaitTimeMs(),
-          TimeUnit.MILLISECONDS);
+      // sleepForErrors won't be waked up by signal
+      sleepForErrors();
+      // await can be waked up by signal
+      getEventAwaitForSignal().await(getWaitTimeMs(), TimeUnit.MILLISECONDS);
     } catch (InterruptedException ie) {
       Thread.currentThread().interrupt();
       LOG.warn("{} is interrupted: {}", this, ie.toString());
     }
   }
 
-  private long errorWaitTimeMs() {
-    return errorRetryWaitPolicy.handleAttemptFailure(replyState::getErrorCount)
-        .getSleepTime().toLong(TimeUnit.MILLISECONDS);
+  private void sleepForErrors() throws InterruptedException {
+    final int errorCount = 
getFollower().getErrorState().getErrorCountToDelay();
+    if (errorCount < 1) {
+      return;
+    }
+
+    final TimeDuration sleepTime = 
errorRetryWaitPolicy.handleAttemptFailure(() -> errorCount).getSleepTime();
+    LOG.debug("{}: sleepForErrors {}, errorCount={}", this, sleepTime, 
errorCount);
+    sleepTime.sleep();
   }
 
   @Override
diff --git 
a/ratis-server-api/src/main/java/org/apache/ratis/server/leader/FollowerInfo.java
 
b/ratis-server-api/src/main/java/org/apache/ratis/server/leader/FollowerInfo.java
index e56374512..7ab6d7fab 100644
--- 
a/ratis-server-api/src/main/java/org/apache/ratis/server/leader/FollowerInfo.java
+++ 
b/ratis-server-api/src/main/java/org/apache/ratis/server/leader/FollowerInfo.java
@@ -112,4 +112,34 @@ public interface FollowerInfo {
 
   /** Update lastRpcResponseTime and LastRespondedAppendEntriesSendTime */
   void updateLastRespondedAppendEntriesSendTime(Timestamp sendTime);
+
+  /** @return the error state. */
+  ErrorState getErrorState();
+
+  /** Error state such as the count for consecutive errors. */
+  interface ErrorState {
+    /**
+     * If it is an error, increment the count; otherwise, reset the count to 0.
+     *
+     * @return the updated error count.
+     */
+    int updateErrorCount(boolean isError);
+
+    /**
+     * Each error count is returned only once.
+     * For the subsequent calls of the same error count, it returns 0.
+     * <p>
+     * For example,
+     * 1. Error count is 3
+     * 2. Calling getErrorCountToDelay() returns 3
+     * 3. Calling getErrorCountToDelay() again returns 0
+     * 4. Calling updateErrorCount(true) to increment error count to 4
+     * 5. Calling getErrorCountToDelay() returns 4
+     * 6. Calling getErrorCountToDelay() again returns 0
+     * 7. Calling updateErrorCount(false) resets error count to 0
+     *
+     * @return each error count only once.
+     */
+    int getErrorCountToDelay();
+  }
 }
diff --git 
a/ratis-server/src/main/java/org/apache/ratis/server/impl/FollowerInfoImpl.java 
b/ratis-server/src/main/java/org/apache/ratis/server/impl/FollowerInfoImpl.java
index 7c34c1cb8..340a12b00 100644
--- 
a/ratis-server/src/main/java/org/apache/ratis/server/impl/FollowerInfoImpl.java
+++ 
b/ratis-server/src/main/java/org/apache/ratis/server/impl/FollowerInfoImpl.java
@@ -42,6 +42,7 @@ class FollowerInfoImpl implements FollowerInfo {
   private final RaftLogIndex matchIndex = new RaftLogIndex("matchIndex", 
RaftLog.INVALID_LOG_INDEX);
   private final RaftLogIndex commitIndex = new RaftLogIndex("commitIndex", 
RaftLog.INVALID_LOG_INDEX);
   private final RaftLogIndex snapshotIndex = new RaftLogIndex("snapshotIndex", 
0L);
+  private final ErrorState errorState = new ErrorStateImpl();
   private volatile boolean caughtUp;
   private volatile boolean ackInstallSnapshotAttempt = false;
 
@@ -240,4 +241,35 @@ class FollowerInfoImpl implements FollowerInfo {
   public void updateLastRespondedAppendEntriesSendTime(Timestamp sendTime) {
     lastRespondedAppendEntriesSendTime.set(sendTime);
   }
+
+  @Override
+  public ErrorState getErrorState() {
+    return errorState;
+  }
+
+  static class ErrorStateImpl implements ErrorState {
+    /** The number of consecutive errors without getting a successful reply 
for a particular follower. */
+    private int errorCount = 0;
+    private int lastReturnedErrorCount = 0;
+
+    @Override
+    public synchronized int updateErrorCount(boolean isError) {
+      if (isError) {
+        errorCount++;
+      } else {
+        errorCount = 0;
+        lastReturnedErrorCount = 0;
+      }
+      return errorCount;
+    }
+
+    @Override
+    public synchronized int getErrorCountToDelay() {
+      if (errorCount == lastReturnedErrorCount) {
+        return 0;
+      }
+      lastReturnedErrorCount = errorCount;
+      return errorCount;
+    }
+  }
 }

Reply via email to