This is an automated email from the ASF dual-hosted git repository.

amaliujia pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 3a22acfaa21 HDDS-16746. Local Stream onCommand should drain buffers 
before commit PutBlock (#11427)
3a22acfaa21 is described below

commit 3a22acfaa214ed7fcdd25ba193bf7088e8e2bd87
Author: Rui Wang <[email protected]>
AuthorDate: Thu Oct 8 21:35:55 2026 +0800

    HDDS-16746. Local Stream onCommand should drain buffers before commit 
PutBlock (#11427)
---
 .../common/transport/server/ratis/LocalStream.java |  6 +-
 .../ozone/container/keyvalue/impl/Buffers.java     |  5 ++
 .../keyvalue/impl/KeyValueStreamDataChannel.java   | 19 ++++++
 .../server/ratis/ContainerStateMachineTests.java   | 78 ++++++++++++++++++++++
 .../impl/TestKeyValueStreamDataChannel.java        | 34 ++++++++++
 .../rpc/TestContainerStateMachineStream.java       | 23 +++++++
 6 files changed, 164 insertions(+), 1 deletion(-)

diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/LocalStream.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/LocalStream.java
index 47189031a11..b2817e8bd3f 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/LocalStream.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/LocalStream.java
@@ -58,9 +58,13 @@ public CompletableFuture<?> cleanUp() {
 
   @Override
   public CompletableFuture<ByteBuffer> onCommand(ByteBuffer buffer, long 
streamOffset) {
+    if (!(dataChannel instanceof KeyValueStreamDataChannel)) {
+      return JavaUtils.completeExceptionally(new IllegalStateException(
+          "Unexpected DataChannel " + dataChannel.getClass()));
+    }
     return CompletableFuture.supplyAsync(() -> {
       try {
-        // TODO: drain datachannel.buffers
+        ((KeyValueStreamDataChannel) dataChannel).drainBuffers();
         return command.apply(buffer);
       } catch (IOException e) {
         throw new CompletionException(e);
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/Buffers.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/Buffers.java
index 308c8bb13a0..9a65f96d393 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/Buffers.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/Buffers.java
@@ -98,6 +98,7 @@ ReferenceCountedObject<ByteBuf> pollAll() {
       refs.add(ref);
       array[i] = ref.get();
     }
+    length = 0;
     final ByteBuf buf = Unpooled.wrappedBuffer(array).asReadOnly();
     return ReferenceCountedObject.wrap(buf, () -> { }, () -> {
       buf.release();
@@ -105,6 +106,10 @@ ReferenceCountedObject<ByteBuf> pollAll() {
     });
   }
 
+  boolean isEmpty() {
+    return deque.isEmpty();
+  }
+
   void cleanUpAll() {
     while (!deque.isEmpty()) {
       poll().release();
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/KeyValueStreamDataChannel.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/KeyValueStreamDataChannel.java
index 5a735812d79..190c0bd164c 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/KeyValueStreamDataChannel.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/impl/KeyValueStreamDataChannel.java
@@ -152,6 +152,25 @@ private void writeBuffers() throws IOException {
     }
   }
 
+  /**
+   * Write all the data in {@link #buffers} to the channel.
+   * It must be called before committing a PutBlock in the middle of the 
stream;
+   * otherwise, the buffered data is not yet in the block file.
+   */
+  public void drainBuffers() throws IOException {
+    assertOpen();
+    if (buffers.isEmpty()) {
+      return;
+    }
+    final ReferenceCountedObject<ByteBuf> ref = buffers.pollAll();
+    final ByteBuf buf = ref.retain();
+    try {
+      writeFully(buf.nioBuffer(), super::writeFileChannel);
+    } finally {
+      ref.release();
+    }
+  }
+
   static ContainerCommandRequestProto closeBuffers(
       Buffers buffers, WriteMethod writeMethod) throws IOException {
     final ReferenceCountedObject<ByteBuf> ref = buffers.pollAll();
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachineTests.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachineTests.java
index 8ad92ec358e..57cbae9b3c4 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachineTests.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachineTests.java
@@ -22,9 +22,14 @@
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -32,6 +37,7 @@
 
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
@@ -45,17 +51,21 @@
 import java.util.stream.IntStream;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.ratis.ContainerCommandRequestMessage;
 import 
org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
 import org.apache.hadoop.ozone.container.common.interfaces.ContainerDispatcher;
+import 
org.apache.hadoop.ozone.container.keyvalue.impl.KeyValueStreamDataChannel;
 import org.apache.hadoop.ozone.container.ozoneimpl.ContainerController;
 import org.apache.ozone.test.tag.Flaky;
 import org.apache.ratis.proto.RaftProtos;
 import org.apache.ratis.protocol.Message;
+import org.apache.ratis.protocol.RaftClientRequest;
 import org.apache.ratis.protocol.RaftGroup;
 import org.apache.ratis.protocol.RaftGroupId;
 import org.apache.ratis.protocol.RaftPeer;
 import org.apache.ratis.server.DivisionInfo;
 import org.apache.ratis.server.RaftServer;
+import org.apache.ratis.statemachine.StateMachine;
 import org.apache.ratis.statemachine.TransactionContext;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.junit.jupiter.api.AfterAll;
@@ -65,6 +75,7 @@
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InOrder;
 
 /**
  * Test class to ContainerStateMachine class.
@@ -247,6 +258,73 @@ public void testWriteTimout() throws Exception {
     assertEquals(ContainerProtos.Result.CONTAINER_INTERNAL_ERROR, 
sce.getResult());
   }
 
+  @Test
+  public void testStreamCommandDrainsBuffersBeforePutBlock() throws Exception {
+    final KeyValueStreamDataChannel channel = 
mock(KeyValueStreamDataChannel.class);
+    final StateMachine.DataStream stream = setUpStream(channel);
+
+    stream.onCommand(newPutBlockCommand(), 0).get();
+
+    final InOrder inOrder = inOrder(channel, dispatcher);
+    inOrder.verify(channel).drainBuffers();
+    inOrder.verify(dispatcher).dispatch(argThat(r -> r.getCmdType() == 
ContainerProtos.Type.PutBlock),
+        argThat(c -> DispatcherContext.op(c) == 
DispatcherContext.Op.STREAM_COMMAND));
+  }
+
+  @Test
+  public void testStreamCommandNotDispatchedIfDrainFails() throws Exception {
+    final KeyValueStreamDataChannel channel = 
mock(KeyValueStreamDataChannel.class);
+    doThrow(new IOException("drain failed")).when(channel).drainBuffers();
+    final StateMachine.DataStream stream = setUpStream(channel);
+
+    final ExecutionException e = assertThrows(ExecutionException.class,
+        () -> stream.onCommand(newPutBlockCommand(), 0).get());
+    assertInstanceOf(IOException.class, e.getCause());
+    verify(dispatcher, never()).dispatch(argThat(r -> r.getCmdType() == 
ContainerProtos.Type.PutBlock), any());
+  }
+
+  @Test
+  public void testStreamCommandWithUnexpectedDataChannel() throws Exception {
+    final StateMachine.DataStream stream = 
setUpStream(mock(StateMachine.DataChannel.class));
+
+    final ExecutionException e = assertThrows(ExecutionException.class,
+        () -> stream.onCommand(newPutBlockCommand(), 0).get());
+    assertInstanceOf(IllegalStateException.class, e.getCause());
+    verify(dispatcher, never()).dispatch(argThat(r -> r.getCmdType() == 
ContainerProtos.Type.PutBlock), any());
+  }
+
+  private StateMachine.DataStream setUpStream(StateMachine.DataChannel 
channel) throws Exception {
+    when(dispatcher.dispatch(any(), 
any())).thenReturn(ContainerProtos.ContainerCommandResponseProto.newBuilder()
+        
.setCmdType(ContainerProtos.Type.PutBlock).setResult(ContainerProtos.Result.SUCCESS).build());
+    when(dispatcher.getStreamDataChannel(any(), any())).thenReturn(channel);
+    final ContainerProtos.ContainerCommandRequestProto streamInit = 
ContainerProtos.ContainerCommandRequestProto
+        .newBuilder()
+        .setCmdType(ContainerProtos.Type.StreamInit)
+        
.setWriteChunk(ContainerProtos.WriteChunkRequestProto.newBuilder().setBlockID(newBlockID()).build())
+        .setContainerID(1)
+        .setDatanodeUuid(UUID.randomUUID().toString())
+        .build();
+    final RaftClientRequest request = mock(RaftClientRequest.class);
+    
when(request.getMessage()).thenReturn(ContainerCommandRequestMessage.toMessage(streamInit,
 null));
+    return stateMachine.stream(request).get();
+  }
+
+  private static ByteBuffer newPutBlockCommand() {
+    final ContainerProtos.ContainerCommandRequestProto putBlock = 
ContainerProtos.ContainerCommandRequestProto
+        .newBuilder()
+        .setCmdType(ContainerProtos.Type.PutBlock)
+        .setPutBlock(ContainerProtos.PutBlockRequestProto.newBuilder()
+            
.setBlockData(ContainerProtos.BlockData.newBuilder().setBlockID(newBlockID()).build()))
+        .setContainerID(1)
+        .setDatanodeUuid(UUID.randomUUID().toString())
+        .build();
+    return ContainerCommandRequestMessage.toMessage(putBlock, 
null).getContent().asReadOnlyByteBuffer();
+  }
+
+  private static ContainerProtos.DatanodeBlockID newBlockID() {
+    return 
ContainerProtos.DatanodeBlockID.newBuilder().setContainerID(1).setLocalID(1).build();
+  }
+
   private void setUpMockDispatcherReturn(boolean failWithException) {
     if (failWithException) {
       when(dispatcher.dispatch(any(), any())).thenThrow(new 
RuntimeException());
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/impl/TestKeyValueStreamDataChannel.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/impl/TestKeyValueStreamDataChannel.java
index 638c5707725..59ce314d632 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/impl/TestKeyValueStreamDataChannel.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/impl/TestKeyValueStreamDataChannel.java
@@ -131,6 +131,40 @@ public void testClosePutBlockBehavior(boolean 
commitPutBlockOnClose) throws Exce
     assertArrayEquals(data, Files.readAllBytes(tempFile.toPath()));
   }
 
+  @Test
+  public void testDrainBuffers() throws Exception {
+    File tempFile = File.createTempFile("test-kv-drain", ".tmp");
+    tempFile.deleteOnExit();
+    AtomicReference<ContainerCommandRequestProto> processed = new 
AtomicReference<>();
+    KeyValueStreamDataChannel channel = newChannel(tempFile, true, processed);
+    final byte[] data1 = RandomUtils.secure().randomBytes(50);
+    final byte[] data2 = RandomUtils.secure().randomBytes(30);
+    final ByteBuffer putBlockBuf = ContainerCommandRequestMessage.toMessage(
+        PUT_BLOCK_PROTO, null).getContent().asReadOnlyByteBuffer();
+    final ByteBuffer protoLengthBuf = getProtoLength(putBlockBuf, 
PUT_BLOCK_REQUEST_LENGTH_MAX);
+
+    assertEquals(0, tempFile.length());
+    write(channel, data1);
+    // data1 is smaller than PUT_BLOCK_REQUEST_LENGTH_MAX, so it is still 
buffered
+    assertEquals(0, tempFile.length());
+    channel.drainBuffers();
+    assertEquals(data1.length, tempFile.length());
+    // draining empty buffers is a no-op
+    channel.drainBuffers();
+    assertEquals(data1.length, tempFile.length());
+
+    write(channel, data2);
+    write(channel, putBlockBuf.duplicate());
+    write(channel, protoLengthBuf.duplicate());
+    channel.close();
+
+    assertEquals(PUT_BLOCK_PROTO, processed.get());
+    final byte[] expected = new byte[data1.length + data2.length];
+    System.arraycopy(data1, 0, expected, 0, data1.length);
+    System.arraycopy(data2, 0, expected, data1.length, data2.length);
+    assertArrayEquals(expected, Files.readAllBytes(tempFile.toPath()));
+  }
+
   @Test
   public void testReadPutBlockRequestBufferTooShort() {
     final ByteBuf buf = Unpooled.buffer(2);
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestContainerStateMachineStream.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestContainerStateMachineStream.java
index c10dfe784fb..060ef7ce8c7 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestContainerStateMachineStream.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestContainerStateMachineStream.java
@@ -20,6 +20,7 @@
 import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CHUNK_SIZE_KEY;
 import static 
org.apache.hadoop.ozone.container.OzoneTestHelper.createStreamKey;
 import static 
org.apache.hadoop.ozone.container.OzoneTestHelper.getDatanodeService;
+import static org.apache.hadoop.ozone.container.OzoneTestHelper.validateData;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 
@@ -42,6 +43,7 @@
 import org.apache.ozone.test.NonHATests;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -119,4 +121,25 @@ void testContainerStateMachineForStreaming(int offset, 
boolean putBlockWithoutRa
         .isGreaterThanOrEqualTo(size);
   }
 
+  @Test
+  void testReadAfterHsyncWithPutBlockWithoutRaft() throws Exception {
+    // Smaller than PUT_BLOCK_REQUEST_LENGTH_MAX, so the datanodes still 
buffer all the data when hsync commits PutBlock
+    final int size = 1000;
+    OzoneConfiguration conf = new OzoneConfiguration(cluster().getConf());
+    OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
+    clientConfig.setDatastreamPutBlockWithoutRaftEnabled(true);
+    conf.setFromObject(clientConfig);
+
+    final String keyName = "hsync-" + UUID.randomUUID();
+    try (OzoneClient streamingClient = OzoneClientFactory.getRpcClient(conf);
+         OzoneDataStreamOutput key = createStreamKey(keyName, 
ReplicationType.RATIS, size,
+             streamingClient.getObjectStore(), volumeName, bucketName)) {
+      final byte[] data = ContainerTestHelper.generateData(size, true);
+      key.write(ByteBuffer.wrap(data));
+      assertInstanceOf(KeyDataStreamOutput.class, 
key.getByteBufStreamOutput()).hsync();
+
+      validateData(keyName, data, client.getObjectStore(), volumeName, 
bucketName);
+    }
+  }
+
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to