From 6e9b0a9abfdd3f713c60db67c990719d34564eab Mon Sep 17 00:00:00 2001 From: Ritesh H Shukla Date: Fri, 26 Jun 2026 11:41:51 +0530 Subject: [PATCH 1/2] HDDS-15682. Address review nits: null-safe SCMNodeInfo getters, document HostAndPort host-spelling From the AI-assisted review of #10612: - SCMNodeInfo block/client/security/datanode getters NPE on null fields; the prior String getters returned null gracefully, so restore that contract. - Document the host-spelling contract on the InetSocketAddress-derived HostAndPort constructor so equal-by-host instances do not miss each other in the endpoint maps. Generated-by: Claude Code (AI-assisted) --- .../java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java | 8 ++++---- .../java/org/apache/hadoop/hdds/scm/net/HostAndPort.java | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java index 0006b2762d40..1faa834f638b 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/ha/SCMNodeInfo.java @@ -229,15 +229,15 @@ public String getNodeId() { } public String getBlockClientAddress() { - return blockClientAddress.getHostAndPortString(); + return blockClientAddress == null ? null : blockClientAddress.getHostAndPortString(); } public String getScmClientAddress() { - return scmClientAddress.getHostAndPortString(); + return scmClientAddress == null ? null : scmClientAddress.getHostAndPortString(); } public String getScmSecurityAddress() { - return scmSecurityAddress.getHostAndPortString(); + return scmSecurityAddress == null ? null : scmSecurityAddress.getHostAndPortString(); } public HostAndPort getScmDatanodeHostPortAddress() { @@ -245,6 +245,6 @@ public HostAndPort getScmDatanodeHostPortAddress() { } public String getScmDatanodeAddress() { - return scmDatanodeAddress.getHostAndPortString(); + return scmDatanodeAddress == null ? null : scmDatanodeAddress.getHostAndPortString(); } } diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java index c56776d74b55..0be0e087cba0 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java @@ -44,6 +44,12 @@ public HostAndPort(String host, int port) { this(host, port, null); } + /** + * Derives the host from {@code address.getHostName()}, which can differ from the configured + * spelling used by {@link #HostAndPort(String, int)} (IP literal vs hostname). Since equals/hashCode + * key on the host string, callers must use one consistent spelling per node or instances will not + * match in the endpoint maps. + */ public HostAndPort(InetSocketAddress address) { this(address.getHostName(), address.getPort(), address); } From a18d256aa0993850c1946e094d5810e7acb05f5b Mon Sep 17 00:00:00 2001 From: Ritesh H Shukla Date: Fri, 26 Jun 2026 11:55:38 +0530 Subject: [PATCH 2/2] HDDS-15533. DN->SCM DNS refresh on heartbeat failure (on the HostAndPort identity model) Ports the DN->SCM DNS-refresh feature onto HDDS-15682's host:port identity. Because SCM nodes are now keyed by the stable host:port, this drops the endpoint re-key dance, the resolved-address collision check, and the StateContext queue migration that the InetSocketAddress-keyed version (#10488) needed. - HostAndPort.refresh(): re-resolve the host:port and swap the cached address if the IP changed (DNS lookup outside the monitor; only the swap synchronized). Completes the mutable-address hook. - SCMConnectionManager.refreshSCMServer(): on a changed IP, rebuild the endpoint value under the same key and close the stale proxy. buildScmEndpoint() factored out of addSCMServer. - HeartbeatEndpointTask: on a connection-class heartbeat failure, gated by the resolve-needed flag and the new hdds.heartbeat.address.refresh.threshold knob, ask the connection manager to refresh. Generated-by: Claude Code (AI-assisted) --- .../apache/hadoop/hdds/HddsConfigKeys.java | 7 + .../hadoop/hdds/scm/net/HostAndPort.java | 21 +++ .../src/main/resources/ozone-default.xml | 13 ++ .../statemachine/SCMConnectionManager.java | 95 ++++++++---- .../endpoint/HeartbeatEndpointTask.java | 33 +++++ .../TestHeartbeatEndpointTaskDnsRefresh.java | 136 ++++++++++++++++++ 6 files changed, 278 insertions(+), 27 deletions(-) create mode 100644 hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTaskDnsRefresh.java diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java index 8e9d4e9e098b..973d2c654c0b 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java @@ -39,6 +39,13 @@ public final class HddsConfigKeys { "hdds.heartbeat.recon.initial-interval"; public static final String HDDS_RECON_INITIAL_HEARTBEAT_INTERVAL_DEFAULT = "2s"; + /** + * Number of consecutive heartbeat failures the DN tolerates against the same SCM endpoint before + * re-resolving its hostname. Only consulted when ozone.client.failover.resolve-needed is true. + */ + public static final String HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD = + "hdds.heartbeat.address.refresh.threshold"; + public static final int HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD_DEFAULT = 3; public static final String HDDS_NODE_REPORT_INTERVAL = "hdds.node.report.interval"; public static final String HDDS_NODE_REPORT_INTERVAL_DEFAULT = diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java index 0be0e087cba0..55500bae553a 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java @@ -17,6 +17,7 @@ package org.apache.hadoop.hdds.scm.net; +import java.net.InetAddress; import java.net.InetSocketAddress; import org.apache.hadoop.net.NetUtils; @@ -70,6 +71,26 @@ public synchronized InetSocketAddress getAddress() { return address; } + /** + * Re-resolves the host:port and swaps the cached address if the IP changed. The DNS lookup runs + * outside the monitor; only the swap is synchronized, mirroring {@link #getAddress()}. + * @return true if the cached address changed + */ + public boolean refresh() { + final InetSocketAddress latest = NetUtils.createSocketAddr(hostAndPortString); + final InetAddress latestIp = latest.getAddress(); + if (latestIp == null) { + return false; + } + synchronized (this) { + if (latestIp.equals(address.getAddress())) { + return false; + } + address = latest; + return true; + } + } + @Override public int hashCode() { return hash; diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml index dd33a02b02a7..8ad5b0f8796b 100644 --- a/hadoop-hdds/common/src/main/resources/ozone-default.xml +++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml @@ -3964,6 +3964,19 @@ + + hdds.heartbeat.address.refresh.threshold + 3 + OZONE, DATANODE, HA + Number of consecutive heartbeat failures the DataNode + tolerates against the same SCM endpoint before re-resolving its + hostname. Only consulted when ozone.client.failover.resolve-needed + is true. Conservative default avoids re-resolution on transient + blips while still recovering from a peer pod IP change within + seconds. + + + ozone.directory.deleting.service.interval 1m diff --git a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/SCMConnectionManager.java b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/SCMConnectionManager.java index 25be82cd0414..643053df07cc 100644 --- a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/SCMConnectionManager.java +++ b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/SCMConnectionManager.java @@ -139,33 +139,7 @@ public void addSCMServer(HostAndPort address, "Ignoring the request."); return; } - - Configuration hadoopConfig = - LegacyHadoopConfigurationSource.asHadoopConfiguration(this.conf); - RPC.setProtocolEngine( - hadoopConfig, - StorageContainerDatanodeProtocolPB.class, - ProtobufRpcEngine.class); - long version = - RPC.getProtocolVersion(StorageContainerDatanodeProtocolPB.class); - - RetryPolicy retryPolicy = - RetryPolicies.retryUpToMaximumCountWithFixedSleep( - getScmRpcRetryCount(conf), getScmRpcRetryInterval(conf), - TimeUnit.MILLISECONDS); - - StorageContainerDatanodeProtocolPB rpcProxy = RPC.getProtocolProxy( - StorageContainerDatanodeProtocolPB.class, version, - address.getAddress(), UserGroupInformation.getCurrentUser(), hadoopConfig, - NetUtils.getDefaultSocketFactory(hadoopConfig), getRpcTimeout(), - retryPolicy).getProxy(); - - StorageContainerDatanodeProtocolClientSideTranslatorPB rpcClient = - new StorageContainerDatanodeProtocolClientSideTranslatorPB( - rpcProxy); - - EndpointStateMachine endPoint = new EndpointStateMachine(address, - rpcClient, this.conf, threadNamePrefix); + EndpointStateMachine endPoint = buildScmEndpoint(address, threadNamePrefix); endPoint.setPassive(false); scmMachines.put(address, endPoint); } finally { @@ -173,6 +147,73 @@ public void addSCMServer(HostAndPort address, } } + /** + * Builds (but does not register) an active-SCM endpoint dialing {@code address.getAddress()}. + * Factored out so {@link #refreshSCMServer} can rebuild an endpoint on a freshly-resolved IP. + */ + private EndpointStateMachine buildScmEndpoint(HostAndPort address, String threadNamePrefix) + throws IOException { + Configuration hadoopConfig = + LegacyHadoopConfigurationSource.asHadoopConfiguration(this.conf); + RPC.setProtocolEngine(hadoopConfig, StorageContainerDatanodeProtocolPB.class, + ProtobufRpcEngine.class); + long version = RPC.getProtocolVersion(StorageContainerDatanodeProtocolPB.class); + RetryPolicy retryPolicy = RetryPolicies.retryUpToMaximumCountWithFixedSleep( + getScmRpcRetryCount(conf), getScmRpcRetryInterval(conf), TimeUnit.MILLISECONDS); + StorageContainerDatanodeProtocolPB rpcProxy = RPC.getProtocolProxy( + StorageContainerDatanodeProtocolPB.class, version, + address.getAddress(), UserGroupInformation.getCurrentUser(), hadoopConfig, + NetUtils.getDefaultSocketFactory(hadoopConfig), getRpcTimeout(), + retryPolicy).getProxy(); + StorageContainerDatanodeProtocolClientSideTranslatorPB rpcClient = + new StorageContainerDatanodeProtocolClientSideTranslatorPB(rpcProxy); + return new EndpointStateMachine(address, rpcClient, this.conf, threadNamePrefix); + } + + /** + * Re-resolves the hostname of the active SCM endpoint at {@code address} and, if the IP changed, + * rebuilds the endpoint in place under the same key. Because the key is the stable host:port, no + * re-keying or StateContext queue migration is needed -- only the {@link EndpointStateMachine} + * value (which holds the frozen RPC proxy) is replaced. + * + * @return true if the endpoint was rebuilt on a new address + */ + public boolean refreshSCMServer(HostAndPort address, String threadNamePrefix) + throws IOException { + final EndpointStateMachine current; + readLock(); + try { + current = scmMachines.get(address); + if (current == null || current.isPassive()) { + return false; + } + } finally { + readUnlock(); + } + // DNS lookup runs outside the map lock; HostAndPort swaps its cached address only on a change. + if (!address.refresh()) { + return false; + } + final EndpointStateMachine stale; + writeLock(); + try { + if (scmMachines.get(address) != current) { + // Lost the race: another refresh or removeSCMServer replaced this entry. Abandon. + return false; + } + EndpointStateMachine rebuilt = buildScmEndpoint(address, threadNamePrefix); + rebuilt.setPassive(false); + scmMachines.put(address, rebuilt); + stale = current; + } finally { + writeUnlock(); + } + // Close the stale proxy outside the lock; teardown can block on socket close. + stale.close(); + LOG.info("Re-resolved SCM endpoint {} after connection failure.", address); + return true; + } + /** * Adds a new Recon server to the set of endpoints. * diff --git a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java index 7cb24558c7cb..497d0042ea0f 100644 --- a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java +++ b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java @@ -19,8 +19,12 @@ import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_ACTION_MAX_LIMIT; import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_ACTION_MAX_LIMIT_DEFAULT; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD_DEFAULT; import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_ACTION_MAX_LIMIT; import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_ACTION_MAX_LIMIT_DEFAULT; +import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_DEFAULT; +import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY; import static org.apache.hadoop.ozone.container.upgrade.UpgradeUtils.toLayoutVersionProto; import com.google.common.base.Preconditions; @@ -44,6 +48,7 @@ import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.SCMHeartbeatRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.SCMHeartbeatResponseProto; import org.apache.hadoop.hdds.upgrade.HDDSLayoutVersionManager; +import org.apache.hadoop.hdds.utils.ConnectionFailureUtils; import org.apache.hadoop.hdfs.util.EnumCounters; import org.apache.hadoop.ozone.container.common.helpers.DeletedContainerBlocksSummary; import org.apache.hadoop.ozone.container.common.statemachine.EndpointStateMachine; @@ -77,6 +82,8 @@ public class HeartbeatEndpointTask private int maxContainerActionsPerHB; private int maxPipelineActionsPerHB; private HDDSLayoutVersionManager layoutVersionManager; + private final boolean resolveOnFailureEnabled; + private final int refreshThreshold; /** * Constructs a SCM heart beat. @@ -100,6 +107,10 @@ public HeartbeatEndpointTask(EndpointStateMachine rpcEndpoint, } else { this.layoutVersionManager = context.getParent().getLayoutVersionManager(); } + this.resolveOnFailureEnabled = conf.getBoolean(OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY, + OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_DEFAULT); + this.refreshThreshold = Math.max(1, conf.getInt(HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD, + HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD_DEFAULT)); } /** @@ -157,12 +168,34 @@ public EndpointStateMachine.EndPointStates call() throws Exception { // put back the reports which failed to be sent putBackIncrementalReports(requestBuilder); rpcEndpoint.logIfNeeded(ex); + maybeRefreshScmAddress(ex); } finally { rpcEndpoint.unlock(); } return rpcEndpoint.getState(); } + /** + * On a connection-class heartbeat failure, once the missed-count threshold is reached and DNS + * refresh is enabled, ask the connection manager to re-resolve this SCM peer and rebuild the + * endpoint if its IP changed. Off by default (ozone.client.failover.resolve-needed). + */ + private void maybeRefreshScmAddress(IOException heartbeatFailure) { + if (!resolveOnFailureEnabled + || rpcEndpoint.isPassive() + || rpcEndpoint.getMissedCount() < refreshThreshold + || !ConnectionFailureUtils.isConnectionFailure(heartbeatFailure)) { + return; + } + try { + context.getParent().getConnectionManager() + .refreshSCMServer(rpcEndpoint.getAddress(), context.getThreadNamePrefix()); + } catch (IOException ex) { + LOG.warn("Failed to refresh SCM address {} after {} missed heartbeats", + rpcEndpoint.getAddress(), rpcEndpoint.getMissedCount(), ex); + } + } + // TODO: Make it generic. private void putBackIncrementalReports( SCMHeartbeatRequestProto.Builder requestBuilder) { diff --git a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTaskDnsRefresh.java b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTaskDnsRefresh.java new file mode 100644 index 000000000000..485a33fca5f9 --- /dev/null +++ b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTaskDnsRefresh.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.container.common.states.endpoint; + +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD; +import static org.apache.hadoop.hdds.upgrade.HDDSLayoutVersionManager.maxLayoutVersion; +import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.ConnectException; +import java.util.UUID; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.protocol.DatanodeDetails; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.SCMCommandProto; +import org.apache.hadoop.hdds.scm.net.HostAndPort; +import org.apache.hadoop.hdds.upgrade.HDDSLayoutVersionManager; +import org.apache.hadoop.hdfs.util.EnumCounters; +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.statemachine.StateContext; +import org.apache.hadoop.ozone.protocolPB.StorageContainerDatanodeProtocolClientSideTranslatorPB; +import org.junit.jupiter.api.Test; + +/** + * Verifies that a connection-class heartbeat failure triggers a DNS re-resolution of the SCM peer + * (HDDS-15533), gated by the resolve-needed flag, the passive (Recon) check, the missed-heartbeat + * threshold, and the connection-class classifier. Because #10612 keys SCM identity on the stable + * host:port, the trigger only asks the connection manager to rebuild the endpoint -- there is no + * re-keying or StateContext queue migration to assert here. + */ +public class TestHeartbeatEndpointTaskDnsRefresh { + + private static final HostAndPort SCM = new HostAndPort("test-scm-1", 9861); + + @Test + public void connectionFailureAtThresholdTriggersRefresh() throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + conf.setBoolean(OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY, true); + conf.setInt(HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD, 2); + SCMConnectionManager cm = runHeartbeat(conf, 3, new ConnectException("refused")); + verify(cm, times(1)).refreshSCMServer(eq(SCM), any()); + } + + @Test + public void flagOffSuppressesRefresh() throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + conf.setBoolean(OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY, false); + SCMConnectionManager cm = runHeartbeat(conf, 5, new ConnectException("refused")); + verify(cm, never()).refreshSCMServer(any(), any()); + } + + @Test + public void applicationErrorDoesNotTriggerRefresh() throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + conf.setBoolean(OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY, true); + SCMConnectionManager cm = runHeartbeat(conf, 5, new IOException("application-level")); + verify(cm, never()).refreshSCMServer(any(), any()); + } + + @Test + public void belowThresholdDoesNotTriggerRefresh() throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + conf.setBoolean(OZONE_CLIENT_FAILOVER_RESOLVE_NEEDED_KEY, true); + conf.setInt(HDDS_HEARTBEAT_ADDRESS_REFRESH_THRESHOLD, 5); + SCMConnectionManager cm = runHeartbeat(conf, 1, new ConnectException("refused")); + verify(cm, never()).refreshSCMServer(any(), any()); + } + + /** + * Drives a single heartbeat that fails with {@code failure}, with the endpoint reporting + * {@code missedCount} missed heartbeats, and returns the mocked connection manager to verify. + */ + private SCMConnectionManager runHeartbeat(OzoneConfiguration conf, long missedCount, + IOException failure) throws Exception { + StorageContainerDatanodeProtocolClientSideTranslatorPB proxy = + mock(StorageContainerDatanodeProtocolClientSideTranslatorPB.class); + when(proxy.sendHeartbeat(any())).thenThrow(failure); + + EndpointStateMachine endpoint = mock(EndpointStateMachine.class); + when(endpoint.getEndPoint()).thenReturn(proxy); + when(endpoint.getAddress()).thenReturn(SCM); + when(endpoint.getMissedCount()).thenReturn(missedCount); + when(endpoint.isPassive()).thenReturn(false); + + SCMConnectionManager connectionManager = mock(SCMConnectionManager.class); + DatanodeStateMachine dsm = mock(DatanodeStateMachine.class); + when(dsm.getConnectionManager()).thenReturn(connectionManager); + when(dsm.getQueuedCommandCount()) + .thenReturn(new EnumCounters<>(SCMCommandProto.Type.class)); + StateContext context = new StateContext(conf, DatanodeStates.RUNNING, dsm, ""); + + HDDSLayoutVersionManager lvm = mock(HDDSLayoutVersionManager.class); + when(lvm.getSoftwareLayoutVersion()).thenReturn(maxLayoutVersion()); + when(lvm.getMetadataLayoutVersion()).thenReturn(maxLayoutVersion()); + + DatanodeDetails dn = DatanodeDetails.newBuilder() + .setUuid(UUID.randomUUID()) + .setHostName("localhost") + .setIpAddress("127.0.0.1") + .build(); + + HeartbeatEndpointTask.newBuilder() + .setConfig(conf) + .setDatanodeDetails(dn) + .setContext(context) + .setLayoutVersionManager(lvm) + .setEndpointStateMachine(endpoint) + .build() + .call(); + return connectionManager; + } +}