Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,9 @@ public final class ScmConfigKeys {

public static final String NET_TOPOLOGY_NODE_SWITCH_MAPPING_IMPL_KEY =
"net.topology.node.switch.mapping.impl";

public static final String HDDS_CONTAINER_RATIS_STATEMACHINE_WRITE_WAIT_INTERVAL
= "hdds.container.ratis.statemachine.write.wait.interval";
public static final long HDDS_CONTAINER_RATIS_STATEMACHINE_WRITE_WAIT_INTERVAL_DEFAULT = 10 * 60 * 1000L;
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
/**
* Never constructed.
*/
Expand Down
8 changes: 8 additions & 0 deletions hadoop-hdds/common/src/main/resources/ozone-default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3548,6 +3548,14 @@
Timeout for the request submitted directly to Ratis in datanode.
</description>
</property>
<property>
<name>hdds.container.ratis.statemachine.write.wait.interval</name>
<tag>OZONE, DATANODE</tag>
<value>10m</value>
<description>
Timeout for the write path for container blocks.
</description>
</property>
<property>
<name>hdds.datanode.slow.op.warning.threshold</name>
<tag>OZONE, DATANODE, PERFORMANCE</tag>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,21 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.apache.hadoop.hdds.HddsUtils;
Expand Down Expand Up @@ -187,13 +191,38 @@ long getStartTime() {
}
}

static class WriteFutureContext {
private final Future<ContainerCommandResponseProto> writeChunkFuture;
private final CompletableFuture<Message> raftFuture;
private final long startTime;
Comment thread
sumitagrawl marked this conversation as resolved.

WriteFutureContext(Future<ContainerCommandResponseProto> writeChunkFuture,
CompletableFuture<Message> raftFuture, long startTime) {
this.writeChunkFuture = writeChunkFuture;
this.raftFuture = raftFuture;
this.startTime = startTime;
}

public Future<ContainerCommandResponseProto> getWriteChunkFuture() {
return writeChunkFuture;
}

public CompletableFuture<Message> getRaftFuture() {
return raftFuture;
}

long getStartTime() {
return startTime;
}
}

private final SimpleStateMachineStorage storage =
new SimpleStateMachineStorage();
private final ContainerDispatcher dispatcher;
private final ContainerController containerController;
private final XceiverServerRatis ratisServer;
private final ConcurrentHashMap<Long,
CompletableFuture<ContainerCommandResponseProto>> writeChunkFutureMap;
private final NavigableMap<Long, WriteFutureContext> writeChunkFutureMap;
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
private final long writeChunkWaitMaxMs;
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated

