This is an automated email from the ASF dual-hosted git repository.
ivandika3 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 8acc0413a95 HDDS-14812. Support datanode topology in MiniOzoneCluster
(#10598)
8acc0413a95 is described below
commit 8acc0413a9548462df8a699133d009a230dff2c5
Author: Han-Wen Hsu <[email protected]>
AuthorDate: Thu Jul 30 09:19:58 2026 +0800
HDDS-14812. Support datanode topology in MiniOzoneCluster (#10598)
---
.../dev-support/findbugsExcludeFile.xml | 12 +
.../hadoop/hdds/scm/TestRackAwarePlacement.java | 412 +++++++++++++++++++++
.../org/apache/hadoop/ozone/FixedHostMapping.java | 153 ++++++++
.../org/apache/hadoop/ozone/MiniOzoneCluster.java | 50 +++
.../apache/hadoop/ozone/MiniOzoneClusterImpl.java | 92 +++++
.../hadoop/ozone/MiniOzoneHAClusterImpl.java | 1 +
6 files changed, 720 insertions(+)
diff --git a/hadoop-ozone/integration-test/dev-support/findbugsExcludeFile.xml
b/hadoop-ozone/integration-test/dev-support/findbugsExcludeFile.xml
index 55abc263017..bbc0b6b17af 100644
--- a/hadoop-ozone/integration-test/dev-support/findbugsExcludeFile.xml
+++ b/hadoop-ozone/integration-test/dev-support/findbugsExcludeFile.xml
@@ -16,4 +16,16 @@
limitations under the License.
-->
<FindBugsFilter>
+ <Match>
+ <Class
name="org.apache.hadoop.hdds.scm.TestRackAwarePlacement$WithRacksAndHosts"/>
+ <Bug pattern="SIC_INNER_SHOULD_BE_STATIC"/>
+ </Match>
+ <Match>
+ <Class
name="org.apache.hadoop.hdds.scm.TestRackAwarePlacement$WithRacksOnly"/>
+ <Bug pattern="SIC_INNER_SHOULD_BE_STATIC"/>
+ </Match>
+ <Match>
+ <Class
name="org.apache.hadoop.hdds.scm.TestRackAwarePlacement$WithHostsOnly"/>
+ <Bug pattern="SIC_INNER_SHOULD_BE_STATIC"/>
+ </Match>
</FindBugsFilter>
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestRackAwarePlacement.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestRackAwarePlacement.java
new file mode 100644
index 00000000000..c8258eb0e43
--- /dev/null
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestRackAwarePlacement.java
@@ -0,0 +1,412 @@
+/*
+ * 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.hdds.scm;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerReplica;
+import org.apache.hadoop.hdds.scm.node.NodeManager;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import org.apache.hadoop.net.NetworkTopology;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
+import org.apache.ozone.test.GenericTestUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Integration tests that verify rack/host topology is correctly propagated
+ * to SCM and that pipeline and container placement respect rack boundaries.
+ *
+ */
+public class TestRackAwarePlacement {
+
+ private static final String RACK0 = "/rack0";
+ private static final String RACK1 = "/rack1";
+
+ private static final String[] RACKS = {
+ RACK0, RACK0, RACK0,
+ RACK1, RACK1, RACK1
+ };
+
+ private static final String[] HOSTS = {
+ "host0.test", "host1.test", "host2.test",
+ "host3.test", "host4.test", "host5.test"
+ };
+
+ private static void applyReplicationSpeedupConfig(OzoneConfiguration conf) {
+ conf.setTimeDuration(ScmConfigKeys.OZONE_SCM_HEARTBEAT_PROCESS_INTERVAL,
+ 100, TimeUnit.MILLISECONDS);
+ conf.setTimeDuration(ScmConfigKeys.OZONE_SCM_STALENODE_INTERVAL,
+ 3, TimeUnit.SECONDS);
+ conf.setTimeDuration(ScmConfigKeys.OZONE_SCM_DEADNODE_INTERVAL,
+ 6, TimeUnit.SECONDS);
+ conf.setTimeDuration("hdds.scm.replication.thread.interval",
+ 1, TimeUnit.SECONDS);
+ conf.setTimeDuration("hdds.scm.replication.under.replicated.interval",
+ 5, TimeUnit.SECONDS);
+ conf.setTimeDuration("hdds.scm.replication.over.replicated.interval",
+ 5, TimeUnit.SECONDS);
+ }
+
+ static Stream<Arguments> rackAwarePolicies() {
+ return Stream.of(
+ Arguments.of(
+ "org.apache.hadoop.hdds.scm.container.placement.algorithms"
+ + ".SCMContainerPlacementRackAware"),
+ Arguments.of(
+ "org.apache.hadoop.hdds.scm.container.placement.algorithms"
+ + ".SCMContainerPlacementRackScatter")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("rackAwarePolicies")
+ void testContainerPlacementWithPolicy(
+ String placementClassName) throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(ScmConfigKeys.OZONE_SCM_CONTAINER_PLACEMENT_IMPL_KEY,
+ placementClassName);
+ applyReplicationSpeedupConfig(conf);
+
+ try (MiniOzoneCluster cluster = MiniOzoneCluster.newBuilder(conf)
+ .setNumDatanodes(RACKS.length)
+ .setRacks(RACKS)
+ .setHosts(HOSTS)
+ .build()) {
+ cluster.waitForClusterToBeReady();
+ cluster.waitForPipelineTobeReady(ReplicationFactor.THREE, 60_000);
+
+ StorageContainerManager scm = cluster.getStorageContainerManager();
+ PlacementPolicy actualPolicy = scm.getContainerPlacementPolicy();
+ assertEquals(placementClassName, actualPolicy.getClass().getName(),
+ "Placement policy was not set correctly");
+
+ assertPipelinesSpanMultipleRacks(cluster);
+ assertContainerReplicationIsRackAware(cluster);
+ }
+ }
+
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithRacksAndHosts {
+
+ private MiniOzoneCluster cluster;
+
+ @BeforeAll
+ void init() throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ applyReplicationSpeedupConfig(conf);
+ cluster = MiniOzoneCluster.newBuilder(conf)
+ .setNumDatanodes(RACKS.length)
+ .setRacks(RACKS)
+ .setHosts(HOSTS)
+ .build();
+ cluster.waitForClusterToBeReady();
+ cluster.waitForPipelineTobeReady(ReplicationFactor.THREE, 60_000);
+ }
+
+ @AfterAll
+ void tearDown() {
+ if (cluster != null) {
+ cluster.shutdown();
+ }
+ }
+
+ @Test
+ void testDatanodesHaveCorrectRack() {
+ assertRackAssignments(cluster, RACKS);
+ }
+
+ @Test
+ void testDatanodesHaveCorrectHostname() {
+ assertHostnameAssignments(cluster, HOSTS);
+ }
+
+ @Test
+ void testRatisPipelineSpansMultipleRacks() {
+ assertPipelinesSpanMultipleRacks(cluster);
+ }
+
+ @Test
+ void testContainerReplicationIsRackAware() throws Exception {
+ assertContainerReplicationIsRackAware(cluster);
+ }
+ }
+
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithRacksOnly {
+
+ private MiniOzoneCluster cluster;
+
+ @BeforeAll
+ void init() throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ applyReplicationSpeedupConfig(conf);
+ cluster = MiniOzoneCluster.newBuilder(conf)
+ .setNumDatanodes(RACKS.length)
+ .setRacks(RACKS)
+ .build();
+ cluster.waitForClusterToBeReady();
+ cluster.waitForPipelineTobeReady(ReplicationFactor.THREE, 60_000);
+ }
+
+ @AfterAll
+ void tearDown() {
+ if (cluster != null) {
+ cluster.shutdown();
+ }
+ }
+
+ @Test
+ void testDatanodesHaveCorrectRack() {
+ assertRackAssignments(cluster, RACKS);
+ }
+
+ @Test
+ void testRatisPipelineSpansMultipleRacks() {
+ assertPipelinesSpanMultipleRacks(cluster);
+ }
+
+ @Test
+ void testContainerReplicationIsRackAware() throws Exception {
+ assertContainerReplicationIsRackAware(cluster);
+ }
+ }
+
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithHostsOnly {
+
+ private MiniOzoneCluster cluster;
+
+ @BeforeAll
+ void init() throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ cluster = MiniOzoneCluster.newBuilder(conf)
+ .setNumDatanodes(HOSTS.length)
+ .setHosts(HOSTS)
+ .build();
+ cluster.waitForClusterToBeReady();
+ cluster.waitForPipelineTobeReady(ReplicationFactor.THREE, 60_000);
+ }
+
+ @AfterAll
+ void tearDown() {
+ if (cluster != null) {
+ cluster.shutdown();
+ }
+ }
+
+ @Test
+ void testDatanodesHaveCorrectHostname() {
+ assertHostnameAssignments(cluster, HOSTS);
+ }
+
+ @Test
+ void testDatanodesAllInDefaultRack() {
+ NodeManager nodeManager =
+ cluster.getStorageContainerManager().getScmNodeManager();
+ List<? extends DatanodeDetails> allNodes = nodeManager.getAllNodes();
+
+ for (DatanodeDetails dn : allNodes) {
+ assertEquals(NetworkTopology.DEFAULT_RACK, dn.getNetworkLocation(),
+ "Datanode " + dn.getHostName()
+ + " should be in default rack when no racks are configured");
+ }
+ }
+ }
+
+ private static void assertContainerReplicationIsRackAware(
+ MiniOzoneCluster cluster) throws Exception {
+ StorageContainerManager scm = cluster.getStorageContainerManager();
+
+ try (OzoneClient client = cluster.newClient()) {
+ ObjectStore store = client.getObjectStore();
+ store.createVolume("testvol");
+ OzoneVolume volume = store.getVolume("testvol");
+ volume.createBucket("testbucket");
+ OzoneBucket bucket = volume.getBucket("testbucket");
+
+ byte[] data = "test-data".getBytes(StandardCharsets.UTF_8);
+ try (OzoneOutputStream out = bucket.createKey(
+ "testkey", data.length,
+ RatisReplicationConfig.getInstance(ReplicationFactor.THREE),
+ new HashMap<>())) {
+ out.write(data);
+ }
+ }
+
+ ContainerInfo targetContainer = null;
+ Set<ContainerReplica> replicas = null;
+ for (ContainerInfo c : scm.getContainerManager().getContainers()) {
+ Set<ContainerReplica> r =
+ scm.getContainerManager().getContainerReplicas(c.containerID());
+ if (r.size() >= 3) {
+ targetContainer = c;
+ replicas = r;
+ break;
+ }
+ }
+ assertNotNull(targetContainer,
+ "Should find a container with 3 replicas");
+ ContainerID containerID = targetContainer.containerID();
+
+ DatanodeDetails stoppedDn =
+ replicas.iterator().next().getDatanodeDetails();
+ cluster.shutdownHddsDatanode(stoppedDn);
+
+ GenericTestUtils.waitFor(() -> {
+ try {
+ return scm.getScmNodeManager()
+ .getNodeStatus(stoppedDn)
+ .getHealth() == HddsProtos.NodeState.DEAD;
+ } catch (Exception e) {
+ return false;
+ }
+ }, 500, 30_000);
+
+ GenericTestUtils.waitFor(() -> {
+ try {
+ return scm.getContainerManager()
+ .getContainerReplicas(containerID)
+ .size() >= 3;
+ } catch (Exception e) {
+ return false;
+ }
+ }, 1_000, 60_000);
+
+ Set<String> racks = scm.getContainerManager()
+ .getContainerReplicas(containerID)
+ .stream()
+ .map(r -> r.getDatanodeDetails().getNetworkLocation())
+ .collect(Collectors.toSet());
+
+ assertTrue(racks.size() >= 2,
+ "Container replicas after re-replication should span at least "
+ + "2 racks, but were on: " + racks);
+ }
+
+ private static void assertRackAssignments(MiniOzoneCluster cluster,
+ String[] expectedRacks) {
+ NodeManager nodeManager =
+ cluster.getStorageContainerManager().getScmNodeManager();
+ List<? extends DatanodeDetails> allNodes = nodeManager.getAllNodes();
+
+ assertEquals(expectedRacks.length, allNodes.size(),
+ "Number of registered datanodes should match number of configured
racks");
+
+ long actualRack0 = allNodes.stream()
+ .filter(dn -> RACK0.equals(dn.getNetworkLocation()))
+ .count();
+ long actualRack1 = allNodes.stream()
+ .filter(dn -> RACK1.equals(dn.getNetworkLocation()))
+ .count();
+
+ long expectedRack0 =
+ Arrays.stream(expectedRacks).filter(RACK0::equals).count();
+ long expectedRack1 =
+ Arrays.stream(expectedRacks).filter(RACK1::equals).count();
+
+ assertEquals(expectedRack0, actualRack0,
+ "Expected " + expectedRack0 + " datanodes on " + RACK0);
+ assertEquals(expectedRack1, actualRack1,
+ "Expected " + expectedRack1 + " datanodes on " + RACK1);
+
+ for (DatanodeDetails dn : allNodes) {
+ String location = dn.getNetworkLocation();
+ assertNotNull(location,
+ "Network location must not be null for " + dn.getHostName());
+ assertTrue(location.equals(RACK0) || location.equals(RACK1),
+ "Unexpected rack for datanode " + dn.getHostName()
+ + ": " + location);
+ }
+ }
+
+ private static void assertHostnameAssignments(MiniOzoneCluster cluster,
+ String[] expectedHosts) {
+ NodeManager nodeManager =
+ cluster.getStorageContainerManager().getScmNodeManager();
+ List<? extends DatanodeDetails> allNodes = nodeManager.getAllNodes();
+
+ assertEquals(expectedHosts.length, allNodes.size(),
+ "Number of registered datanodes should match number of configured
hosts");
+
+ Set<String> actual = allNodes.stream()
+ .map(DatanodeDetails::getHostName)
+ .collect(Collectors.toSet());
+
+ Set<String> expected = Arrays.stream(expectedHosts)
+ .collect(Collectors.toSet());
+
+ assertEquals(expected, actual,
+ "Registered datanode hostnames should match configured hosts");
+ }
+
+ private static void assertPipelinesSpanMultipleRacks(
+ MiniOzoneCluster cluster) {
+ List<Pipeline> pipelines = cluster.getStorageContainerManager()
+ .getPipelineManager()
+ .getPipelines(
+ RatisReplicationConfig.getInstance(ReplicationFactor.THREE),
+ Pipeline.PipelineState.OPEN);
+
+ assertFalse(pipelines.isEmpty(),
+ "There should be at least one open RATIS THREE pipeline");
+
+ for (Pipeline pipeline : pipelines) {
+ Set<String> racks = pipeline.getNodes().stream()
+ .map(DatanodeDetails::getNetworkLocation)
+ .collect(Collectors.toSet());
+
+ assertTrue(racks.size() >= 2,
+ "Pipeline " + pipeline.getId()
+ + " should span at least 2 racks, but spans: " + racks);
+ }
+ }
+}
diff --git
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/FixedHostMapping.java
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/FixedHostMapping.java
new file mode 100644
index 00000000000..801af9fb1cf
--- /dev/null
+++
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/FixedHostMapping.java
@@ -0,0 +1,153 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import org.apache.hadoop.net.CachedDNSToSwitchMapping;
+import org.apache.hadoop.net.DNSToSwitchMapping;
+import org.apache.hadoop.net.NetworkTopology;
+
+/**
+ * A {@link CachedDNSToSwitchMapping} implementation that resolves hostnames
+ * to rack locations using a statically configured map, bypassing DNS lookups.
+ *
+ * <p>This is intended for use in test environments (e.g. {@code
MiniOzoneCluster})
+ * where DataNode hostnames may be synthetic or unresolvable via DNS. The
standard
+ * {@link CachedDNSToSwitchMapping} performs DNS normalization before rack
resolution,
+ * which can cause synthetic hostnames to be incorrectly resolved to a real IP
address,
+ * leading to rack mapping failures. This class avoids that by resolving
directly
+ * against the registered hostname.
+ *
+ * <p>The mapping is stored in a JVM-wide static map. Callers must invoke
+ * {@link #addNode(String, String)} before cluster startup to register
hostname-to-rack
+ * entries, and should call {@link #clear()} after each test to avoid
cross-test pollution.
+ *
+ * <p>Usage:
+ * <pre>{@code
+ * FixedHostMapping.addNode("dn-0.test", "/rack1");
+ * FixedHostMapping.addNode("dn-1.test", "/rack1");
+ * FixedHostMapping.addNode("dn-2.test", "/rack2");
+ *
+ * conf.setClass(
+ * CommonConfigurationKeysPublic.NET_TOPOLOGY_NODE_SWITCH_MAPPING_IMPL_KEY,
+ * FixedHostMapping.class,
+ * DNSToSwitchMapping.class);
+ * }</pre>
+ */
+public class FixedHostMapping extends CachedDNSToSwitchMapping {
+
+ private static final Map<String, String> RACK_MAP = new
ConcurrentHashMap<>();
+
+ /**
+ * Constructs a {@code FixedHostMapping} with a no-op raw mapping.
+ * The raw mapping is unused since {@link #resolve(List)} is fully
overridden.
+ */
+ public FixedHostMapping() {
+ super(new NoOpMapping());
+ }
+
+ /**
+ * Constructs a {@code FixedHostMapping} with the given raw mapping.
+ * The raw mapping is unused since {@link #resolve(List)} is fully
overridden,
+ * but is accepted to satisfy {@link CachedDNSToSwitchMapping} constructor
requirements.
+ *
+ * @param rawMapping the raw DNS mapping (not used for resolution)
+ */
+ public FixedHostMapping(DNSToSwitchMapping rawMapping) {
+ super(rawMapping);
+ }
+
+ /**
+ * Registers a hostname-to-rack mapping entry.
+ * Must be called before cluster startup for the mapping to take effect
during
+ * DataNode registration.
+ *
+ * @param host the DataNode hostname as it will appear in {@link
#resolve(List)}
+ * @param rack the rack path (e.g. {@code "/rack1"})
+ */
+ public static void addNode(String host, String rack) {
+ RACK_MAP.put(host, rack);
+ }
+
+ /**
+ * Clears all registered hostname-to-rack mappings.
+ * Should be called in test teardown (e.g. {@code @AfterEach}) to prevent
+ * cross-test pollution of the JVM-wide static map.
+ */
+ public static void clear() {
+ RACK_MAP.clear();
+ }
+
+ /**
+ * Resolves a list of hostnames to their rack locations using the static map.
+ * Hostnames not present in the map are assigned {@link
NetworkTopology#DEFAULT_RACK}.
+ * Unlike the parent class, this method does not perform DNS normalization.
+ *
+ * @param names the list of hostnames to resolve
+ * @return a list of rack paths in the same order as the input
+ */
+ @Override
+ public List<String> resolve(List<String> names) {
+ return names.stream()
+ .map(name -> RACK_MAP.getOrDefault(name, NetworkTopology.DEFAULT_RACK))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * No-op: this implementation does not maintain a cache.
+ */
+ @Override
+ public void reloadCachedMappings() {
+ }
+
+ /**
+ * No-op: this implementation does not maintain a cache.
+ *
+ * @param names the hostnames whose cached mappings should be reloaded
(ignored)
+ */
+ @Override
+ public void reloadCachedMappings(List<String> names) {
+ }
+
+ /**
+ * A no-op {@link DNSToSwitchMapping} used as a placeholder raw mapping.
+ * All hostnames are mapped to {@link NetworkTopology#DEFAULT_RACK}.
+ * This is never invoked during normal resolution since {@link
FixedHostMapping#resolve(List)}
+ * is fully overridden.
+ */
+ private static class NoOpMapping implements DNSToSwitchMapping {
+
+ @Override
+ public List<String> resolve(List<String> names) {
+ return names.stream()
+ .map(n -> NetworkTopology.DEFAULT_RACK)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void reloadCachedMappings() {
+ }
+
+ @Override
+ public void reloadCachedMappings(List<String> names) {
+ }
+ }
+}
diff --git
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneCluster.java
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneCluster.java
index d768e44756b..8765c2aaaae 100644
---
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneCluster.java
+++
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneCluster.java
@@ -19,9 +19,11 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
+import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -263,6 +265,8 @@ abstract class Builder {
protected CertificateClient certClient;
protected SecretKeyClient secretKeyClient;
protected DatanodeFactory dnFactory =
UniformDatanodesFactory.newBuilder().build();
+ protected String[] racks;
+ protected String[] hosts;
private final List<Service> services = new ArrayList<>();
protected Builder(OzoneConfiguration conf) {
@@ -289,6 +293,15 @@ protected void prepareForNextBuild() {
conf.unset(OMConfigKeys.OZONE_OM_DB_DIRS);
conf.unset(OMConfigKeys.OZONE_OM_SNAPSHOT_DIFF_DB_DIR);
+ // dn rack configs
+ if (racks != null) {
+
conf.unset(CommonConfigurationKeysPublic.NET_TOPOLOGY_NODE_SWITCH_MAPPING_IMPL_KEY);
+ conf.unset(HddsConfigKeys.HDDS_DATANODE_USE_DN_HOSTNAME);
+ conf.unset("hadoop.configured.node.mapping");
+ racks = null;
+ hosts = null;
+ }
+
setClusterId();
}
@@ -369,6 +382,43 @@ public Builder setDatanodeFactory(DatanodeFactory factory)
{
return this;
}
+ /**
+ * Sets the rack location for each datanode. Each entry is a rack path
+ * such as {@code "/rack0"}. The length of the array must match the
+ * number of datanodes.
+ *
+ * @param racks rack path per datanode
+ * @return this Builder
+ */
+ public Builder setRacks(String[] racks) {
+ this.racks = Arrays.copyOf(racks, racks.length);
+ return this;
+ }
+
+ /**
+ * Sets the hostname for each datanode. When used together with
+ * {@link #setRacks}, the hostnames are used as keys in the
+ * {@code StaticMapping} instead of the default synthetic names
+ * ({@code "dn-0"}, {@code "dn-1"}, …). The length of the array must
+ * match the number of datanodes.
+ *
+ * @param hosts hostname per datanode
+ * @return this Builder
+ */
+ public Builder setHosts(String[] hosts) {
+ this.hosts = Arrays.copyOf(hosts, hosts.length);
+ return this;
+ }
+
+ protected void validateDatanodeConfiguration() {
+ if (racks != null && racks.length != numOfDatanodes) {
+ throw new IllegalArgumentException("Number of racks must match the
number of datanodes");
+ }
+ if (hosts != null && hosts.length != numOfDatanodes) {
+ throw new IllegalArgumentException("Number of hosts must match the
number of datanodes");
+ }
+ }
+
public Builder addService(Service service) {
services.add(service);
return this;
diff --git
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneClusterImpl.java
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneClusterImpl.java
index b0e2c1efbdb..e13719dde6e 100644
---
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneClusterImpl.java
+++
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneClusterImpl.java
@@ -39,12 +39,14 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
import org.apache.hadoop.hdds.client.RatisReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.scm.HddsTestUtils;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
@@ -54,6 +56,7 @@
import org.apache.hadoop.hdds.scm.node.NodeStatus;
import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.pipeline.PipelineManager;
import
org.apache.hadoop.hdds.scm.protocolPB.StorageContainerLocationProtocolClientSideTranslatorPB;
import org.apache.hadoop.hdds.scm.proxy.SCMClientConfig;
import
org.apache.hadoop.hdds.scm.proxy.SCMContainerLocationFailoverProxyProvider;
@@ -64,14 +67,17 @@
import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
import
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
+import org.apache.hadoop.hdds.utils.HddsServerUtil;
import org.apache.hadoop.hdds.utils.IOUtils;
import org.apache.hadoop.hdds.utils.db.CodecBuffer;
import org.apache.hadoop.hdds.utils.db.CodecTestUtil;
import org.apache.hadoop.hdds.utils.db.managed.ManagedRocksObjectMetrics;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.hadoop.net.DNSToSwitchMapping;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneClientFactory;
import org.apache.hadoop.ozone.common.Storage.StorageState;
+import org.apache.hadoop.ozone.container.common.helpers.ContainerUtils;
import org.apache.hadoop.ozone.container.common.utils.ContainerCache;
import org.apache.hadoop.ozone.container.common.utils.DatanodeStoreCache;
import org.apache.hadoop.ozone.om.OMConfigKeys;
@@ -505,6 +511,7 @@ public Builder(OzoneConfiguration conf) {
@Override
public MiniOzoneCluster build() throws IOException {
+ validateDatanodeConfiguration();
DefaultMetricsSystem.setMiniClusterMode(true);
DatanodeStoreCache.setMiniClusterMode();
initializeConfiguration();
@@ -527,6 +534,12 @@ public MiniOzoneCluster build() throws IOException {
cluster.startHddsDatanodes();
}
+ // Recreate the Ratis pipeline to prevent imbalanced node placement
across racks
+ // caused by asynchronous DN registration.
+ if (racks != null && startDataNodes) {
+ resetPipelinesForRackAwareness(cluster);
+ }
+
prepareForNextBuild();
return cluster;
} catch (Exception ex) {
@@ -559,6 +572,33 @@ protected void setClients(OzoneManager om) throws
IOException {
}
}
+ /**
+ * Waits for all DNs to be healthy, then removes any pipelines that
+ * were created before the full rack topology was visible, and creates one
+ * fresh rack-aware pipeline directly (bypassing the background timer).
+ */
+ private void resetPipelinesForRackAwareness(MiniOzoneClusterImpl cluster)
+ throws IOException {
+ try {
+ cluster.waitForClusterToBeReady();
+ } catch (TimeoutException | InterruptedException e) {
+ throw new IOException(
+ "Timed out waiting for rack-aware cluster to be ready", e);
+ }
+ RatisReplicationConfig threeWay =
+
RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE);
+ PipelineManager pm =
+ cluster.getStorageContainerManager().getPipelineManager();
+ for (Pipeline p : pm.getPipelines(threeWay)) {
+ if (!p.isClosed()) {
+ pm.closePipeline(p.getId());
+ }
+ pm.deletePipeline(p.getId());
+ }
+
+ pm.createPipeline(threeWay);
+ }
+
/**
* Initializes the configuration required for starting MiniOzoneCluster.
*/
@@ -577,6 +617,40 @@ protected void initializeConfiguration() throws
IOException {
// pipeline.
conf.setInt(HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE,
numOfDatanodes >= 3 ? 3 : 1);
+
+ configureHostAndRackTopology();
+ }
+
+ private void configureHostAndRackTopology() throws IOException {
+ FixedHostMapping.clear();
+ if (racks == null && hosts == null) {
+ return;
+ }
+
+ conf.setBoolean(HddsConfigKeys.HDDS_DATANODE_USE_DN_HOSTNAME, true);
+
+ if (hosts == null) {
+ hosts = new String[racks.length];
+ for (int i = 0; i < racks.length; i++) {
+ hosts[i] = "host" + i + ".foo.com";
+ }
+ }
+
+ if (racks != null) {
+
+ if (hosts.length != racks.length) {
+ throw new IllegalArgumentException(
+ "The length of hosts [" + hosts.length
+ + "] must match the length of racks [" + racks.length +
"].");
+ }
+
+
conf.setClass(CommonConfigurationKeysPublic.NET_TOPOLOGY_NODE_SWITCH_MAPPING_IMPL_KEY,
+ FixedHostMapping.class, DNSToSwitchMapping.class);
+
+ for (int i = 0; i < racks.length; i++) {
+ FixedHostMapping.addNode(hosts[i], racks[i]);
+ }
+ }
}
void removeConfiguration() {
@@ -698,14 +772,32 @@ protected List<HddsDatanodeService> createHddsDatanodes()
for (int i = 0; i < numOfDatanodes; i++) {
OzoneConfiguration dnConf = dnFactory.apply(conf);
+ if (hosts != null) {
+ dnConf.set(HddsConfigKeys.HDDS_DATANODE_HOST_NAME_KEY, hosts[i]);
+ }
+ // Bypass InetAddress.getName() resolution for custom hostnames by
starting DN via YAML.
+ confDatanodeViaYaml(dnConf);
HddsDatanodeService datanode = new HddsDatanodeService(NO_ARGS);
+ dnConf.setStrings(ScmConfigKeys.OZONE_SCM_NAMES,
conf.getStrings(ScmConfigKeys.OZONE_SCM_NAMES));
datanode.setConfiguration(dnConf);
hddsDatanodes.add(datanode);
}
+
return hddsDatanodes;
}
+ private void confDatanodeViaYaml(OzoneConfiguration dnConf) throws
IOException {
+ DatanodeDetails datanodeDetails = DatanodeDetails.newBuilder()
+ .setID(DatanodeID.randomID())
+ .setHostName(dnConf.get(HddsConfigKeys.HDDS_DATANODE_HOST_NAME_KEY))
+ .setIpAddress("127.0.0.1")
+ .build();
+ datanodeDetails.setNetworkName(datanodeDetails.getUuidString());
+ String dnFilePath = HddsServerUtil.getDatanodeIdFilePath(dnConf);
+ ContainerUtils.writeDatanodeDetailsTo(datanodeDetails, new
File(dnFilePath), dnConf);
+ }
+
protected void configureSCM(boolean isHA) throws IOException {
conf.set(ScmConfigKeys.OZONE_SCM_CLIENT_ADDRESS_KEY,
localhostWithFreePort());
diff --git
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneHAClusterImpl.java
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneHAClusterImpl.java
index 303a16a1a63..507777d8bfe 100644
---
a/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneHAClusterImpl.java
+++
b/hadoop-ozone/mini-cluster/src/main/java/org/apache/hadoop/ozone/MiniOzoneHAClusterImpl.java
@@ -464,6 +464,7 @@ public Builder setSCMServiceId(String serviceId) {
@Override
public MiniOzoneHAClusterImpl build() throws IOException {
+ validateDatanodeConfiguration();
if (numOfActiveOMs > numOfOMs) {
throw new IllegalArgumentException("Number of active OMs cannot be " +
"more than the total number of OMs");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]