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

smengcl 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 896949610f9 HDDS-16425. Fail datanode startup cleanly on 
initialization errors (#11246)
896949610f9 is described below

commit 896949610f9bfabc1e445978566fffb90a8baad6
Author: Siyao Meng <[email protected]>
AuthorDate: Fri Sep 18 11:09:36 2026 -0700

    HDDS-16425. Fail datanode startup cleanly on initialization errors (#11246)
    
    Generated-by: Codex (GPT-6)
---
 .../common/statemachine/EndpointStateMachine.java  |   4 +-
 .../states/datanode/RunningDatanodeState.java      |  19 ++-
 .../states/endpoint/VersionEndpointTask.java       |  31 ++---
 .../ozone/container/ozoneimpl/OzoneContainer.java  |  49 ++++----
 .../container/common/TestDatanodeStateMachine.java |  91 +++++++++++++++
 .../states/datanode/TestRunningDatanodeState.java  | 127 +++++++++++++++++++--
 .../container/ozoneimpl/TestOzoneContainer.java    |  81 +++++++++++++
 7 files changed, 346 insertions(+), 56 deletions(-)

diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/EndpointStateMachine.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/EndpointStateMachine.java
index 60bf26a2dc0..8e98705cade 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/EndpointStateMachine.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/EndpointStateMachine.java
@@ -49,7 +49,9 @@ public class EndpointStateMachine
   private final HostAndPort hostAndPort;
   private final Lock lock;
   private final ConfigurationSource conf;
-  private EndPointStates state = EndPointStates.FIRST;
+  // RunningDatanodeState reads this without the endpoint lock and must see 
late SHUTDOWN transitions,
+  // even after its wait for the endpoint task has timed out.
+  private volatile EndPointStates state = EndPointStates.FIRST;
   private VersionResponse version;
   private ZonedDateTime lastSuccessfulHeartbeat;
   private boolean isPassive;
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
index 6b8ea712a93..fb7eac48db6 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
@@ -52,9 +52,7 @@ public class RunningDatanodeState implements DatanodeState {
   private final ConfigurationSource conf;
   private final StateContext context;
   private CompletionService<EndPointStates> ecs;
-  // Since we connectionManager endpoints can be changed by reconfiguration
-  // we should not rely on ConnectionManager#getValues being unchanged between
-  // execute and await
+  // Include tasks from earlier heartbeats whose completions have not been 
collected yet.
   private int executingEndpointCount = 0;
 
   public RunningDatanodeState(ConfigurationSource conf,
@@ -88,8 +86,10 @@ public void onExit() {
    */
   @Override
   public void execute(ExecutorService executor) {
-    ecs = new ExecutorCompletionService<>(executor);
-    executingEndpointCount = 0;
+    // Reuse the completion queue across heartbeats so results arriving after 
await() times out are not lost.
+    if (ecs == null) {
+      ecs = new ExecutorCompletionService<>(executor);
+    }
     for (EndpointStateMachine endpoint : connectionManager.getValues()) {
       Callable<EndPointStates> endpointTask = buildEndPointTask(endpoint);
       if (endpointTask != null) {
@@ -210,26 +210,21 @@ private Callable<EndPointStates> buildEndPointTask(
   public DatanodeStateMachine.DatanodeStates
       await(long duration, TimeUnit timeUnit)
       throws InterruptedException {
-    int returned = 0;
     long durationMS = timeUnit.toMillis(duration);
     long timeLeft = durationMS;
     long startTime = Time.monotonicNow();
     List<Future<EndPointStates>> results = new LinkedList<>();
 
-    while (returned < executingEndpointCount && timeLeft > 0) {
+    while (executingEndpointCount > 0 && timeLeft > 0) {
       Future<EndPointStates> result =
           ecs.poll(timeLeft, TimeUnit.MILLISECONDS);
       if (result != null) {
         results.add(result);
-        returned++;
+        executingEndpointCount--;
       }
       timeLeft = durationMS - (Time.monotonicNow() - startTime);
     }
     return computeNextContainerState(results);
   }
 
-  @Override
-  public void clear() {
-    ecs = null;
-  }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/VersionEndpointTask.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/VersionEndpointTask.java
index 40844c563e9..a1e9cbc3765 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/VersionEndpointTask.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/VersionEndpointTask.java
@@ -18,7 +18,6 @@
 package org.apache.hadoop.ozone.container.common.states.endpoint;
 
 import java.io.IOException;
-import java.net.BindException;
 import java.util.Objects;
 import java.util.concurrent.Callable;
 import org.apache.hadoop.hdds.conf.ConfigurationSource;
@@ -72,20 +71,27 @@ public EndpointStateMachine.EndPointStates call() throws 
Exception {
 
         if (!rpcEndPoint.isPassive()) {
           // If end point is passive, datanode does not need to check volumes.
-          String scmId = response.getValue(OzoneConsts.SCM_ID);
-          String clusterId = response.getValue(OzoneConsts.CLUSTER_ID);
+          try {
+            String scmId = response.getValue(OzoneConsts.SCM_ID);
+            String clusterId = response.getValue(OzoneConsts.CLUSTER_ID);
 
-          Objects.requireNonNull(scmId, "scmId == null");
-          Objects.requireNonNull(clusterId, "clusterId == null");
+            Objects.requireNonNull(scmId, "scmId == null");
+            Objects.requireNonNull(clusterId, "clusterId == null");
 
-          // Check DbVolumes, format DbVolume at first register time.
-          checkVolumeSet(ozoneContainer.getDbVolumeSet(), scmId, clusterId);
+            // Check DbVolumes, format DbVolume at first register time.
+            checkVolumeSet(ozoneContainer.getDbVolumeSet(), scmId, clusterId);
 
-          // Check HddsVolumes
-          checkVolumeSet(ozoneContainer.getVolumeSet(), scmId, clusterId);
+            // Check HddsVolumes
+            checkVolumeSet(ozoneContainer.getVolumeSet(), scmId, clusterId);
 
-          // Start the container services after getting the version information
-          ozoneContainer.start(clusterId);
+            // Start the container services after getting the version 
information
+            ozoneContainer.start(clusterId);
+          } catch (Exception | Error ex) {
+            // Handle this in the task: its caller may already have timed out 
waiting for startup.
+            LOG.error("Failed to start required container services for SCM {}. 
Shutting down datanode.",
+                rpcEndPoint.getAddress(), ex);
+            return 
rpcEndPoint.setState(EndpointStateMachine.EndPointStates.SHUTDOWN);
+          }
         }
         EndpointStateMachine.EndPointStates nextState =
             rpcEndPoint.getState().getNextState();
@@ -95,9 +101,8 @@ public EndpointStateMachine.EndPointStates call() throws 
Exception {
         LOG.debug("Cannot execute GetVersion task as endpoint state machine " +
             "is in {} state", rpcEndPoint.getState());
       }
-    } catch (DiskOutOfSpaceException | BindException ex) {
-      rpcEndPoint.setState(EndpointStateMachine.EndPointStates.SHUTDOWN);
     } catch (IOException ex) {
+      // Communication failures are retryable; local initialization failures 
are handled above.
       rpcEndPoint.logIfNeeded(ex);
     } finally {
       rpcEndPoint.unlock();
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
index 08d5c18c1d1..ff1fedaa6ac 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
@@ -51,7 +51,6 @@
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
 import org.apache.hadoop.hdds.HddsConfigKeys;
 import org.apache.hadoop.hdds.conf.ConfigurationSource;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -147,7 +146,10 @@ public class OzoneContainer {
       recoveringContainerScrubbingService;
   private final GrpcTlsConfig tlsClientConfig;
   private DiskBalancerService diskBalancerService;
-  private final AtomicReference<InitializingStatus> initializingStatus;
+  private final Object initializationLock = new Object();
+  // Guarded by initializationLock.
+  private InitializingStatus initializingStatus;
+  private Throwable initializationFailure;
   private final ReplicationServer replicationServer;
   private DatanodeDetails datanodeDetails;
   private StateContext context;
@@ -160,7 +162,7 @@ public class OzoneContainer {
   private final DatanodeStorageMetrics datanodeStorageMetrics;
 
   enum InitializingStatus {
-    UNINITIALIZED, INITIALIZING, INITIALIZED
+    UNINITIALIZED, INITIALIZING, INITIALIZED, FAILED
   }
 
   /**
@@ -334,7 +336,7 @@ public OzoneContainer(HddsDatanodeService 
hddsDatanodeService,
 
     datanodeStorageMetrics = DatanodeStorageMetrics.create(volumeSet);
 
-    initializingStatus = new 
AtomicReference<>(InitializingStatus.UNINITIALIZED);
+    initializingStatus = InitializingStatus.UNINITIALIZED;
   }
 
   /**
@@ -547,24 +549,30 @@ public OnDemandContainerScanner getOnDemandScanner() {
    * @throws IOException
    */
   public void start(String clusterId) throws IOException {
-    // If SCM HA is enabled, OzoneContainer#start() will be called multi-times
-    // from VersionEndpointTask. The first call should do the initializing job,
-    // the successive calls should wait until OzoneContainer is initialized.
-    if (!initializingStatus.compareAndSet(
-        InitializingStatus.UNINITIALIZED, InitializingStatus.INITIALIZING)) {
-
-      // wait OzoneContainer to finish its initializing.
-      while (initializingStatus.get() != InitializingStatus.INITIALIZED) {
-        try {
-          Thread.sleep(1);
-        } catch (InterruptedException e) {
-          Thread.currentThread().interrupt();
-        }
+    synchronized (initializationLock) {
+      // SCM endpoints share one initialization attempt, including its failure.
+      if (initializingStatus == InitializingStatus.INITIALIZED) {
+        LOG.info("Ignore. OzoneContainer already started.");
+        return;
+      }
+      if (initializingStatus == InitializingStatus.FAILED) {
+        throw new IOException("OzoneContainer initialization previously 
failed", initializationFailure);
+      }
+
+      initializingStatus = InitializingStatus.INITIALIZING;
+      try {
+        initializeContainerServices(clusterId);
+        initializingStatus = InitializingStatus.INITIALIZED;
+      } catch (IOException | RuntimeException | Error ex) {
+        // Partially started services cannot safely be initialized again.
+        initializationFailure = ex;
+        initializingStatus = InitializingStatus.FAILED;
+        throw ex;
       }
-      LOG.info("Ignore. OzoneContainer already started.");
-      return;
     }
+  }
 
+  private void initializeContainerServices(String clusterId) throws 
IOException {
     DatanodeLayoutStorage layoutStorage
         = new DatanodeLayoutStorage(config);
     layoutStorage.setClusterId(clusterId);
@@ -607,9 +615,6 @@ public void start(String clusterId) throws IOException {
     recoveringContainerScrubbingService.start();
 
     initHddsVolumeContainer();
-
-    // mark OzoneContainer as INITIALIZED.
-    initializingStatus.set(InitializingStatus.INITIALIZED);
   }
 
   /**
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/TestDatanodeStateMachine.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/TestDatanodeStateMachine.java
index 721f50bec13..ef03aa148df 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/TestDatanodeStateMachine.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/TestDatanodeStateMachine.java
@@ -18,44 +18,65 @@
 package org.apache.hadoop.ozone.container.common;
 
 import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HEARTBEAT_RPC_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.Socket;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.conf.ReconfigurationHandler;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.scm.ScmConfigKeys;
 import org.apache.hadoop.hdds.upgrade.HDDSLayoutFeature;
 import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ozone.HddsDatanodeStopService;
 import org.apache.hadoop.ozone.OzoneConfigKeys;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.container.common.helpers.ContainerUtils;
 import 
org.apache.hadoop.ozone.container.common.interfaces.VolumeChoosingPolicy;
 import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine;
+import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine.DatanodeStates;
 import 
org.apache.hadoop.ozone.container.common.statemachine.EndpointStateMachine;
 import 
org.apache.hadoop.ozone.container.common.statemachine.SCMConnectionManager;
 import org.apache.hadoop.ozone.container.common.states.DatanodeState;
 import 
org.apache.hadoop.ozone.container.common.states.datanode.InitDatanodeState;
 import 
org.apache.hadoop.ozone.container.common.states.datanode.RunningDatanodeState;
+import 
org.apache.hadoop.ozone.container.common.states.endpoint.VersionEndpointTask;
+import 
org.apache.hadoop.ozone.container.common.transport.server.XceiverServerSpi;
 import 
org.apache.hadoop.ozone.container.common.volume.CapacityVolumeChoosingPolicy;
+import org.apache.hadoop.ozone.container.ozoneimpl.OzoneContainer;
+import 
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig;
 import org.apache.hadoop.util.concurrent.HadoopExecutors;
 import org.apache.ozone.test.GenericTestUtils;
+import org.apache.ozone.test.GenericTestUtils.LogCapturer;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.io.TempDir;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -149,6 +170,76 @@ public void testStartStopDatanodeStateMachine() throws 
IOException,
     }
   }
 
+  @Test
+  @Timeout(60)
+  void testDelayedRatisStartupFailureStopsDatanode() throws Exception {
+    conf.setFromObject(conf.getObject(ReplicationConfig.class).setPort(0));
+    DatanodeDetails datanodeDetails = getNewDatanodeDetails();
+    ContainerTestUtils.initializeDatanodeLayout(conf, datanodeDetails);
+    CountDownLatch initializing = new CountDownLatch(1);
+    CountDownLatch release = new CountDownLatch(1);
+    CountDownLatch shutdown = new CountDownLatch(1);
+    HddsDatanodeStopService stopService = mock(HddsDatanodeStopService.class);
+    doAnswer(invocation -> {
+      shutdown.countDown();
+      return null;
+    }).when(stopService).stopService();
+    DatanodeStateMachine stateMachine = new DatanodeStateMachine(null, 
datanodeDetails, conf, null, null,
+        stopService, new ReconfigurationHandler("DN", conf, op -> { }));
+    try (LogCapturer startupLogs = 
LogCapturer.captureLogs(VersionEndpointTask.class);
+         LogCapturer stateLogs = 
LogCapturer.captureLogs(RunningDatanodeState.class)) {
+      OzoneContainer container = stateMachine.getContainer();
+      XceiverServerSpi writeChannel = spy(container.getWriteChannel());
+      Field writeChannelField = 
OzoneContainer.class.getDeclaredField("writeChannel");
+      writeChannelField.setAccessible(true);
+      writeChannelField.set(container, writeChannel);
+      IllegalStateException failure = new IllegalStateException("Failed to 
initRaftLog",
+          new IOException("Corrupt Raft log"));
+      doAnswer(invocation -> {
+        initializing.countDown();
+        assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+        throw failure;
+      }).when(writeChannel).start();
+
+      stateMachine.startDaemon();
+      assertThat(initializing.await(10, TimeUnit.SECONDS)).isTrue();
+      // Replication is already serving when Ratis startup fails.
+      try (Socket socket = new Socket("127.0.0.1", 
container.getReplicationServer().getPort())) {
+        assertThat(socket.isConnected()).isTrue();
+      }
+      String clusterId = mockServers.get(0).getClusterId();
+      CompletableFuture<Thread> waitingThread = new CompletableFuture<>();
+      Future<?> waitingCaller = executorService.submit(() -> {
+        waitingThread.complete(Thread.currentThread());
+        container.start(clusterId);
+        return null;
+      });
+      Thread caller = waitingThread.get(10, TimeUnit.SECONDS);
+      GenericTestUtils.waitFor(() -> caller.getState() == 
Thread.State.BLOCKED, 10, 10000);
+      // Let the real heartbeat wait expire before completing the 
initialization attempt.
+      GenericTestUtils.waitFor(() -> stateLogs.getOutput().contains("Detected 
timeout"), 10, 10000);
+      
assertThat(stateMachine.getContext().getState()).isEqualTo(DatanodeStates.RUNNING);
+      assertThat(shutdown.getCount()).isEqualTo(1);
+      release.countDown();
+
+      assertThat(assertThrows(ExecutionException.class, () -> 
waitingCaller.get(10, TimeUnit.SECONDS)).getCause())
+          .isInstanceOf(IOException.class).hasCause(failure);
+      assertThat(shutdown.await(10, TimeUnit.SECONDS)).isTrue();
+      stateMachine.getStateMachineThread().join(10000);
+      assertThat(stateMachine.getStateMachineThread().isAlive()).isFalse();
+      
assertThat(stateMachine.getContext().getState()).isEqualTo(DatanodeStates.SHUTDOWN);
+      assertThat(stateMachine.getContext().getShutdownOnError()).isTrue();
+      assertThat(startupLogs.getOutput()).contains("Failed to start required 
container services",
+          "java.lang.IllegalStateException: Failed to initRaftLog", "Caused 
by: java.io.IOException: Corrupt Raft log");
+      assertThat(assertThrows(IOException.class, () -> 
container.start(clusterId))).hasCause(failure);
+      verify(writeChannel, times(1)).start();
+      verify(stopService, times(1)).stopService();
+    } finally {
+      release.countDown();
+      stateMachine.stopDaemon();
+    }
+  }
+
   /**
    * This test explores the state machine by invoking each call in sequence 
just
    * like as if the state machine would call it. Because this is a test we are
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/datanode/TestRunningDatanodeState.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/datanode/TestRunningDatanodeState.java
index 51390ae632b..459dc663c27 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/datanode/TestRunningDatanodeState.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/datanode/TestRunningDatanodeState.java
@@ -19,20 +19,41 @@
 
 import static 
org.apache.hadoop.ozone.container.common.statemachine.EndpointStateMachine.EndPointStates.SHUTDOWN;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorCompletionService;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.scm.net.HostAndPort;
+import org.apache.hadoop.ozone.OzoneConsts;
+import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine;
+import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine.DatanodeStates;
 import 
org.apache.hadoop.ozone.container.common.statemachine.EndpointStateMachine;
+import 
org.apache.hadoop.ozone.container.common.statemachine.EndpointStateMachine.EndPointStates;
 import 
org.apache.hadoop.ozone.container.common.statemachine.SCMConnectionManager;
+import org.apache.hadoop.ozone.container.common.statemachine.StateContext;
+import 
org.apache.hadoop.ozone.container.common.states.endpoint.VersionEndpointTask;
+import org.apache.hadoop.ozone.container.ozoneimpl.OzoneContainer;
+import org.apache.hadoop.ozone.protocol.VersionResponse;
+import 
org.apache.hadoop.ozone.protocolPB.StorageContainerDatanodeProtocolClientSideTranslatorPB;
 import org.apache.hadoop.util.Time;
+import org.apache.ozone.test.GenericTestUtils.LogCapturer;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * Test class for RunningDatanodeState.
@@ -71,20 +92,110 @@ public void testAwait() throws InterruptedException {
     long endTime = Time.monotonicNow();
     assertThat(endTime - startTime).isGreaterThanOrEqualTo(500);
 
+    // The next heartbeat must still collect old completions, even if 
endpoints were removed.
+    state.clear();
+    stateMachines.clear();
+    state.execute(executorService);
     futureOne.complete(SHUTDOWN);
 
-    CompletableFuture<EndpointStateMachine.EndPointStates> futureTwo =
-        new CompletableFuture<>();
-    for (int i = 0; i < threadPoolSize; i++) {
-      ecs.submit(() -> futureTwo.get());
-    }
-    futureTwo.complete(SHUTDOWN);
-
     startTime = Time.monotonicNow();
-    state.await(500, TimeUnit.MILLISECONDS);
+    assertThat(state.await(500, 
TimeUnit.MILLISECONDS)).isEqualTo(DatanodeStates.SHUTDOWN);
     endTime = Time.monotonicNow();
     assertThat(endTime - startTime).isLessThan(500);
 
     executorService.shutdown();
   }
+
+  @ParameterizedTest
+  @ValueSource(ints = {0, 1, 2, 3})
+  void testStartupCompletesAfterHeartbeatTimeout(int failureType) throws 
Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    OzoneContainer container = mock(OzoneContainer.class);
+    DatanodeStateMachine datanode = mock(DatanodeStateMachine.class);
+    SCMConnectionManager connections = mock(SCMConnectionManager.class);
+    when(datanode.getConnectionManager()).thenReturn(connections);
+    when(datanode.getContainer()).thenReturn(container);
+    StateContext context = new StateContext(conf, DatanodeStates.RUNNING, 
datanode, "");
+    StorageContainerDatanodeProtocolClientSideTranslatorPB scm =
+        mock(StorageContainerDatanodeProtocolClientSideTranslatorPB.class);
+    
when(scm.getVersion(null)).thenReturn(versionResponse().getProtobufMessage());
+    CountDownLatch initializing = new CountDownLatch(1);
+    CountDownLatch release = new CountDownLatch(1);
+    Throwable failure = failureType == 1 ? new IOException("Corrupt Raft log")
+        : failureType == 2 ? new IllegalStateException("Failed to 
initRaftLog", new IOException("Corrupt Raft log"))
+        : new AssertionError("Failed to initRaftLog", new IOException("Corrupt 
Raft log"));
+    doAnswer(invocation -> {
+      initializing.countDown();
+      assertThat(release.await(10, TimeUnit.SECONDS)).isTrue();
+      if (failureType != 0) {
+        throw failure;
+      }
+      return null;
+    }).when(container).start("cluster");
+    ExecutorService executor = Executors.newCachedThreadPool();
+    try (LogCapturer logs = LogCapturer.captureLogs(VersionEndpointTask.class);
+         EndpointStateMachine endpoint = new EndpointStateMachine(new 
HostAndPort("scm", 9861), scm, conf, "")) {
+      
when(connections.getValues()).thenReturn(Collections.singletonList(endpoint));
+      // Keep startup blocked until both the inner future wait and this 
heartbeat have finished.
+      context.execute(executor, 5, TimeUnit.SECONDS);
+      assertThat(initializing.getCount()).isZero();
+      assertThat(context.getState()).isEqualTo(DatanodeStates.RUNNING);
+      assertThat(endpoint.getState()).isEqualTo(EndPointStates.GETVERSION);
+      release.countDown();
+      // The endpoint executor is serial; this barrier waits for the startup 
task to finish.
+      endpoint.getExecutorService().submit(() -> { }).get(5, TimeUnit.SECONDS);
+      if (failureType == 0) {
+        assertThat(endpoint.getState()).isEqualTo(EndPointStates.REGISTER);
+        assertThat(context.getShutdownOnError()).isFalse();
+      } else {
+        assertThat(endpoint.getState()).isEqualTo(SHUTDOWN);
+        assertThat(logs.getOutput()).contains("Failed to start required 
container services", "scm:9861",
+            failure.getClass().getName(), "Corrupt Raft log");
+        context.execute(executor, 5, TimeUnit.SECONDS);
+        assertThat(context.getState()).isEqualTo(DatanodeStates.SHUTDOWN);
+        assertThat(context.getShutdownOnError()).isTrue();
+      }
+      verify(container, times(1)).start("cluster");
+    } finally {
+      release.countDown();
+      executor.shutdownNow();
+    }
+  }
+
+  @Test
+  void testUnavailableScmDoesNotPreventStartup() throws Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    OzoneContainer container = mock(OzoneContainer.class);
+    DatanodeStateMachine datanode = mock(DatanodeStateMachine.class);
+    SCMConnectionManager connections = mock(SCMConnectionManager.class);
+    when(datanode.getConnectionManager()).thenReturn(connections);
+    when(datanode.getContainer()).thenReturn(container);
+    StateContext context = new StateContext(conf, DatanodeStates.RUNNING, 
datanode, "");
+    StorageContainerDatanodeProtocolClientSideTranslatorPB unavailable =
+        mock(StorageContainerDatanodeProtocolClientSideTranslatorPB.class);
+    when(unavailable.getVersion(null)).thenThrow(new IOException("SCM 
unavailable"));
+    StorageContainerDatanodeProtocolClientSideTranslatorPB available =
+        mock(StorageContainerDatanodeProtocolClientSideTranslatorPB.class);
+    
when(available.getVersion(null)).thenReturn(versionResponse().getProtobufMessage());
+    ExecutorService executor = Executors.newCachedThreadPool();
+    try (EndpointStateMachine first = new EndpointStateMachine(new 
HostAndPort("scm1", 9861), unavailable, conf, "");
+         EndpointStateMachine second = new EndpointStateMachine(new 
HostAndPort("scm2", 9861), available, conf, "")) {
+      when(connections.getValues()).thenReturn(Arrays.asList(first, second));
+      context.execute(executor, 5, TimeUnit.SECONDS);
+      assertThat(first.getState()).isEqualTo(EndPointStates.GETVERSION);
+      assertThat(second.getState()).isEqualTo(EndPointStates.REGISTER);
+      assertThat(context.getState()).isEqualTo(DatanodeStates.RUNNING);
+      assertThat(context.getShutdownOnError()).isFalse();
+      verify(container, times(1)).start("cluster");
+    } finally {
+      executor.shutdownNow();
+    }
+  }
+
+  private static VersionResponse versionResponse() {
+    return VersionResponse.newBuilder().setVersion(1)
+        .addValue(OzoneConsts.SCM_ID, "scm")
+        .addValue(OzoneConsts.CLUSTER_ID, "cluster")
+        .build();
+  }
 }
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainer.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainer.java
index 91c3f8ed58c..9f8f9fe4604 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainer.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainer.java
@@ -19,15 +19,21 @@
 
 import static 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Result.DISK_OUT_OF_SPACE;
 import static 
org.apache.hadoop.ozone.container.common.ContainerTestUtils.createDbInstancesForTestIfNeeded;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import java.io.File;
+import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -39,6 +45,12 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.conf.StorageUnit;
 import org.apache.hadoop.hdds.HddsConfigKeys;
@@ -51,11 +63,15 @@
 import org.apache.hadoop.hdds.utils.db.Table;
 import org.apache.hadoop.ozone.OzoneConfigKeys;
 import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
+import org.apache.hadoop.ozone.container.common.SCMTestUtils;
 import org.apache.hadoop.ozone.container.common.helpers.BlockData;
 import org.apache.hadoop.ozone.container.common.helpers.ChunkInfo;
 import org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion;
 import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
 import org.apache.hadoop.ozone.container.common.interfaces.DBHandle;
+import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine;
+import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine.DatanodeStates;
+import org.apache.hadoop.ozone.container.common.statemachine.StateContext;
 import org.apache.hadoop.ozone.container.common.utils.StorageVolumeUtil;
 import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
 import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
@@ -67,6 +83,8 @@
 import org.apache.hadoop.ozone.container.keyvalue.helpers.BlockUtils;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * This class is used to test OzoneContainer.
@@ -116,6 +134,69 @@ public void cleanUp() {
     }
   }
 
+  @ParameterizedTest
+  @ValueSource(ints = {0, 1, 2, 3})
+  void testConcurrentStartup(int failureType) throws Exception {
+    conf = SCMTestUtils.getConf(folder.toFile());
+    conf.setBoolean(OzoneConfigKeys.HDDS_CONTAINER_RATIS_IPC_RANDOM_PORT, 
true);
+    conf.setBoolean(OzoneConfigKeys.HDDS_CONTAINER_IPC_RANDOM_PORT, true);
+    ContainerTestUtils.initializeDatanodeLayout(conf, datanodeDetails);
+    OzoneContainer container = 
spy(ContainerTestUtils.getOzoneContainer(datanodeDetails, conf));
+    CountDownLatch initializing = new CountDownLatch(1);
+    CountDownLatch release = new CountDownLatch(1);
+    CountDownLatch secondCaller = new CountDownLatch(1);
+    Throwable failure = failureType == 1 ? new IOException("Cannot read 
container metadata")
+        : failureType == 2 ? new IllegalStateException("Failed to 
initRaftLog", new IOException("Corrupt Raft log"))
+        : new AssertionError("Failed to initRaftLog", new IOException("Corrupt 
Raft log"));
+    ExecutorService executor = Executors.newFixedThreadPool(2,
+        new ThreadFactoryBuilder().setDaemon(true).build());
+    try {
+      doAnswer(invocation -> {
+        initializing.countDown();
+        assertThat(release.await(10, TimeUnit.SECONDS)).isTrue();
+        if (failureType != 0) {
+          throw failure;
+        }
+        return invocation.callRealMethod();
+      }).when(container).buildContainerSet();
+      Future<?> first = executor.submit(() -> {
+        container.start(clusterId);
+        return null;
+      });
+      assertThat(initializing.await(10, TimeUnit.SECONDS)).isTrue();
+      // Full reports use the container monitor and must not wait for local 
service startup.
+      DatanodeStateMachine datanode = mock(DatanodeStateMachine.class);
+      when(datanode.getContainer()).thenReturn(container);
+      StateContext reportContext = new StateContext(conf, 
DatanodeStates.RUNNING, datanode, "");
+      
assertThat(executor.submit(reportContext::getFullContainerReportDiscardPendingICR)
+          .get(5, TimeUnit.SECONDS).getReportsCount()).isZero();
+      Future<?> second = executor.submit(() -> {
+        secondCaller.countDown();
+        container.start(clusterId);
+        return null;
+      });
+      assertThat(secondCaller.await(10, TimeUnit.SECONDS)).isTrue();
+      assertThat(second.isDone()).isFalse();
+      release.countDown();
+      if (failureType == 0) {
+        first.get(10, TimeUnit.SECONDS);
+        second.get(10, TimeUnit.SECONDS);
+        container.start(clusterId);
+      } else {
+        assertThat(assertThrows(ExecutionException.class, () -> first.get(10, 
TimeUnit.SECONDS)))
+            .hasCause(failure);
+        assertThat(assertThrows(ExecutionException.class, () -> second.get(10, 
TimeUnit.SECONDS)).getCause())
+            .isInstanceOf(IOException.class).hasCause(failure);
+        assertThat(assertThrows(IOException.class, () -> 
container.start(clusterId))).hasCause(failure);
+      }
+      verify(container, times(1)).buildContainerSet();
+    } finally {
+      release.countDown();
+      executor.shutdownNow();
+      container.stop();
+    }
+  }
+
   /**
    * Create a mock {@link HddsVolume} to track container IDs.
    */


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

Reply via email to