// keeps track of the containers created per pipeline
private final Map<Long, Long> container2BCSIDMap;
Expand Down Expand Up @@ -229,7 +258,7 @@ public ContainerStateMachine(HddsDatanodeService hddsDatanodeService, RaftGroupI
this.containerController = containerController;
this.ratisServer = ratisServer;
metrics = CSMMetrics.create(gid);
this.writeChunkFutureMap = new ConcurrentHashMap<>();
this.writeChunkFutureMap = new ConcurrentSkipListMap<>();
applyTransactionCompletionMap = new ConcurrentHashMap<>();
this.unhealthyContainers = ConcurrentHashMap.newKeySet();
long pendingRequestsBytesLimit = (long)conf.getStorageSize(
Expand Down Expand Up @@ -273,6 +302,8 @@ public ContainerStateMachine(HddsDatanodeService hddsDatanodeService, RaftGroupI
this.waitOnBothFollowers = conf.getObject(
DatanodeConfiguration.class).waitOnAllFollowers();

this.writeChunkWaitMaxMs = conf.getTimeDuration(ScmConfigKeys.HDDS_CONTAINER_RATIS_STATEMACHINE_WRITE_WAIT_INTERVAL,
ScmConfigKeys.HDDS_CONTAINER_RATIS_STATEMACHINE_WRITE_WAIT_INTERVAL_DEFAULT, TimeUnit.MILLISECONDS);
}

private void validatePeers() throws IOException {
Expand Down Expand Up @@ -542,6 +573,15 @@ private ContainerCommandResponseProto dispatchCommand(
private CompletableFuture<Message> writeStateMachineData(
ContainerCommandRequestProto requestProto, long entryIndex, long term,
long startTime) {
if (writeChunkFutureMap.containsKey(entryIndex)) {
// generally state machine will wait forever, for precaution, a check is added if retry happens.
return writeChunkFutureMap.get(entryIndex).getRaftFuture();
}
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
try {
validateLongRunningWrite();
} catch (IOException e) {
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
return completeExceptionally(e);
}
final WriteChunkRequestProto write = requestProto.getWriteChunk();
RaftServer server = ratisServer.getServer();
Preconditions.checkArgument(!write.getData().isEmpty());
Expand All @@ -564,79 +604,111 @@ private CompletableFuture<Message> writeStateMachineData(
.setContainer2BCSIDMap(container2BCSIDMap)
.build();
CompletableFuture<Message> raftFuture = new CompletableFuture<>();
// ensure the write chunk happens asynchronously in writeChunkExecutor pool
// thread.
CompletableFuture<ContainerCommandResponseProto> writeChunkFuture =
CompletableFuture.supplyAsync(() -> {
try {
try {
checkContainerHealthy(write.getBlockID().getContainerID(), true);
} catch (StorageContainerException e) {
return ContainerUtils.logAndReturnError(LOG, e, requestProto);
}
metrics.recordWriteStateMachineQueueingLatencyNs(
Time.monotonicNowNanos() - startTime);
return dispatchCommand(requestProto, context);
} catch (Exception e) {
LOG.error("{}: writeChunk writeStateMachineData failed: blockId" +
// ensure the write chunk happens asynchronously in writeChunkExecutor pool thread.
Future<ContainerCommandResponseProto> future = getChunkExecutor(requestProto.getWriteChunk()).submit(() -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why replacing CompletableFuture.supplyAsync(..)?

@sumitagrawl sumitagrawl Mar 11, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompetableFuture do not support cancel() operation to interrupt running thread. Tested same and it was not working, so replaced with Future as suggeted. below is reference as used...
https://stackoverflow.com/questions/29013831/how-to-interrupt-underlying-execution-of-completablefuture

Since (unlike FutureTask) this class has no direct control over the computation that causes it to be completed, cancellation is treated as just another form of exceptional completion. Method cancel has the same effect as completeExceptionally(new CancellationException()).

try {
try {
checkContainerHealthy(write.getBlockID().getContainerID(), true);
} catch (StorageContainerException e) {
ContainerCommandResponseProto result = ContainerUtils.logAndReturnError(LOG, e, requestProto);
handleCommandResult(requestProto, entryIndex, startTime, result, write, raftFuture);
return result;
}
metrics.recordWriteStateMachineQueueingLatencyNs(
Time.monotonicNowNanos() - startTime);
ContainerCommandResponseProto result = dispatchCommand(requestProto, context);
handleCommandResult(requestProto, entryIndex, startTime, result, write, raftFuture);
return result;
} catch (Exception e) {
LOG.error("{}: writeChunk writeStateMachineData failed: blockId" +
"{} logIndex {} chunkName {}", getGroupId(), write.getBlockID(),
entryIndex, write.getChunkData().getChunkName(), e);
metrics.incNumWriteDataFails();
// write chunks go in parallel. It's possible that one write chunk
// see the stateMachine is marked unhealthy by other parallel thread
unhealthyContainers.add(write.getBlockID().getContainerID());
stateMachineHealthy.set(false);
raftFuture.completeExceptionally(e);
throw e;
}
}, getChunkExecutor(requestProto.getWriteChunk()));
entryIndex, write.getChunkData().getChunkName(), e);
metrics.incNumWriteDataFails();
// write chunks go in parallel. It's possible that one write chunk
// see the stateMachine is marked unhealthy by other parallel thread
unhealthyContainers.add(write.getBlockID().getContainerID());
stateMachineHealthy.set(false);
raftFuture.completeExceptionally(e);
throw e;
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
} finally {
// Remove the future once it finishes execution from the
writeChunkFutureMap.remove(entryIndex);
}
});

writeChunkFutureMap.put(entryIndex, writeChunkFuture);
writeChunkFutureMap.put(entryIndex, new WriteFutureContext(future, raftFuture, startTime));
if (LOG.isDebugEnabled()) {
LOG.debug("{}: writeChunk writeStateMachineData : blockId" +
"{} logIndex {} chunkName {}", getGroupId(), write.getBlockID(),
entryIndex, write.getChunkData().getChunkName());
}
// Remove the future once it finishes execution from the
// writeChunkFutureMap.
writeChunkFuture.thenApply(r -> {
if (r.getResult() != ContainerProtos.Result.SUCCESS
&& r.getResult() != ContainerProtos.Result.CONTAINER_NOT_OPEN
&& r.getResult() != ContainerProtos.Result.CLOSED_CONTAINER_IO
// After concurrent flushes are allowed on the same key, chunk file inconsistencies can happen and
// that should not crash the pipeline.
&& r.getResult() != ContainerProtos.Result.CHUNK_FILE_INCONSISTENCY) {
StorageContainerException sce =
new StorageContainerException(r.getMessage(), r.getResult());
LOG.error(getGroupId() + ": writeChunk writeStateMachineData failed: blockId" +
return raftFuture;
}

private void handleCommandResult(ContainerCommandRequestProto requestProto, long entryIndex, long startTime,
ContainerCommandResponseProto r, WriteChunkRequestProto write,
CompletableFuture<Message> raftFuture) {
if (r.getResult() != ContainerProtos.Result.SUCCESS
&& r.getResult() != ContainerProtos.Result.CONTAINER_NOT_OPEN
&& r.getResult() != ContainerProtos.Result.CLOSED_CONTAINER_IO
// After concurrent flushes are allowed on the same key, chunk file inconsistencies can happen and
// that should not crash the pipeline.
&& r.getResult() != ContainerProtos.Result.CHUNK_FILE_INCONSISTENCY) {
StorageContainerException sce =
new StorageContainerException(r.getMessage(), r.getResult());
LOG.error(getGroupId() + ": writeChunk writeStateMachineData failed: blockId" +
write.getBlockID() + " logIndex " + entryIndex + " chunkName " +
write.getChunkData().getChunkName() + " Error message: " +
r.getMessage() + " Container Result: " + r.getResult());
metrics.incNumWriteDataFails();
// If the write fails currently we mark the stateMachine as unhealthy.
// This leads to pipeline close. Any change in that behavior requires
// handling the entry for the write chunk in cache.
stateMachineHealthy.set(false);
unhealthyContainers.add(write.getBlockID().getContainerID());
raftFuture.completeExceptionally(sce);
} else {
metrics.incNumBytesWrittenCount(
requestProto.getWriteChunk().getChunkData().getLen());
if (LOG.isDebugEnabled()) {
LOG.debug(getGroupId() +
": writeChunk writeStateMachineData completed: blockId" +
write.getBlockID() + " logIndex " + entryIndex + " chunkName " +
write.getChunkData().getChunkName() + " Error message: " +
r.getMessage() + " Container Result: " + r.getResult());
metrics.incNumWriteDataFails();
// If the write fails currently we mark the stateMachine as unhealthy.
// This leads to pipeline close. Any change in that behavior requires
// handling the entry for the write chunk in cache.
stateMachineHealthy.set(false);
unhealthyContainers.add(write.getBlockID().getContainerID());
raftFuture.completeExceptionally(sce);
} else {
metrics.incNumBytesWrittenCount(
requestProto.getWriteChunk().getChunkData().getLen());
if (LOG.isDebugEnabled()) {
LOG.debug(getGroupId() +
": writeChunk writeStateMachineData completed: blockId" +
write.getBlockID() + " logIndex " + entryIndex + " chunkName " +
write.getChunkData().getChunkName());
}
raftFuture.complete(r::toByteString);
metrics.recordWriteStateMachineCompletionNs(
Time.monotonicNowNanos() - startTime);
write.getChunkData().getChunkName());
}
raftFuture.complete(r::toByteString);
metrics.recordWriteStateMachineCompletionNs(
Time.monotonicNowNanos() - startTime);
}
}

writeChunkFutureMap.remove(entryIndex);
return r;
});
return raftFuture;
private void validateLongRunningWrite() throws IOException {
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
// get min valid write chunk operation's future context
Map.Entry<Long, WriteFutureContext> writeFutureContextEntry = null;
while (!writeChunkFutureMap.isEmpty()) {
writeFutureContextEntry = writeChunkFutureMap.firstEntry();
// there is a possibility of entry being removed before added in map, cleanup those
if (null == writeFutureContextEntry || !writeFutureContextEntry.getValue().getWriteChunkFuture().isDone()) {
break;
}
writeChunkFutureMap.remove(writeFutureContextEntry.getKey());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not call remove here and the it be removed in the usual way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a possibility observed in integration test that writeChunkFutureMap have dangling entry. So one of possibility,
Thread T1: called from ratis

  • future = executorService.submit()
  • map.add(index, future)

Thread T2: scheduled as above

  • operate write
  • remove from map map.remove(index)

So due to ordering of T1 and T2, there seems possibility that entry from map for index is removed first, and then entry is added to map.

So to handle this situation, additional remove is added if future is completed already.

}
if (null == writeFutureContextEntry) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeFutureContextEntry cannot be null here.

// validate for timeout in milli second
long waitTime = Time.monotonicNow() - writeFutureContextEntry.getValue().getStartTime() / 1000000;
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
if (waitTime > writeChunkWaitMaxMs) {
LOG.error("Write chunk has taken {}ms crossing threshold {}ms for groupId {}", waitTime, writeChunkWaitMaxMs,
getGroupId());
stateMachineHealthy.set(false);
writeChunkFutureMap.forEach((key, value) -> {
LOG.error("Cancelling write chunk for transaction {}, groupId {}", key, getGroupId());
value.getWriteChunkFuture().cancel(true);
});
throw new StorageContainerException("Write chunk has taken " + waitTime + " crossing threshold "
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
+ writeChunkWaitMaxMs + " for groupId " + getGroupId(), ContainerProtos.Result.CONTAINER_INTERNAL_ERROR);
}
}

private StateMachine.DataChannel getStreamDataChannel(
Expand Down Expand Up @@ -821,7 +893,7 @@ private ByteString readStateMachineData(
public CompletableFuture<Void> flush(long index) {
return CompletableFuture.allOf(
writeChunkFutureMap.entrySet().stream().filter(x -> x.getKey() <= index)
.map(Map.Entry::getValue).toArray(CompletableFuture[]::new));
.map(e -> e.getValue().getRaftFuture()).toArray(CompletableFuture[]::new));
Comment thread
sumitagrawl marked this conversation as resolved.
Outdated
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
Expand All @@ -30,8 +31,10 @@

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
Expand All @@ -57,6 +60,7 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
Expand Down Expand Up @@ -263,4 +267,54 @@ public void testApplyTransactionFailure(boolean failWithException) throws Execut
ContainerProtos.ContainerCommandResponseProto.parseFrom(succcesfulTransaction.getContent());
assertEquals(ContainerProtos.Result.SUCCESS, resp.getResult());
}

@Test
public void testWriteTimout() throws Exception {
RaftProtos.LogEntryProto entry = mock(RaftProtos.LogEntryProto.class);
when(entry.getTerm()).thenReturn(1L);
when(entry.getIndex()).thenReturn(1L);
RaftProtos.LogEntryProto entryNext = mock(RaftProtos.LogEntryProto.class);
when(entryNext.getTerm()).thenReturn(1L);
when(entryNext.getIndex()).thenReturn(2L);
TransactionContext trx = mock(TransactionContext.class);
ContainerStateMachine.Context context = mock(ContainerStateMachine.Context.class);
when(trx.getStateMachineContext()).thenReturn(context);
doAnswer(e -> {
try {
Thread.sleep(200000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw ie;
}
return null;
}).when(dispatcher).dispatch(any(), any());

when(context.getRequestProto()).thenReturn(ContainerProtos.ContainerCommandRequestProto.newBuilder()
.setCmdType(ContainerProtos.Type.WriteChunk).setWriteChunk(
ContainerProtos.WriteChunkRequestProto.newBuilder().setData(ByteString.copyFromUtf8("Test Data"))
.setBlockID(
ContainerProtos.DatanodeBlockID.newBuilder().setContainerID(1).setLocalID(1).build()).build())
.setContainerID(1)
.setDatanodeUuid(UUID.randomUUID().toString()).build());
AtomicReference<Throwable> throwable = new AtomicReference<>(null);
Function<Throwable, ? extends Message> throwableSetter = t -> {
throwable.set(t);
return null;
};
Field writeChunkWaitMaxMs = stateMachine.getClass().getDeclaredField("writeChunkWaitMaxMs");
writeChunkWaitMaxMs.setAccessible(true);
writeChunkWaitMaxMs.set(stateMachine, 1000);
CompletableFuture<Message> firstWrite = stateMachine.write(entry, trx);
Thread.sleep(2000);
CompletableFuture<Message> secondWrite = stateMachine.write(entryNext, trx);
firstWrite.exceptionally(throwableSetter).get();
assertNotNull(throwable.get());
assertInstanceOf(InterruptedException.class, throwable.get());

secondWrite.exceptionally(throwableSetter).get();
assertNotNull(throwable.get());
assertInstanceOf(StorageContainerException.class, throwable.get());
StorageContainerException sce = (StorageContainerException) throwable.get();
assertEquals(ContainerProtos.Result.CONTAINER_INTERNAL_ERROR, sce.getResult());
}
}