-
Notifications
You must be signed in to change notification settings - Fork 616
HDDS-12469. mark statemachine unhealthy for write operation timeout #8022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
a138456
93b9dc4
75bb186
d6a6a0f
71191ba
bf48067
b9dbb66
3b07686
2b37bbe
29da870
c743146
c5afb02
b8eae7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -187,13 +191,38 @@ long getStartTime() { | |
| } | ||
| } | ||
|
|
||
| static class WriteFutureContext { | ||
| private final Future<ContainerCommandResponseProto> writeChunkFuture; | ||
| private final CompletableFuture<Message> raftFuture; | ||
| private final long startTime; | ||
|
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; | ||
|
sumitagrawl marked this conversation as resolved.
Outdated
|
||
| private final long writeChunkWaitMaxMs; | ||
|
sumitagrawl marked this conversation as resolved.
Outdated
|
||
|
|
||
| // keeps track of the containers created per pipeline | ||
| private final Map<Long, Long> container2BCSIDMap; | ||
|
|
@@ -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( | ||
|
|
@@ -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 { | ||
|
|
@@ -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(); | ||
| } | ||
|
sumitagrawl marked this conversation as resolved.
Outdated
|
||
| try { | ||
| validateLongRunningWrite(); | ||
| } catch (IOException e) { | ||
|
sumitagrawl marked this conversation as resolved.
Outdated
|
||
| return completeExceptionally(e); | ||
| } | ||
| final WriteChunkRequestProto write = requestProto.getWriteChunk(); | ||
| RaftServer server = ratisServer.getServer(); | ||
| Preconditions.checkArgument(!write.getData().isEmpty()); | ||
|
|
@@ -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(() -> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why replacing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...
|
||
| 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; | ||
|
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 { | ||
|
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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 T2: scheduled as above
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; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| // validate for timeout in milli second | ||
| long waitTime = Time.monotonicNow() - writeFutureContextEntry.getValue().getStartTime() / 1000000; | ||
|
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 " | ||
|
sumitagrawl marked this conversation as resolved.
Outdated
|
||
| + writeChunkWaitMaxMs + " for groupId " + getGroupId(), ContainerProtos.Result.CONTAINER_INTERNAL_ERROR); | ||
| } | ||
| } | ||
|
|
||
| private StateMachine.DataChannel getStreamDataChannel( | ||
|
|
@@ -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)); | ||
|
sumitagrawl marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.