diff --git a/ambry-api/src/main/java/com/github/ambry/clustermap/ClusterParticipant.java b/ambry-api/src/main/java/com/github/ambry/clustermap/ClusterParticipant.java index 9361c3425c..2335df599d 100644 --- a/ambry-api/src/main/java/com/github/ambry/clustermap/ClusterParticipant.java +++ b/ambry-api/src/main/java/com/github/ambry/clustermap/ClusterParticipant.java @@ -45,6 +45,11 @@ void participateAndBlockStateTransition(List ambryStatsReports */ void unblockStateTransition(); + /** + * Populates initial data node config. + */ + boolean populateDataNodeConfig(); + /** * Set the sealed state of the given replica. * @param replicaId the {@link ReplicaId} whose sealed state will be updated. diff --git a/ambry-api/src/main/java/com/github/ambry/clustermap/DataNodeConfig.java b/ambry-api/src/main/java/com/github/ambry/clustermap/DataNodeConfig.java index 20e5c2bf36..6131b7ba65 100644 --- a/ambry-api/src/main/java/com/github/ambry/clustermap/DataNodeConfig.java +++ b/ambry-api/src/main/java/com/github/ambry/clustermap/DataNodeConfig.java @@ -42,6 +42,25 @@ public class DataNodeConfig { private final Map diskConfigs = new TreeMap<>(); private final Map> extraMapFields = new HashMap<>(); + /** + * @param datacenterName the datacenter this server is in. + * @param hostName the host name of the server. + * @param http2Port the HTTP2 port, or {@code null} if the server does not have one. + * @param port the port of the server. + * @param sslPort the ssl port, or {@code null} if the server does not have one. + */ + public DataNodeConfig(String datacenterName, String hostName, Integer http2Port, int port, + Integer sslPort) { + this.datacenterName = datacenterName; + this.hostName = hostName; + this.http2Port = http2Port; + this.port = port; + this.sslPort = sslPort; + this.instanceName = ""; + this.rackId = ""; + this.xid = 0; + } + /** * @param instanceName a name that can be used as a unique key for this server. * @param hostName the host name of the server. @@ -156,6 +175,15 @@ Map getDiskConfigs() { return diskConfigs; } + /** + * Add a disk configuration to this DataNode. + * @param mountPath the mount path of the disk + * @param diskConfig the disk configuration + */ + public void addDiskConfig(String mountPath, DiskConfig diskConfig) { + diskConfigs.put(mountPath, diskConfig); + } + /** * This can be used for extra fields that are not recognized by {@link DataNodeConfigSource} but still need to be * read from or written to the source of truth. This should be used sparingly and is mainly provided for legacy diff --git a/ambry-api/src/main/java/com/github/ambry/config/ClusterMapConfig.java b/ambry-api/src/main/java/com/github/ambry/config/ClusterMapConfig.java index 893c3af17a..afdb03f828 100644 --- a/ambry-api/src/main/java/com/github/ambry/config/ClusterMapConfig.java +++ b/ambry-api/src/main/java/com/github/ambry/config/ClusterMapConfig.java @@ -417,7 +417,13 @@ public class ClusterMapConfig { @Default("false") public final boolean enableFileCopyProtocol; + /** + * The VerifiableProperties used to construct this config. + */ + public final VerifiableProperties verifiableProperties; + public ClusterMapConfig(VerifiableProperties verifiableProperties) { + this.verifiableProperties = verifiableProperties; clusterMapFixedTimeoutDatanodeErrorThreshold = verifiableProperties.getIntInRange("clustermap.fixedtimeout.datanode.error.threshold", 3, 1, 100); clusterMapResourceStatePolicyFactory = verifiableProperties.getString("clustermap.resourcestatepolicy.factory", diff --git a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixClusterAgentsFactory.java b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixClusterAgentsFactory.java index 53b0d08300..57322f6c92 100644 --- a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixClusterAgentsFactory.java +++ b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixClusterAgentsFactory.java @@ -94,5 +94,29 @@ public List getClusterParticipants() throws IOException { } return helixParticipants; } + + /** + * Get the ClusterMapConfig. Exposed for subclasses. + * @return the {@link ClusterMapConfig} associated with this factory. + */ + protected ClusterMapConfig getClusterMapConfig() { + return clusterMapConfig; + } + + /** + * Get the HelixFactory. Exposed for subclasses. + * @return the {@link HelixFactory} used by this factory. + */ + protected HelixFactory getHelixFactory() { + return helixFactory; + } + + /** + * Get the MetricRegistry. Exposed for subclasses. + * @return the {@link MetricRegistry} used by this factory. + */ + protected MetricRegistry getMetricRegistry() { + return metricRegistry; + } } diff --git a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixParticipant.java b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixParticipant.java index 6162673071..d92411ae13 100644 --- a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixParticipant.java +++ b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixParticipant.java @@ -622,6 +622,12 @@ public void unblockStateTransition() { this.blockStateTransitionLatch.countDown(); } + @Override + public boolean populateDataNodeConfig() { + // No-op in base implementation, returns true for backward compatibility. + return true; + } + /** * A zookeeper based implementation for distributed lock. */ @@ -671,6 +677,22 @@ protected void markDisablePartitionComplete() { disablePartitionsComplete = true; } + /** + * Get the ClusterMapConfig. Exposed for subclasses. + * @return the {@link ClusterMapConfig} associated with this participant. + */ + protected ClusterMapConfig getClusterMapConfig() { + return clusterMapConfig; + } + + /** + * Get the DataNodeConfigSource. Exposed for subclasses. + * @return the {@link DataNodeConfigSource} associated with this participant. + */ + protected DataNodeConfigSource getDataNodeConfigSource() { + return dataNodeConfigSource; + } + /** * Disable/enable partition on local node. This method will update both InstanceConfig and DataNodeConfig in PropertyStore. * @param partitionName name of partition on local node diff --git a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/RecoveryTestClusterAgentsFactory.java b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/RecoveryTestClusterAgentsFactory.java index ad54c7e210..1c90149562 100644 --- a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/RecoveryTestClusterAgentsFactory.java +++ b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/RecoveryTestClusterAgentsFactory.java @@ -95,6 +95,13 @@ public void participateAndBlockStateTransition(List ambryHealt public void unblockStateTransition() { } + @Override + public boolean populateDataNodeConfig() { + // Recovery test cluster doesn't support populating data node config. + // Return false to indicate this operation is not supported. + return false; + } + @Override public void close() { } diff --git a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/StaticClusterAgentsFactory.java b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/StaticClusterAgentsFactory.java index 5ef9d3cd58..803bbe7bed 100644 --- a/ambry-clustermap/src/main/java/com/github/ambry/clustermap/StaticClusterAgentsFactory.java +++ b/ambry-clustermap/src/main/java/com/github/ambry/clustermap/StaticClusterAgentsFactory.java @@ -104,6 +104,13 @@ public void participateAndBlockStateTransition(List ambryStats public void unblockStateTransition() { } + @Override + public boolean populateDataNodeConfig() { + // Static clustermap doesn't support populating data node config dynamically. + // Return false to indicate this operation is not supported. + return false; + } + @Override public void close() { diff --git a/ambry-server/src/main/java/com/github/ambry/server/AmbryServer.java b/ambry-server/src/main/java/com/github/ambry/server/AmbryServer.java index e7ca2bb66c..30310c5411 100644 --- a/ambry-server/src/main/java/com/github/ambry/server/AmbryServer.java +++ b/ambry-server/src/main/java/com/github/ambry/server/AmbryServer.java @@ -364,6 +364,9 @@ public void startup() throws InstantiationException { // wait for dataNode to be populated if (nodeId == null) { + if (clusterParticipant != null && !clusterParticipant.populateDataNodeConfig()) { + logger.error("Failed to populate data node config to property store for instance: {}", networkConfig.hostName); + } logger.info("Waiting on dataNode config to be populated..."); if(!dataNodeLatch.await(serverConfig.serverDatanodeConfigTimeout, TimeUnit.SECONDS)) { throw new IllegalArgumentException("Startup timed out waiting for data node config to be populated"); diff --git a/ambry-server/src/test/java/com/github/ambry/server/AmbryServerTest.java b/ambry-server/src/test/java/com/github/ambry/server/AmbryServerTest.java index 286460948d..9583b06566 100644 --- a/ambry-server/src/test/java/com/github/ambry/server/AmbryServerTest.java +++ b/ambry-server/src/test/java/com/github/ambry/server/AmbryServerTest.java @@ -154,4 +154,51 @@ public void testAmbryServerStartupWithoutDataNodeIdTimeoutCase() throws Exceptio "failure during startup java.lang.IllegalArgumentException: Startup timed out waiting for data node config to be populated"); }); } + + @Test + public void testPopulateDataNodeConfigWithHostnameCheck() throws Exception { + ClusterAgentsFactory spyClusterAgentsFactory = spy(new MockClusterAgentsFactory(false, false, 1, 1, 1)); + DataNodeId dataNodeId = spyClusterAgentsFactory.getClusterMap().getDataNodeIds().get(0); + MockClusterMap spyClusterMap = spy(new MockClusterMap(false, false, 1, 1, 1, false, false, null)); + doReturn(null).when(spyClusterMap).getDataNodeId(dataNodeId.getHostname(), dataNodeId.getPort()); + doReturn(spyClusterMap).when(spyClusterAgentsFactory).getClusterMap(); + + // Test case 1: Matching hostnames - should call populateDataNodeConfig + Properties props1 = new Properties(); + props1.setProperty("host.name", "localhost"); + props1.setProperty("port", "1234"); + props1.setProperty("clustermap.cluster.name", "test"); + props1.setProperty("clustermap.datacenter.name", "DC1"); + props1.setProperty("clustermap.host.name", "localhost"); // Same as host.name + props1.setProperty("clustermap.port", "1234"); + props1.setProperty("server.datanode.config.timeout", "1"); // Short timeout for test + + AmbryServer ambryServer1 = new AmbryServer(new VerifiableProperties(props1), spyClusterAgentsFactory, null, + new LoggingNotificationSystem(), SystemTime.getInstance(), null); + + // Should timeout because populateDataNodeConfig is called but no listener triggers + assertException(InstantiationException.class, ambryServer1::startup, e -> { + assertTrue("Should timeout waiting for DataNode config", + e.getMessage().contains("Startup timed out waiting for data node config to be populated")); + }); + + // Test case 2: Non-matching hostnames - should NOT call populateDataNodeConfig + Properties props2 = new Properties(); + props2.setProperty("host.name", "localhost"); + props2.setProperty("port", "1234"); + props2.setProperty("clustermap.cluster.name", "test"); + props2.setProperty("clustermap.datacenter.name", "DC1"); + props2.setProperty("clustermap.host.name", "different-host"); // Different from host.name + props2.setProperty("clustermap.port", "1234"); + props2.setProperty("server.datanode.config.timeout", "1"); // Short timeout for test + + AmbryServer ambryServer2 = new AmbryServer(new VerifiableProperties(props2), spyClusterAgentsFactory, null, + new LoggingNotificationSystem(), SystemTime.getInstance(), null); + + // Should also timeout, but for different reason - no DataNode config population attempted + assertException(InstantiationException.class, ambryServer2::startup, e -> { + assertTrue("Should timeout waiting for DataNode config", + e.getMessage().contains("Startup timed out waiting for data node config to be populated")); + }); + } } diff --git a/ambry-server/src/test/java/com/github/ambry/server/ParticipantsConsistencyTest.java b/ambry-server/src/test/java/com/github/ambry/server/ParticipantsConsistencyTest.java index 93fc80c35c..1e1f268fb8 100644 --- a/ambry-server/src/test/java/com/github/ambry/server/ParticipantsConsistencyTest.java +++ b/ambry-server/src/test/java/com/github/ambry/server/ParticipantsConsistencyTest.java @@ -216,6 +216,13 @@ public void participateAndBlockStateTransition(List ambryStats public void unblockStateTransition() { } + @Override + public boolean populateDataNodeConfig() { + // Mock cluster participant doesn't support populating data node config. + // Return false to indicate this operation is not supported. + return false; + } + @Override public boolean setReplicaSealedState(ReplicaId replicaId, ReplicaSealStatus replicaSealStatus) { String replicaPath = replicaId.getReplicaPath(); diff --git a/ambry-test-utils/src/main/java/com/github/ambry/clustermap/MockClusterAgentsFactory.java b/ambry-test-utils/src/main/java/com/github/ambry/clustermap/MockClusterAgentsFactory.java index 736e6df2de..48eb924e6e 100644 --- a/ambry-test-utils/src/main/java/com/github/ambry/clustermap/MockClusterAgentsFactory.java +++ b/ambry-test-utils/src/main/java/com/github/ambry/clustermap/MockClusterAgentsFactory.java @@ -98,6 +98,13 @@ public void participateAndBlockStateTransition(List ambryHealt public void unblockStateTransition() { } + @Override + public boolean populateDataNodeConfig() { + // Mock cluster doesn't support populating data node config. + // Return false to indicate this operation is not supported. + return false; + } + @Override public void close() {