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 7d9e8417e83 HDDS-16008. PutBlocks from Flushes also go without Raft
(#11356)
7d9e8417e83 is described below
commit 7d9e8417e83adad42356f08c4ca5d5173adfa003
Author: Rui Wang <[email protected]>
AuthorDate: Tue Oct 6 17:22:11 2026 +0800
HDDS-16008. PutBlocks from Flushes also go without Raft (#11356)
Co-authored-by: Peter Lee <[email protected]>
---
.../apache/hadoop/hdds/scm/OzoneClientConfig.java | 19 +++---
.../hdds/scm/storage/BlockDataStreamOutput.java | 77 ++++++++++++++++++++--
.../hadoop/hdds/scm/TestOzoneClientConfig.java | 10 +--
.../hdds/scm/storage/MockDatanodePipeline.java | 45 ++++++++++++-
.../scm/storage/TestBlockDataStreamOutput.java | 59 ++++++++++++++++-
.../server/ratis/ContainerStateMachine.java | 31 ++++++++-
.../transport/server/ratis/DispatcherContext.java | 4 +-
.../common/transport/server/ratis/LocalStream.java | 22 ++++++-
.../container/common/impl/TestHddsDispatcher.java | 3 +-
.../client/rpc/TestBlockDataStreamOutput.java | 42 +++++++-----
.../rpc/TestContainerStateMachineStream.java | 10 +--
11 files changed, 270 insertions(+), 52 deletions(-)
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
index 493d11f37be..e88e6293907 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/OzoneClientConfig.java
@@ -317,13 +317,16 @@ public class OzoneClientConfig {
tags = ConfigTag.CLIENT)
private boolean enablePutblockPiggybacking = false;
- @Config(key = "ozone.client.datastream.putblock.on.close.enabled",
+ @Config(key = "ozone.client.datastream.putblock.without.raft.enabled",
defaultValue = "false",
type = ConfigType.BOOLEAN,
- description = "When enabled, use StreamInitWithPutBlock so datanodes
commit PutBlock " +
- "when the Ratis data stream closes instead of via a separate
WriteAsync PutBlock.",
+ description = "When enabled, the PutBlock of a Ratis data stream is
committed through the data stream " +
+ "instead of a separate WriteAsync PutBlock, so that it does not go
through the Raft log: " +
+ "StreamInitWithPutBlock is used so that datanodes commit PutBlock
when the data stream closes, " +
+ "and the PutBlock triggered by a flush in the middle of the stream
is sent as a data stream command. " +
+ "All the datanodes must support committing PutBlock through the data
stream.",
tags = ConfigTag.CLIENT)
- private boolean datastreamPutBlockOnCloseEnabled = false;
+ private boolean datastreamPutBlockWithoutRaftEnabled = false;
@Config(key = "ozone.client.key.write.concurrency",
defaultValue = "1",
@@ -708,12 +711,12 @@ public void setStreamReadTimeout(Duration
streamReadTimeout) {
this.streamReadTimeout = streamReadTimeout;
}
- public boolean isDatastreamPutBlockOnCloseEnabled() {
- return datastreamPutBlockOnCloseEnabled;
+ public boolean isDatastreamPutBlockWithoutRaftEnabled() {
+ return datastreamPutBlockWithoutRaftEnabled;
}
- public void setDatastreamPutBlockOnCloseEnabled(boolean
datastreamPutBlockOnCloseEnabled) {
- this.datastreamPutBlockOnCloseEnabled = datastreamPutBlockOnCloseEnabled;
+ public void setDatastreamPutBlockWithoutRaftEnabled(boolean
datastreamPutBlockWithoutRaftEnabled) {
+ this.datastreamPutBlockWithoutRaftEnabled =
datastreamPutBlockWithoutRaftEnabled;
}
/**
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
index ae07ee09ca5..e8205859395 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockDataStreamOutput.java
@@ -120,9 +120,14 @@ public class BlockDataStreamOutput implements
ByteBufferStreamOutput {
// be released from the buffer pool.
private final StreamCommitWatcher commitWatcher;
+ // Futures of the PutBlock(s) sent through the Ratis write API, i.e.
committed by the Raft log.
private Queue<CompletableFuture<ContainerCommandResponseProto>>
putBlockFutures = new LinkedList<>();
+ // Futures of the PutBlock(s) sent as data stream commands, i.e. committed
without the Raft log.
+ private final Queue<CompletableFuture<DataStreamReply>>
+ putBlockCommandFutures = new LinkedList<>();
+
private final List<DatanodeDetails> failedServers;
private final Checksum checksum;
@@ -211,7 +216,7 @@ private DataStreamOutput setupStream(Pipeline pipeline)
throws IOException {
// TODO: The datanode UUID is not used meaningfully, consider deprecating
// it or remove it completely if possible
String id = pipeline.getFirstNode().getUuidString();
- ContainerProtos.Type streamInitType =
config.isDatastreamPutBlockOnCloseEnabled()
+ ContainerProtos.Type streamInitType =
config.isDatastreamPutBlockWithoutRaftEnabled()
? ContainerProtos.Type.StreamInitWithPutBlock
: ContainerProtos.Type.StreamInit;
ContainerProtos.ContainerCommandRequestProto.Builder builder =
@@ -322,6 +327,9 @@ private void doFlushIfNeeded() throws IOException {
if (!putBlockFutures.isEmpty()) {
putBlockFutures.remove().get();
}
+ if (!putBlockCommandFutures.isEmpty()) {
+ putBlockCommandFutures.remove().get();
+ }
} catch (ExecutionException e) {
handleExecutionException(e);
} catch (InterruptedException ex) {
@@ -419,9 +427,9 @@ public void executePutBlock(boolean close,
byteBufferList = null;
}
waitFuturesComplete();
- if (close && config.isDatastreamPutBlockOnCloseEnabled()) {
+ if (close && config.isDatastreamPutBlockWithoutRaftEnabled()) {
// Wait for boundary PutBlock(s) before appending the stream-close
PutBlock.
- waitPutBlockFuturesComplete();
+ waitPutBlockCommandFuturesComplete();
}
final BlockData blockData = containerBlockData.build();
if (close) {
@@ -444,11 +452,14 @@ public void executePutBlock(boolean close,
}
}
});
- if (config.isDatastreamPutBlockOnCloseEnabled()) {
+ if (config.isDatastreamPutBlockWithoutRaftEnabled()) {
// PutBlock is supposed to be committed after the data stream close so
there
// is no need to continue.
return;
}
+ } else if (config.isDatastreamPutBlockWithoutRaftEnabled()) {
+ executePutBlockCommand(blockData, byteBufferList);
+ return;
}
try {
XceiverClientReply asyncReply =
@@ -498,6 +509,57 @@ public void executePutBlock(boolean close,
}
}
+ /**
+ * Commit a PutBlock in the middle of the stream by sending it as a data
stream command,
+ * so that it does not go through the Raft log. The command is ordered with
the data written
+ * so far and the datanodes reply with the serialized {@link
ContainerCommandResponseProto}.
+ *
+ * @param blockData the block metadata to commit
+ * @param byteBufferList the buffers covered by this PutBlock, to be
released once it is acknowledged
+ */
+ private void executePutBlockCommand(BlockData blockData,
+ List<StreamBuffer> byteBufferList) throws IOException {
+ final ContainerCommandRequestProto putBlockRequest
+ = ContainerProtocolCalls.getPutBlockRequest(
+ xceiverClient.getPipeline(), blockData, false, tokenString);
+ final ByteBuffer command = ContainerCommandRequestMessage.toMessage(
+ putBlockRequest, null).getContent().asReadOnlyByteBuffer();
+ Preconditions.checkState(command.remaining() <=
PUT_BLOCK_REQUEST_LENGTH_MAX,
+ "PutBlock command length %s > max %s", command.remaining(),
+ PUT_BLOCK_REQUEST_LENGTH_MAX);
+ RatisHelper.debug(command, "putBlockCommand", LOG);
+ metrics.incrPendingContainerOpsMetrics(ContainerProtos.Type.PutBlock);
+ putBlockCommandFutures.add(out.commandAsync(command)
+ .whenCompleteAsync((reply, e) -> {
+
metrics.decrPendingContainerOpsMetrics(ContainerProtos.Type.PutBlock);
+ try {
+ validatePutBlockCommandReply(reply, e);
+ } catch (IOException ioe) {
+ setIoException(ioe);
+ throw new CompletionException(ioe);
+ }
+ // The command has no log index; use 0 as for the standalone protocol
+ // so that the buffers are released by the next watchForCommit.
+ commitWatcher.updateCommitInfoMap(0, byteBufferList);
+ }, responseExecutor));
+ }
+
+ private void validatePutBlockCommandReply(DataStreamReply reply, Throwable e)
+ throws IOException {
+ if (e != null || reply == null || !reply.isSuccess()) {
+ throw new IOException("Failed to commit PutBlock for blockID " + blockID
+ + " through the data stream, reply=" + reply, e);
+ }
+ final ByteBuffer response = reply.nioBuffer();
+ if (!response.hasRemaining()) {
+ throw new IOException("Datanodes in pipeline "
+ + xceiverClient.getPipeline().getId() + " did not handle the
PutBlock"
+ + " command for blockID " + blockID
+ + "; they may be running a version which does not support it");
+ }
+ validateResponse(ContainerCommandResponseProto.parseFrom(response));
+ }
+
public static CompletableFuture<DataStreamReply> executePutBlockClose(
ContainerCommandRequestProto putBlockRequest, int max,
DataStreamOutput out) {
@@ -559,12 +621,12 @@ public void waitFuturesComplete() throws IOException {
}
}
- private void waitPutBlockFuturesComplete() throws IOException {
- if (putBlockFutures.isEmpty()) {
+ private void waitPutBlockCommandFuturesComplete() throws IOException {
+ if (putBlockCommandFutures.isEmpty()) {
return;
}
try {
-
CompletableFuture.allOf(putBlockFutures.toArray(EMPTY_FUTURE_ARRAY)).get();
+
CompletableFuture.allOf(putBlockCommandFutures.toArray(EMPTY_FUTURE_ARRAY)).get();
checkOpen();
} catch (Exception e) {
LOG.warn("Failed to commit PutBlock before stream close: " + e);
@@ -596,6 +658,7 @@ private void handleFlush(boolean close)
executePutBlock(true, true);
}
CompletableFuture.allOf(putBlockFutures.toArray(EMPTY_FUTURE_ARRAY)).get();
+
CompletableFuture.allOf(putBlockCommandFutures.toArray(EMPTY_FUTURE_ARRAY)).get();
watchForCommit(false);
// just check again if the exception is hit while waiting for the
// futures to ensure flush has indeed succeeded
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
index cb05d31a2d6..c9ba90c87c8 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/TestOzoneClientConfig.java
@@ -93,19 +93,19 @@ public void testStreamReadConfigParsing() {
}
@Test
- void testDatastreamPutBlockOnCloseEnabledDefault() {
+ void testDatastreamPutBlockWithoutRaftEnabledDefault() {
OzoneClientConfig subject = new OzoneConfiguration()
.getObject(OzoneClientConfig.class);
- assertFalse(subject.isDatastreamPutBlockOnCloseEnabled());
+ assertFalse(subject.isDatastreamPutBlockWithoutRaftEnabled());
}
@Test
- void testDatastreamPutBlockOnCloseConfigParsing() {
+ void testDatastreamPutBlockWithoutRaftConfigParsing() {
OzoneConfiguration conf = new OzoneConfiguration();
- conf.setBoolean("ozone.client.datastream.putblock.on.close.enabled", true);
+ conf.setBoolean("ozone.client.datastream.putblock.without.raft.enabled",
true);
OzoneClientConfig subject = conf.getObject(OzoneClientConfig.class);
- assertTrue(subject.isDatastreamPutBlockOnCloseEnabled());
+ assertTrue(subject.isDatastreamPutBlockWithoutRaftEnabled());
}
}
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java
index c1c11b0d1a8..c3cb97b7aa3 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/MockDatanodePipeline.java
@@ -77,8 +77,11 @@ public class MockDatanodePipeline {
// Recorded state
private final List<byte[]> receivedChunks = Collections.synchronizedList(new
ArrayList<>());
private final List<ContainerCommandRequestProto> receivedPutBlocks =
Collections.synchronizedList(new ArrayList<>());
+ private final List<ContainerCommandRequestProto> receivedCommands =
Collections.synchronizedList(new ArrayList<>());
private final AtomicInteger watchForCommitCount = new AtomicInteger(0);
private volatile Type streamInitType;
+ // When true, stream commands are replied without a payload, as a datanode
which does not handle them does.
+ private volatile boolean commandUnsupported;
// Commit tracking
private final AtomicLong nextLogIndex = new AtomicLong(1);
@@ -202,6 +205,26 @@ public MockDatanodePipeline(BlockID blockID) throws
IOException {
return CompletableFuture.completedFuture(dataStreamReply(data.length));
}).when(mockDataStreamOutput).writeAsync(any(ByteBuffer.class),
any(Iterable.class));
+ // Setup commandAsync (mid-stream PutBlock) behavior
+ doAnswer(invocation -> {
+ ByteBuffer src = invocation.getArgument(0);
+ final ContainerCommandRequestProto request = toProto(src);
+ receivedCommands.add(request);
+ if (commandUnsupported) {
+ return CompletableFuture.completedFuture(dataStreamReply(0));
+ }
+ int count = putBlockCount.incrementAndGet();
+ if (count > putBlockFailAfter && putBlockFailure != null) {
+ CompletableFuture<DataStreamReply> failed = new CompletableFuture<>();
+ failed.completeExceptionally(putBlockFailure.get());
+ return failed;
+ }
+ final DataStreamReply reply = dataStreamReply(0);
+ when(reply.nioBuffer()).thenReturn(
+
buildPutBlockResponse(blockID).toByteString().asReadOnlyByteBuffer());
+ return CompletableFuture.completedFuture(reply);
+ }).when(mockDataStreamOutput).commandAsync(any(ByteBuffer.class));
+
// Mock XceiverClientFactory
this.clientFactory = mock(XceiverClientFactory.class);
doReturn(xceiverClient).when(clientFactory).acquireClient(any(Pipeline.class),
anyBoolean());
@@ -234,6 +257,11 @@ public List<ContainerCommandRequestProto>
getReceivedPutBlocks() {
return receivedPutBlocks;
}
+ /** @return the requests received as data stream commands. */
+ public List<ContainerCommandRequestProto> getReceivedCommands() {
+ return receivedCommands;
+ }
+
public int getWatchForCommitCount() {
return watchForCommitCount.get();
}
@@ -274,17 +302,26 @@ public MockDatanodePipeline failWatchAfter(int n,
Supplier<Throwable> err) {
return this;
}
+ /** Reply to stream commands without a payload, as a datanode which does not
handle them does. */
+ public MockDatanodePipeline withUnsupportedCommand() {
+ this.commandUnsupported = true;
+ return this;
+ }
+
// --- Helpers ---
private void captureStreamInitType(ByteBuffer buffer) {
+ streamInitType = toProto(buffer).getCmdType();
+ }
+
+ private static ContainerCommandRequestProto toProto(ByteBuffer buffer) {
ByteBuffer dup = buffer.duplicate();
byte[] bytes = new byte[dup.remaining()];
dup.get(bytes);
try {
- streamInitType = ContainerCommandRequestMessage.toProto(
- ByteString.copyFrom(bytes), null).getCmdType();
+ return
ContainerCommandRequestMessage.toProto(ByteString.copyFrom(bytes), null);
} catch (Exception e) {
- throw new IllegalStateException("Failed to decode stream init request",
e);
+ throw new IllegalStateException("Failed to decode container command
request", e);
}
}
@@ -308,6 +345,8 @@ private static DataStreamReply dataStreamReply(long
bytesWritten) {
when(reply.getBytesWritten()).thenReturn(bytesWritten);
when(reply.getDataLength()).thenReturn(bytesWritten);
when(reply.getCommitInfos()).thenReturn(Collections.emptyList());
+ // Ratis replies with an empty buffer unless the state machine returns a
command reply.
+
when(reply.nioBuffer()).thenReturn(ByteBuffer.allocate(0).asReadOnlyBuffer());
return reply;
}
}
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java
index 35aaac2a5af..b9de99ad016 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockDataStreamOutput.java
@@ -29,6 +29,7 @@
import java.util.List;
import org.apache.commons.lang3.RandomUtils;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandRequestProto;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Type;
import org.apache.hadoop.hdds.scm.OzoneClientConfig;
import org.junit.jupiter.api.Test;
@@ -78,12 +79,12 @@ private BlockDataStreamOutput createStream(
@ParameterizedTest
@ValueSource(booleans = {false, true})
- void streamInitTypeFollowsClientConfig(boolean putBlockOnCloseEnabled)
throws Exception {
+ void streamInitTypeFollowsClientConfig(boolean putBlockWithoutRaft) throws
Exception {
MockDatanodePipeline pipeline = new MockDatanodePipeline();
OzoneClientConfig config = createConfig();
- config.setDatastreamPutBlockOnCloseEnabled(putBlockOnCloseEnabled);
+ config.setDatastreamPutBlockWithoutRaftEnabled(putBlockWithoutRaft);
try (BlockDataStreamOutput stream = createStream(pipeline, config)) {
- Type expected = putBlockOnCloseEnabled ? Type.StreamInitWithPutBlock :
Type.StreamInit;
+ Type expected = putBlockWithoutRaft ? Type.StreamInitWithPutBlock :
Type.StreamInit;
assertEquals(expected, pipeline.getStreamInitType());
}
}
@@ -130,6 +131,58 @@ void writeFlushBoundaryTriggersPutBlock() throws Exception
{
assertEquals(2, pipeline.getReceivedPutBlocks().size());
}
+ @Test
+ void midStreamPutBlockUsesStreamCommandWhenEnabled() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ OzoneClientConfig config = createConfig();
+ config.setDatastreamPutBlockWithoutRaftEnabled(true);
+ byte[] data = randomBytes(400);
+ try (BlockDataStreamOutput stream = createStream(pipeline, config)) {
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ // 4 chunks of 100B each → hits flush boundary → 1 PutBlock, sent as a
stream command
+ assertEquals(4, pipeline.getReceivedChunks().size());
+ assertEquals(0, pipeline.getReceivedPutBlocks().size(), "The mid-stream
PutBlock must not go through Raft");
+ assertEquals(1, pipeline.getReceivedCommands().size());
+
+ ContainerCommandRequestProto command =
pipeline.getReceivedCommands().get(0);
+ assertEquals(Type.PutBlock, command.getCmdType());
+ assertThat(command.getPutBlock().getEof()).isFalse();
+ assertEquals(4, command.getPutBlock().getBlockData().getChunksCount());
+ }
+ // Close does not add another PutBlock command: it is appended to the
stream instead
+ assertEquals(1, pipeline.getReceivedCommands().size());
+ assertEquals(0, pipeline.getReceivedPutBlocks().size());
+ assertArrayEquals(data, pipeline.getAllReceivedData());
+ }
+
+ @Test
+ void midStreamPutBlockCommandReleasesBuffers() throws Exception {
+ MockDatanodePipeline pipeline = new MockDatanodePipeline();
+ OzoneClientConfig config = createConfig();
+ config.setDatastreamPutBlockWithoutRaftEnabled(true);
+ BlockDataStreamOutput stream = createStream(pipeline, config);
+ byte[] data = randomBytes(400);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+ stream.close();
+
+ assertEquals(400, stream.getTotalAckDataLength(),
+ "The buffers of a PutBlock committed by a stream command should be
acknowledged");
+ }
+
+ @Test
+ void midStreamPutBlockFailsOnDatanodeWithoutCommandSupport() throws
Exception {
+ MockDatanodePipeline pipeline = new
MockDatanodePipeline().withUnsupportedCommand();
+ OzoneClientConfig config = createConfig();
+ config.setDatastreamPutBlockWithoutRaftEnabled(true);
+ BlockDataStreamOutput stream = createStream(pipeline, config);
+ byte[] data = randomBytes(400);
+ stream.write(ByteBuffer.wrap(data), 0, data.length);
+
+ IOException e = assertThrows(IOException.class, stream::hsync);
+ assertThat(e).hasStackTraceContaining("did not handle the PutBlock
command");
+ assertThrows(IOException.class, stream::close);
+ }
+
@Test
void writeAcrossStreamWindowTriggersBackPressure() throws Exception {
MockDatanodePipeline pipeline = new MockDatanodePipeline();
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachine.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachine.java
index 9a3e988e27a..f77bfadefad 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachine.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/ContainerStateMachine.java
@@ -28,6 +28,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collection;
@@ -106,6 +107,7 @@
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
import
org.apache.ratis.thirdparty.com.google.protobuf.InvalidProtocolBufferException;
import org.apache.ratis.thirdparty.com.google.protobuf.TextFormat;
+import org.apache.ratis.thirdparty.com.google.protobuf.UnsafeByteOperations;
import org.apache.ratis.util.FileUtils;
import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.LifeCycle;
@@ -740,6 +742,33 @@ void streamPutBlock(ContainerCommandRequestProto request)
throws IOException {
dispatchCommand(request, context);
}
+ /**
+ * Commit a PutBlock sent as a command in the middle of a data stream;
+ * see {@link StateMachine.DataStream#onCommand(ByteBuffer, long)}.
+ * The PutBlock is applied without a Raft log entry, so it has no log index
to use as bcsId.
+ *
+ * @return the serialized {@link ContainerCommandResponseProto},
+ * which is identical on all the peers of the pipeline.
+ */
+ ByteBuffer streamCommand(ByteBuffer command) throws IOException {
+ // unsafeWrap is okay since the command buffer can be modified after this
method has returned.
+ final ContainerCommandRequestProto request =
ContainerCommandRequestMessage.toProto(
+ UnsafeByteOperations.unsafeWrap(command), getGroupId());
+ if (request.getCmdType() != Type.PutBlock) {
+ throw new StorageContainerException("Unexpected stream command " +
request.getCmdType()
+ + ", expected " + Type.PutBlock,
ContainerProtos.Result.MALFORMED_REQUEST);
+ }
+ final DispatcherContext context =
DispatcherContext.newBuilder(DispatcherContext.Op.STREAM_COMMAND)
+ .setStage(DispatcherContext.WriteChunkStage.COMBINED)
+ .setContainer2BCSIDMap(container2BCSIDMap)
+ .build();
+ final ContainerCommandResponseProto response = dispatchCommand(request,
context);
+ if (response.getResult() != ContainerProtos.Result.SUCCESS) {
+ throw new StorageContainerException(response.getMessage(),
response.getResult());
+ }
+ return response.toByteString().asReadOnlyByteBuffer();
+ }
+
@Override
public CompletableFuture<DataStream> stream(RaftClientRequest request) {
return CompletableFuture.supplyAsync(() -> {
@@ -755,7 +784,7 @@ public CompletableFuture<DataStream>
stream(RaftClientRequest request) {
final DataChannel channel = getStreamDataChannel(requestProto,
context);
final ExecutorService chunkExecutor = requestProto.hasWriteChunk() ?
getChunkExecutor(requestProto.getWriteChunk()) : null;
- return new LocalStream(channel, chunkExecutor);
+ return new LocalStream(channel, chunkExecutor, this::streamCommand);
} catch (IOException e) {
throw new CompletionException("Failed to create data stream", e);
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/DispatcherContext.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/DispatcherContext.java
index e0f0ccbe193..b5fd18aa405 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/DispatcherContext.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/transport/server/ratis/DispatcherContext.java
@@ -108,7 +108,8 @@ public enum Op {
APPLY_TRANSACTION,
STREAM_INIT,
- STREAM_LINK;
+ STREAM_LINK,
+ STREAM_COMMAND;
public boolean readFromTmpFile() {
return this == READ_STATE_MACHINE_DATA;
@@ -120,6 +121,7 @@ public boolean validateToken() {
case WRITE_STATE_MACHINE_DATA:
case READ_STATE_MACHINE_DATA:
case STREAM_LINK:
+ case STREAM_COMMAND:
return false;
default:
return true;
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 f2b73e3e8fa..47189031a11 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
@@ -17,19 +17,27 @@
package org.apache.hadoop.ozone.container.common.transport.server.ratis;
+import java.io.IOException;
+import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import
org.apache.hadoop.ozone.container.keyvalue.impl.KeyValueStreamDataChannel;
import org.apache.ratis.statemachine.StateMachine;
import org.apache.ratis.util.JavaUtils;
+import org.apache.ratis.util.function.CheckedFunction;
class LocalStream implements StateMachine.DataStream {
private final StateMachine.DataChannel dataChannel;
private final Executor executor;
+ /** Handler for the commands sent in the middle of the stream. */
+ private final CheckedFunction<ByteBuffer, ByteBuffer, IOException> command;
- LocalStream(StateMachine.DataChannel dataChannel, Executor executor) {
+ LocalStream(StateMachine.DataChannel dataChannel, Executor executor,
+ CheckedFunction<ByteBuffer, ByteBuffer, IOException> command) {
this.dataChannel = dataChannel;
this.executor = executor;
+ this.command = command;
}
@Override
@@ -48,6 +56,18 @@ public CompletableFuture<?> cleanUp() {
executor);
}
+ @Override
+ public CompletableFuture<ByteBuffer> onCommand(ByteBuffer buffer, long
streamOffset) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: drain datachannel.buffers
+ return command.apply(buffer);
+ } catch (IOException e) {
+ throw new CompletionException(e);
+ }
+ }, executor);
+ }
+
@Override
public Executor getExecutor() {
return executor;
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/impl/TestHddsDispatcher.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/impl/TestHddsDispatcher.java
index d60ca220ece..f6137361ca9 100644
---
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/impl/TestHddsDispatcher.java
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/impl/TestHddsDispatcher.java
@@ -907,7 +907,8 @@ public void verify(Token<?> token,
newContext(Op.WRITE_STATE_MACHINE_DATA, WriteChunkStage.WRITE_DATA),
newContext(Op.READ_STATE_MACHINE_DATA),
newContext(Op.APPLY_TRANSACTION),
- newContext(Op.STREAM_LINK, WriteChunkStage.COMMIT_DATA)
+ newContext(Op.STREAM_LINK, WriteChunkStage.COMMIT_DATA),
+ newContext(Op.STREAM_COMMAND, WriteChunkStage.COMMIT_DATA)
};
for (DispatcherContext context : notVerify) {
LOG.info("notVerify {}", context);
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
index c011c774c77..6a431e477b9 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
@@ -170,17 +170,17 @@ private static Stream<Arguments> dataLengthParameters() {
private static Stream<Arguments> streamWriteParameters() {
return dataLengthParameters().flatMap(dataLength ->
- Stream.of(true, false).map(putBlockOnCloseEnabled ->
- Arguments.of(dataLength.get()[0], putBlockOnCloseEnabled)));
+ Stream.of(true, false).map(putBlockWithoutRaft ->
+ Arguments.of(dataLength.get()[0], putBlockWithoutRaft)));
}
static OzoneClientConfig newClientConfig(ConfigurationSource source,
boolean flushDelay,
- boolean putBlockOnCloseEnabled) {
+ boolean putBlockWithoutRaft) {
OzoneClientConfig clientConfig = source.getObject(OzoneClientConfig.class);
clientConfig.setChecksumType(ContainerProtos.ChecksumType.NONE);
clientConfig.setStreamBufferFlushDelay(flushDelay);
- clientConfig.setDatastreamPutBlockOnCloseEnabled(putBlockOnCloseEnabled);
+ clientConfig.setDatastreamPutBlockWithoutRaftEnabled(putBlockWithoutRaft);
return clientConfig;
}
@@ -210,13 +210,13 @@ public void shutdown() {
@ParameterizedTest
@MethodSource("streamWriteParameters")
@Flaky("HDDS-12027")
- public void testStreamWrite(int dataLength, boolean putBlockOnCloseEnabled)
throws Exception {
- OzoneClientConfig config = newClientConfig(cluster.getConf(), false,
putBlockOnCloseEnabled);
+ public void testStreamWrite(int dataLength, boolean putBlockWithoutRaft)
throws Exception {
+ OzoneClientConfig config = newClientConfig(cluster.getConf(), false,
putBlockWithoutRaft);
try (OzoneClient client = newClient(cluster.getConf(), config)) {
testWrite(client, dataLength);
// Forced container close before stream close relies on async PutBlock
recovery;
// that path is not used when PutBlock is committed only on data stream
close.
- if (!putBlockOnCloseEnabled) {
+ if (!putBlockWithoutRaft) {
testWriteWithFailure(client, dataLength);
}
}
@@ -267,9 +267,9 @@ static void validateData(OzoneClient client, String
keyName, byte[] data) throws
@ParameterizedTest
@MethodSource("clientParameters")
- public void testPutBlockAtBoundary(boolean flushDelay, boolean
putBlockOnCloseEnabled)
+ public void testPutBlockAtBoundary(boolean flushDelay, boolean
putBlockWithoutRaft)
throws Exception {
- OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockOnCloseEnabled);
+ OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockWithoutRaft);
try (OzoneClient client = newClient(cluster.getConf(), config)) {
int dataLength = 500;
XceiverClientMetrics metrics =
@@ -287,22 +287,30 @@ public void testPutBlockAtBoundary(boolean flushDelay,
boolean putBlockOnCloseEn
key.write(ByteBuffer.wrap(data));
assertThat(metrics.getPendingContainerOpCountMetrics(ContainerProtos.Type.PutBlock))
.isLessThanOrEqualTo(pendingPutBlockCount + 1);
+ BlockDataStreamOutputEntry entry =
+ ((KeyDataStreamOutput)
key.getByteBufStreamOutput()).getStreamEntries().get(0);
key.close();
// Since data length is 500, first putBlock will be at 400 (flush
boundary).
- // Close commits via WriteAsync PutBlock only when putBlockOnClose is
disabled.
- int expectedPutBlocks = putBlockOnCloseEnabled ? 1 : 2;
+ // Close commits via WriteAsync PutBlock only when PutBlock does not go
through the data stream.
+ int expectedPutBlocks = putBlockWithoutRaft ? 1 : 2;
assertEquals(
metrics.getContainerOpCountMetrics(ContainerProtos.Type.PutBlock),
putBlockCount + expectedPutBlocks);
+ if (putBlockWithoutRaft) {
+ // No PutBlock went through Raft, so there is no log index to use as
block commit sequence id.
+ assertEquals(0, entry.getBlockID().getBlockCommitSequenceId());
+ } else {
+ assertThat(entry.getBlockID().getBlockCommitSequenceId()).isPositive();
+ }
validateData(client, keyName, data);
}
}
@ParameterizedTest
@MethodSource("clientParameters")
- public void testMinPacketSize(boolean flushDelay, boolean
putBlockOnCloseEnabled)
+ public void testMinPacketSize(boolean flushDelay, boolean
putBlockWithoutRaft)
throws Exception {
- OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockOnCloseEnabled);
+ OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockWithoutRaft);
try (OzoneClient client = newClient(cluster.getConf(), config)) {
String keyName = getKeyName();
XceiverClientMetrics metrics =
@@ -329,9 +337,9 @@ public void testMinPacketSize(boolean flushDelay, boolean
putBlockOnCloseEnabled
@ParameterizedTest
@MethodSource("clientParameters")
- public void testTotalAckDataLength(boolean flushDelay, boolean
putBlockOnCloseEnabled)
+ public void testTotalAckDataLength(boolean flushDelay, boolean
putBlockWithoutRaft)
throws Exception {
- OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockOnCloseEnabled);
+ OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockWithoutRaft);
try (OzoneClient client = newClient(cluster.getConf(), config)) {
int dataLength = 400;
String keyName = getKeyName();
@@ -352,9 +360,9 @@ public void testTotalAckDataLength(boolean flushDelay,
boolean putBlockOnCloseEn
@ParameterizedTest
@MethodSource("clientParameters")
- public void testDatanodeVersion(boolean flushDelay, boolean
putBlockOnCloseEnabled)
+ public void testDatanodeVersion(boolean flushDelay, boolean
putBlockWithoutRaft)
throws Exception {
- OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockOnCloseEnabled);
+ OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay,
putBlockWithoutRaft);
try (OzoneClient client = newClient(cluster.getConf(), config)) {
// Verify all DNs internally have versions set correctly
List<HddsDatanodeService> dns = cluster.getHddsDatanodes();
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 5e10814f508..c10dfe784fb 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
@@ -77,23 +77,23 @@ void shutdown() {
private static Stream<Arguments> streamingParameters() {
return Stream.of(-1, +1).flatMap(offset ->
- Stream.of(false, true).map(putBlockOnCloseEnabled ->
- Arguments.of(offset, putBlockOnCloseEnabled)));
+ Stream.of(false, true).map(putBlockWithoutRaft ->
+ Arguments.of(offset, putBlockWithoutRaft)));
}
@ParameterizedTest
@MethodSource("streamingParameters")
- void testContainerStateMachineForStreaming(int offset, boolean
putBlockOnCloseEnabled)
+ void testContainerStateMachineForStreaming(int offset, boolean
putBlockWithoutRaft)
throws Exception {
final int size = chunkSize + offset;
OzoneConfiguration conf = new OzoneConfiguration(cluster().getConf());
OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
- clientConfig.setDatastreamPutBlockOnCloseEnabled(putBlockOnCloseEnabled);
+ clientConfig.setDatastreamPutBlockWithoutRaftEnabled(putBlockWithoutRaft);
conf.setFromObject(clientConfig);
final List<OmKeyLocationInfo> locationInfoList;
try (OzoneClient streamingClient = OzoneClientFactory.getRpcClient(conf);
- OzoneDataStreamOutput key = createStreamKey("key" + offset + "-" +
putBlockOnCloseEnabled,
+ OzoneDataStreamOutput key = createStreamKey("key" + offset + "-" +
putBlockWithoutRaft,
ReplicationType.RATIS, size, streamingClient.getObjectStore(),
volumeName, bucketName)) {
byte[] data = ContainerTestHelper.generateData(size, true);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]