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 0b0af3576 RATIS-2568. Add Streaming Read to FileStore (#1488)
0b0af3576 is described below

commit 0b0af35762f5da609d47f5094a7c667991d87e99
Author: Rui Wang <[email protected]>
AuthorDate: Thu Jul 9 03:10:47 2026 +0800

    RATIS-2568. Add Streaming Read to FileStore (#1488)
---
 .../apache/ratis/examples/filestore/FileInfo.java  |  36 ++++-
 .../apache/ratis/examples/filestore/FileStore.java |  21 ++-
 .../ratis/examples/filestore/FileStoreClient.java  |  48 +++++++
 .../examples/filestore/FileStoreStateMachine.java  |  19 +++
 .../ratis/examples/filestore/cli/FileStore.java    |   1 +
 .../apache/ratis/examples/filestore/cli/Read.java  | 158 +++++++++++++++++++++
 .../filestore/FileStoreStreamingBaseTest.java      |  36 +++++
 .../ratis/examples/filestore/FileStoreWriter.java  |  36 ++++-
 8 files changed, 345 insertions(+), 10 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 c3ec21f64..bdb099a60 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
@@ -19,11 +19,7 @@ package org.apache.ratis.examples.filestore;
 
 import org.apache.ratis.protocol.RaftPeerId;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
-import org.apache.ratis.util.CollectionUtils;
-import org.apache.ratis.util.JavaUtils;
-import org.apache.ratis.util.LogUtils;
-import org.apache.ratis.util.Preconditions;
-import org.apache.ratis.util.TaskQueue;
+import org.apache.ratis.util.*;
 import org.apache.ratis.util.function.CheckedFunction;
 import org.apache.ratis.util.function.CheckedSupplier;
 import org.slf4j.Logger;
@@ -31,7 +27,9 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
@@ -88,6 +86,28 @@ abstract class FileInfo {
     }
   }
 
+  void streamRead(CheckedFunction<Path, Path, IOException> resolver, long 
offset, long length,
+      WritableByteChannel stream) throws IOException {
+    if (offset + length > getWriteSize()) {
+      throw new IOException("Failed to read: offset (=" + offset
+          + " + length (=" + length + ") > size = " + getWriteSize()
+          + ", path=" + getRelativePath());
+    }
+
+    try (FileChannel in = FileUtils.newFileChannel(
+            resolver.apply(getRelativePath()), StandardOpenOption.READ)) {
+      long transferred = 0;
+      while (transferred < length) {
+        final long n = in.transferTo(offset + transferred, length - 
transferred, stream);
+        Preconditions.assertTrue(n >= 0);
+        transferred += n;
+      }
+      Preconditions.assertSame(length, transferred, "transferred");
+    } finally {
+      stream.close();
+    }
+  }
+
   UnderConstruction asUnderConstruction() {
     throw new UnsupportedOperationException(
         "File " + getRelativePath() + " is not under construction.");
@@ -121,6 +141,12 @@ abstract class FileInfo {
       this.writeSize = f.getWriteSize();
     }
 
+    ReadOnly(Path relativePath, long size) {
+      super(relativePath);
+      this.committedSize = size;
+      this.writeSize = size;
+    }
+
     @Override
     long getCommittedSize() {
       return committedSize;
diff --git 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStore.java
 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStore.java
index a930170ec..585660e1e 100644
--- 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStore.java
+++ 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/FileStore.java
@@ -43,6 +43,7 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -102,6 +103,11 @@ public class FileStore implements Closeable {
       }
     }
 
+    void putReadOnly(ReadOnly ro) {
+      LOG.trace("{}: putReadOnly {}", name, ro.getRelativePath());
+      map.put(ro.getRelativePath(), ro);
+    }
+
     ReadOnly close(UnderConstruction uc) {
       LOG.trace("{}: close {}", name, uc.getRelativePath());
       final ReadOnly ro = new ReadOnly(uc);
@@ -208,6 +214,12 @@ public class FileStore implements Closeable {
     return submit(task, reader);
   }
 
+  void streamRead(String relative, long offset, long length, 
WritableByteChannel stream)
+      throws IOException {
+    final FileInfo info = files.get(relative);
+    info.streamRead(this::resolve, offset, length, stream);
+  }
+
   CompletableFuture<Path> delete(long index, String relative) {
     final Supplier<String> name = () -> "delete(" + relative + ") @" + getId() 
+ ":" + index;
     final CheckedSupplier<Path, IOException> task = 
LogUtils.newCheckedSupplier(LOG, () -> {
@@ -284,13 +296,18 @@ public class FileStore implements Closeable {
 
   CompletableFuture<StreamWriteReplyProto> streamCommit(String p, long 
bytesWritten) {
     return CompletableFuture.supplyAsync(() -> {
+      final Path relative = normalize(p);
       final long len;
-      try (RandomAccessFile file = new 
RandomAccessFile(resolve(normalize(p)).toFile(), "r")) {
+      try (RandomAccessFile file = new 
RandomAccessFile(resolve(relative).toFile(), "r")) {
         len = file.length();
-        return StreamWriteReplyProto.newBuilder().setIsSuccess(len == 
bytesWritten).setByteWritten(len).build();
       } catch (IOException e) {
         throw new CompletionException("Failed to commit stream " + p + " with 
" + bytesWritten + " B.", e);
       }
+      final boolean success = len == bytesWritten;
+      if (success) {
+        files.putReadOnly(new ReadOnly(relative, len));
+      }
+      return 
StreamWriteReplyProto.newBuilder().setIsSuccess(success).setByteWritten(len).build();
     }, committer);
   }
 
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 fb0ee49dc..8eb6e3c17 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
@@ -18,6 +18,7 @@
 package org.apache.ratis.examples.filestore;
 
 import org.apache.ratis.client.RaftClient;
+import org.apache.ratis.client.api.DataStreamInput;
 import org.apache.ratis.client.api.DataStreamOutput;
 import org.apache.ratis.conf.RaftProperties;
 import org.apache.ratis.proto.ExamplesProtos.DeleteReplyProto;
@@ -29,6 +30,8 @@ import 
org.apache.ratis.proto.ExamplesProtos.StreamWriteRequestProto;
 import org.apache.ratis.proto.ExamplesProtos.WriteReplyProto;
 import org.apache.ratis.proto.ExamplesProtos.WriteRequestHeaderProto;
 import org.apache.ratis.proto.ExamplesProtos.WriteRequestProto;
+import org.apache.ratis.proto.RaftProtos.DataStreamPacketHeaderProto.Type;
+import org.apache.ratis.protocol.DataStreamReply;
 import org.apache.ratis.protocol.Message;
 import org.apache.ratis.protocol.RaftClientReply;
 import org.apache.ratis.protocol.RaftGroup;
@@ -39,6 +42,7 @@ import 
org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.apache.ratis.util.JavaUtils;
 import org.apache.ratis.util.Preconditions;
 import org.apache.ratis.util.ProtoUtils;
+import org.apache.ratis.util.ReferenceCountedObject;
 import org.apache.ratis.util.function.CheckedFunction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -46,6 +50,7 @@ import org.slf4j.LoggerFactory;
 import java.io.Closeable;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.function.Function;
@@ -197,6 +202,49 @@ public class FileStoreClient implements Closeable {
     return 
client.getDataStreamApi().stream(request.toByteString().asReadOnlyByteBuffer(), 
routingTable);
   }
 
+  public DataStreamInput getStreamInput(String path, long offset, long length) 
{
+    final ReadRequestProto read = ReadRequestProto.newBuilder()
+        .setPath(ProtoUtils.toByteString(path))
+        .setOffset(offset)
+        .setLength(length)
+        .build();
+    return 
client.getDataStreamApi().streamReadOnly(read.toByteString().asReadOnlyByteBuffer());
+  }
+
+  /**
+   * Read file data using streaming read and write it to the given channel.
+   *
+   * @return total number of bytes read.
+   */
+  public long streamRead(String path, long offset, long length, 
WritableByteChannel channel)
+      throws IOException {
+    long total = 0;
+    try (DataStreamInput in = getStreamInput(path, offset, length)) {
+      while (true) {
+        final ReferenceCountedObject<DataStreamReply> ref = 
in.readAsync().join();
+        try {
+          final DataStreamReply reply = ref.get();
+          if (reply.getType() == Type.STREAM_HEADER) {
+            Preconditions.assertTrue(reply.isSuccess(),
+                () -> "Failed to stream read " + path + ", reply=" + reply);
+            return total;
+          } else {
+            Preconditions.assertTrue(reply.isSuccess(),
+                    () -> "Failed to stream read " + path + ", reply=" + 
reply);
+            Preconditions.assertEquals(Type.STREAM_DATA, reply.getType(),
+                    "reply type for stream read " + path);
+            final ByteBuffer data = reply.nioBuffer();
+            while (data.hasRemaining()) {
+              total += channel.write(data);
+            }
+          }
+        } finally {
+          ref.release();
+        }
+      }
+    }
+  }
+
   public CompletableFuture<Long> writeAsync(String path, long offset, boolean 
close, ByteBuffer buffer, boolean sync) {
     return writeImpl(this::sendAsync, path, offset, close, buffer, sync
     ).thenApply(reply -> JavaUtils.supplyAndWrapAsCompletionException(
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 d9a1463b9..4ff83369a 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
@@ -43,6 +43,7 @@ import 
org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferExce
 import org.apache.ratis.util.FileUtils;
 
 import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
 import java.nio.file.Path;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicReference;
@@ -106,6 +107,24 @@ public class FileStoreStateMachine extends 
BaseStateMachine {
         .thenApply(reply -> Message.valueOf(reply.toByteString()));
   }
 
+  @Override
+  public void query(Message request, WritableByteChannel stream) {
+    try {
+      final ReadRequestProto proto = 
ReadRequestProto.parseFrom(request.getContent());
+      if (proto.getIsWatch()) {
+        throw new IOException("Watch is not supported for streaming read: " + 
proto);
+      }
+      files.streamRead(proto.getPath().toStringUtf8(), proto.getOffset(), 
proto.getLength(), stream);
+    } catch (Exception e) {
+      LOG.error(getId() + ": Failed streaming read for " + request, e);
+      try {
+        stream.close();
+      } catch (IOException ignored) {
+        // ignore
+      }
+    }
+  }
+
   @Override
   public TransactionContext startTransaction(RaftClientRequest request) throws 
IOException {
     final ByteString content = request.getMessage().getContent();
diff --git 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/FileStore.java
 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/FileStore.java
index 9d50e3421..c62d5a10c 100644
--- 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/FileStore.java
+++ 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/FileStore.java
@@ -35,6 +35,7 @@ public final class FileStore {
     commands.add(new Server());
     commands.add(new LoadGen());
     commands.add(new DataStream());
+    commands.add(new Read());
     return commands;
   }
 }
diff --git 
a/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/Read.java
 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/Read.java
new file mode 100644
index 000000000..128a9abba
--- /dev/null
+++ 
b/ratis-examples/src/main/java/org/apache/ratis/examples/filestore/cli/Read.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.examples.filestore.cli;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import org.apache.ratis.RaftConfigKeys;
+import org.apache.ratis.client.RaftClient;
+import org.apache.ratis.client.RaftClientConfigKeys;
+import org.apache.ratis.conf.RaftProperties;
+import org.apache.ratis.datastream.SupportedDataStreamType;
+import org.apache.ratis.examples.common.SubCommandBase;
+import org.apache.ratis.examples.filestore.FileStoreClient;
+import org.apache.ratis.grpc.GrpcConfigKeys;
+import org.apache.ratis.grpc.GrpcFactory;
+import org.apache.ratis.protocol.ClientId;
+import org.apache.ratis.protocol.RaftGroup;
+import org.apache.ratis.protocol.RaftGroupId;
+import org.apache.ratis.rpc.SupportedRpcType;
+import org.apache.ratis.server.RaftServerConfigKeys;
+import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+import org.apache.ratis.util.FileUtils;
+import org.apache.ratis.util.SizeInBytes;
+import org.apache.ratis.util.TimeDuration;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Subcommand to read files from FileStore using streaming read.
+ */
+@Parameters(commandDescription = "Read files from FileStore using streaming 
read")
+public class Read extends SubCommandBase {
+
+  @Parameter(names = {"--files"}, description = "Comma-separated file names on 
the server", required = true)
+  private String files;
+
+  @Parameter(names = {"--size"}, description = "Number of bytes to read per 
file", required = true)
+  private long fileSizeInBytes;
+
+  @Parameter(names = {"--offset"}, description = "Offset to start reading 
from", required = false)
+  private long offset;
+
+  @Override
+  public void run() throws Exception {
+    final List<String> fileNames = Arrays.asList(files.split(","));
+    if (fileNames.isEmpty()) {
+      throw new IllegalArgumentException("No files specified in --files");
+    }
+
+    final RaftProperties raftProperties = newRaftProperties();
+
+    System.out.println("Starting streaming read now");
+
+    final long startTime = System.currentTimeMillis();
+    long totalReadBytes = 0;
+    final List<ReadResult> results = new ArrayList<>();
+    try (FileStoreClient client = newClient(raftProperties)) {
+      for (String fileName : fileNames) {
+        final String name = fileName.trim();
+        final ReadResult result = readFile(client, name);
+        results.add(result);
+        if (result.bytesRead != fileSizeInBytes) {
+          System.err.println("Error: file:" + name + " read:" + 
result.bytesRead
+              + " mismatch expected size:" + fileSizeInBytes);
+        }
+        totalReadBytes += result.bytesRead;
+      }
+    }
+
+    final long endTime = System.currentTimeMillis();
+    System.out.println("Total files read: " + fileNames.size());
+    for (ReadResult result : results) {
+      System.out.println("  " + result.fileName + " -> " + result.tempFile
+          + " (" + result.bytesRead + " bytes)");
+    }
+    System.out.println("Each file size: " + fileSizeInBytes);
+    System.out.println("Total data read: " + totalReadBytes + " bytes");
+    System.out.println("Total time taken: " + (endTime - startTime) + " 
millis");
+  }
+
+  private ReadResult readFile(FileStoreClient client, String fileName) throws 
IOException {
+    final File tempFile = Files.createTempFile("filestore-read-", "-" + 
fileName).toFile();
+    try (FileChannel out = FileUtils.newFileChannel(tempFile,
+        StandardOpenOption.CREATE, StandardOpenOption.WRITE, 
StandardOpenOption.TRUNCATE_EXISTING)) {
+      final long bytesRead = client.streamRead(fileName, offset, 
fileSizeInBytes, out);
+      return new ReadResult(fileName, tempFile.getAbsolutePath(), bytesRead);
+    } finally {
+      FileUtils.deleteFully(tempFile);
+    }
+  }
+
+  private static final class ReadResult {
+    private final String fileName;
+    private final String tempFile;
+    private final long bytesRead;
+
+    private ReadResult(String fileName, String tempFile, long bytesRead) {
+      this.fileName = fileName;
+      this.tempFile = tempFile;
+      this.bytesRead = bytesRead;
+    }
+  }
+
+  private static RaftProperties newRaftProperties() {
+    final int raftSegmentPreallocatedSize = 1024 * 1024 * 1024;
+    final RaftProperties raftProperties = new RaftProperties();
+    RaftConfigKeys.Rpc.setType(raftProperties, SupportedRpcType.GRPC);
+    GrpcConfigKeys.setMessageSizeMax(raftProperties, 
SizeInBytes.valueOf(raftSegmentPreallocatedSize));
+    RaftServerConfigKeys.Log.Appender.setBufferByteLimit(raftProperties,
+        SizeInBytes.valueOf(raftSegmentPreallocatedSize));
+    RaftServerConfigKeys.Log.setWriteBufferSize(raftProperties,
+        SizeInBytes.valueOf(raftSegmentPreallocatedSize));
+    RaftServerConfigKeys.Log.setPreallocatedSize(raftProperties,
+        SizeInBytes.valueOf(raftSegmentPreallocatedSize));
+    RaftServerConfigKeys.Log.setSegmentSizeMax(raftProperties, 
SizeInBytes.valueOf(raftSegmentPreallocatedSize));
+    RaftConfigKeys.DataStream.setType(raftProperties, 
SupportedDataStreamType.NETTY);
+    RaftServerConfigKeys.Log.setSegmentCacheNumMax(raftProperties, 2);
+    RaftClientConfigKeys.Rpc.setRequestTimeout(raftProperties,
+        TimeDuration.valueOf(50000, TimeUnit.MILLISECONDS));
+    return raftProperties;
+  }
+
+  private FileStoreClient newClient(RaftProperties raftProperties) throws 
IOException {
+    final RaftGroup raftGroup = RaftGroup.valueOf(
+        RaftGroupId.valueOf(ByteString.copyFromUtf8(getRaftGroupId())), 
getPeers());
+    final RaftClient client = RaftClient.newBuilder()
+        .setProperties(raftProperties)
+        .setRaftGroup(raftGroup)
+        .setClientRpc(new GrpcFactory(new org.apache.ratis.conf.Parameters())
+            .newRaftClientRpc(ClientId.randomId(), raftProperties))
+        .setPrimaryDataStreamServer(getPeers()[0])
+        .build();
+    return new FileStoreClient(client);
+  }
+}
diff --git 
a/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreStreamingBaseTest.java
 
b/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreStreamingBaseTest.java
index cdcee0ef0..d70efaaa9 100644
--- 
a/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreStreamingBaseTest.java
+++ 
b/ratis-examples/src/test/java/org/apache/ratis/examples/filestore/FileStoreStreamingBaseTest.java
@@ -104,6 +104,42 @@ public abstract class FileStoreStreamingBaseTest <CLUSTER 
extends MiniRaftCluste
     cluster.shutdown();
   }
 
+  @Test
+  public void testFileStoreStreamReadAfterStreamWrite() throws Exception {
+    final CLUSTER cluster = newCluster(NUM_PEERS);
+    cluster.start();
+    RaftTestUtil.waitForLeader(cluster);
+
+    final RaftGroup raftGroup = cluster.getGroup();
+    final Collection<RaftPeer> peers = raftGroup.getPeers();
+    final RaftPeer primary = cluster.getLeader().getPeer();
+
+    final CheckedSupplier<FileStoreClient, IOException> newClient =
+            () -> new FileStoreClient(cluster.getGroup(), getProperties(), 
primary);
+
+    final RoutingTable routingTable = 
DataStreamTestUtils.getRoutingTableChainTopology(peers, primary);
+    testSingleFileReadAfterStreamWrite("foo", SizeInBytes.valueOf("2M"), 
10_000, newClient, routingTable);
+
+    cluster.shutdown();
+  }
+
+  private static void testSingleFileReadAfterStreamWrite(
+      String path, SizeInBytes fileLength, int bufferSize,
+      CheckedSupplier<FileStoreClient, IOException> newClient, RoutingTable 
routingTable)
+      throws Exception {
+    LOG.info("testSingleFileAfterStreamWrite with path={}, fileLength={}", 
path, fileLength);
+    FileStoreWriter.newBuilder()
+        .setFileName(path)
+        .setFileSize(fileLength)
+        .setBufferSize(bufferSize)
+        .setFileStoreClientSupplier(newClient)
+        .build()
+        .streamWrite(routingTable)
+        .streamRead()
+        .delete()
+        .close();
+  }
+
   private void testSingleFile(
       String path, SizeInBytes fileLength, int bufferSize, 
CheckedSupplier<FileStoreClient, IOException> newClient,
       RoutingTable routingTable)
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 480fe40d1..18f8f89ab 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
@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
 import java.io.Closeable;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -133,7 +134,7 @@ final class FileStoreWriter implements Closeable {
     return this;
   }
 
-  public FileStoreWriter streamWriteAndVerify(RoutingTable routingTable) {
+  public FileStoreWriter streamWrite(RoutingTable routingTable) {
     final int size = fileSize.getSizeInt();
     final DataStreamOutput dataStreamOutput = client.getStreamOutput(fileName, 
size, routingTable);
     final List<CompletableFuture<DataStreamReply>> futures = new ArrayList<>();
@@ -156,8 +157,6 @@ final class FileStoreWriter implements Closeable {
     DataStreamReply reply = dataStreamOutput.closeAsync().join();
     Assertions.assertTrue(reply.isSuccess());
 
-    // TODO: handle when any of the writeAsync has failed.
-    // check writeAsync requests
     for (int i = 0; i < futures.size(); i++) {
       reply = futures.get(i).join();
       Assertions.assertTrue(reply.isSuccess());
@@ -168,6 +167,37 @@ final class FileStoreWriter implements Closeable {
     return this;
   }
 
+  public FileStoreWriter streamWriteAndVerify(RoutingTable routingTable) {
+    return streamWrite(routingTable);
+  }
+
+  FileStoreWriter streamRead() throws IOException {
+    final long expected = fileSize.getSizeInt();
+    final long bytesRead = client.streamRead(fileName, 0, expected, DISCARD);
+    Assertions.assertEquals(expected, bytesRead,
+        () -> "stream read " + fileName + ": expected " + expected + " bytes");
+    LOG.info("Stream read successful: {} bytes from {}", bytesRead, fileName);
+    return this;
+  }
+
+  private static final WritableByteChannel DISCARD = new WritableByteChannel() 
{
+    @Override
+    public int write(ByteBuffer src) {
+      final int n = src.remaining();
+      src.position(src.limit());
+      return n;
+    }
+
+    @Override
+    public boolean isOpen() {
+      return true;
+    }
+
+    @Override
+    public void close() {
+    }
+  };
+
   CompletableFuture<FileStoreWriter> writeAsync(boolean sync) {
     Objects.requireNonNull(asyncExecutor, "asyncExecutor == null");
     final Random r = new Random(seed);

Reply via email to