This is an automated email from the ASF dual-hosted git repository.
chungen0126 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 8664dc4fc3c HDDS-14932. Optional Recon Start Up (#10760)
8664dc4fc3c is described below
commit 8664dc4fc3cd29b7508d6ce5fa532d424abe7801
Author: Chun-Hung Tseng <[email protected]>
AuthorDate: Wed Oct 14 10:50:25 2026 +0200
HDDS-14932. Optional Recon Start Up (#10760)
Co-authored-by: Siyao Meng <[email protected]>
---
hadoop-hdds/docs/content/tools/_index.md | 2 +-
hadoop-ozone/integration-test-recon/pom.xml | 5 +
.../hadoop/ozone/local/TestLocalOzoneRecon.java | 127 ++++++++++++++
.../hadoop/ozone/recon/ConfigurationProvider.java | 7 +-
.../org/apache/hadoop/ozone/recon/ReconServer.java | 1 +
.../scm/ReconStorageContainerManagerFacade.java | 8 +-
hadoop-ozone/tools/pom.xml | 4 +
.../hadoop/ozone/local/LocalOzoneCluster.java | 172 +++++++++++++++++--
.../ozone/local/LocalOzoneClusterConfig.java | 31 ++++
.../hadoop/ozone/local/LocalOzoneRuntime.java | 14 ++
.../org/apache/hadoop/ozone/local/OzoneLocal.java | 28 ++-
.../hadoop/ozone/local/TestLocalOzoneCluster.java | 190 +++++++++++++++++++++
.../ozone/local/TestLocalOzoneClusterConfig.java | 9 +
.../apache/hadoop/ozone/local/TestOzoneLocal.java | 71 ++++++++
14 files changed, 650 insertions(+), 19 deletions(-)
diff --git a/hadoop-hdds/docs/content/tools/_index.md
b/hadoop-hdds/docs/content/tools/_index.md
index adbd4740f87..8bb437c8421 100644
--- a/hadoop-hdds/docs/content/tools/_index.md
+++ b/hadoop-hdds/docs/content/tools/_index.md
@@ -44,7 +44,7 @@ Client commands:
* **sh** - Primary command line interface for ozone to manage
volumes/buckets/keys.
* **fs** - Runs a command on ozone file system (similar to `hdfs dfs`)
* **local** - Runs a single-node local Ozone cluster (SCM, OM, datanodes,
- and optional S3 Gateway) in one process for development.
+ and optional S3 Gateway and Recon) in one process for development.
* **version** - Prints the version of Ozone and HDDS.
diff --git a/hadoop-ozone/integration-test-recon/pom.xml
b/hadoop-ozone/integration-test-recon/pom.xml
index 33e1b93493d..6286e136eda 100644
--- a/hadoop-ozone/integration-test-recon/pom.xml
+++ b/hadoop-ozone/integration-test-recon/pom.xml
@@ -185,6 +185,11 @@
<artifactId>ozone-reconcodegen</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.ozone</groupId>
+ <artifactId>ozone-tools</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.ratis</groupId>
<artifactId>ratis-common</artifactId>
diff --git
a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneRecon.java
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneRecon.java
new file mode 100644
index 00000000000..1c768a7a44e
--- /dev/null
+++
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneRecon.java
@@ -0,0 +1,127 @@
+/*
+ * 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.local;
+
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_SCM_CONTAINER_SYNC_TASK_INTERVAL_DELAY;
+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.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.ConnectException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.server.http.BaseHttpServer;
+import org.apache.ozone.test.GenericTestUtils;
+import org.apache.ozone.test.GenericTestUtils.LogCapturer;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Integration tests for Recon in the {@code ozone local} runtime.
+ */
+class TestLocalOzoneRecon {
+
+ @TempDir
+ private Path tempDir;
+
+ static Stream<Arguments> services() {
+ return Stream.of(false, true).flatMap(s3g -> Stream.of(false,
true).map(recon -> Arguments.of(s3g, recon)));
+ }
+
+ /** A 200 from Recon's /api/v1/clusterState shows that Recon resources can
be injected in the shared JVM. */
+ @ParameterizedTest(name = "s3g={0}, recon={1}")
+ @MethodSource("services")
+ void enabledServicesServeAndStopOnClose(boolean s3gEnabled, boolean
reconEnabled) throws Exception {
+ List<String> endpoints = new ArrayList<>();
+ try (LocalOzoneCluster cluster =
+ new LocalOzoneCluster(config(s3gEnabled, reconEnabled), new
OzoneConfiguration())) {
+ cluster.start();
+
+ assertEquals(reconEnabled, syncThreadRunning());
+ if (reconEnabled) {
+ assertEquals(200, responseCode(cluster.getReconEndpoint() +
"/api/v1/clusterState"));
+ endpoints.add(cluster.getReconEndpoint());
+ }
+ if (s3gEnabled) {
+ assertTrue(responseCode(cluster.getS3Endpoint()) > 0);
+ endpoints.add(cluster.getS3Endpoint());
+ } else {
+ assertEquals(-1, cluster.getS3gPort());
+ }
+ }
+
+ GenericTestUtils.waitFor(() -> !syncThreadRunning(), 100, 10_000);
+ for (String endpoint : endpoints) {
+ assertThrows(ConnectException.class, () -> responseCode(endpoint),
endpoint);
+ }
+ }
+
+ /**
+ * Regression test: ReconServer#call() logged an initialization failure and
returned normally, so when
+ * ReconStorageContainerManagerFacade#start() failed after the Recon HTTP
server started, start() succeeded without
+ * the Recon datanode RPC server. A zero sync interval fails it there,
because
+ * ScheduledExecutorService#scheduleWithFixedDelay rejects a delay that is
not positive.
+ */
+ @ParameterizedTest(name = "s3g={0}")
+ @ValueSource(booleans = {false, true})
+ void startFailsAndStopsReconWhenReconFailsAfterHttpServerStarts(boolean
s3gEnabled) throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OZONE_RECON_SCM_CONTAINER_SYNC_TASK_INTERVAL_DELAY, "0s");
+
+ try (LogCapturer httpLogs = LogCapturer.captureLogs(BaseHttpServer.class);
+ LocalOzoneCluster cluster = new LocalOzoneCluster(config(s3gEnabled,
true), conf)) {
+ assertThrows(IllegalArgumentException.class, cluster::start);
+ assertThat(httpLogs.getOutput()).contains("HTTP server of recon
listening");
+ assertThrows(ConnectException.class, () ->
responseCode(cluster.getReconEndpoint()));
+ }
+ }
+
+ private LocalOzoneClusterConfig config(boolean s3gEnabled, boolean
reconEnabled) {
+ return
LocalOzoneClusterConfig.builder(tempDir.resolve("local-ozone-recon"))
+ .setS3gEnabled(s3gEnabled)
+ .setReconEnabled(reconEnabled)
+ .setStartupTimeout(Duration.ofMinutes(2))
+ .build();
+ }
+
+ private static boolean syncThreadRunning() {
+ return Thread.getAllStackTraces().keySet().stream()
+ .anyMatch(thread -> thread.getName().contains("SyncSCMContainerInfo"));
+ }
+
+ private static int responseCode(String endpoint) throws Exception {
+ HttpURLConnection connection = (HttpURLConnection) new
URL(endpoint).openConnection();
+ try {
+ connection.setConnectTimeout(1_000);
+ connection.setReadTimeout(1_000);
+ return connection.getResponseCode();
+ } finally {
+ connection.disconnect();
+ }
+ }
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ConfigurationProvider.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ConfigurationProvider.java
index c440ba25850..4cd1b48ee66 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ConfigurationProvider.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ConfigurationProvider.java
@@ -17,7 +17,6 @@
package org.apache.hadoop.ozone.recon;
-import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Provider;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configuration.DeprecationDelta;
@@ -50,16 +49,14 @@ private static void addDeprecations() {
});
}
- @VisibleForTesting
public static void setConfiguration(OzoneConfiguration conf) {
- // Nullity check is used in case the configuration was already set
- // in the MiniOzoneCluster
+ // First writer wins: ReconService and LocalOzoneCluster set this before
ReconServer#call()
+ // reaches it, and their configuration is the one the in-JVM server has to
run with.
if (configuration == null) {
ConfigurationProvider.configuration = conf;
}
}
- @VisibleForTesting
public static void resetConfiguration() {
configuration = null;
}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
index deb9791b152..d015de32dc9 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
@@ -209,6 +209,7 @@ public Void call() throws Exception {
} catch (Exception e) {
LOG.error("Error during initializing Recon server.", e);
updateAndLogReconHealthStatus();
+ throw e;
}
ShutdownHookManager.get().addShutdownHook(() -> {
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
index 6c5ce522423..47c8e256b29 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
@@ -175,6 +175,7 @@ public class ReconStorageContainerManagerFacade
private final ReconStorageContainerSyncHelper containerSyncHelper;
private final ReconScmContainerSyncMetrics containerSyncMetrics;
private final ExecutorService scmSnapshotExecutor;
+ private ScheduledExecutorService scmContainerSyncScheduler;
private final Object scmSnapshotLock = new Object();
private Future<?> scmSnapshotFuture;
private ScmDbSnapshotSyncStatus scmSnapshotStatus =
@@ -587,7 +588,7 @@ public void start() {
"Recon ScmDatanodeProtocol RPC server",
getDatanodeProtocolServer().getDatanodeRpcAddress()));
}
- ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
+ scmContainerSyncScheduler = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder().setNameFormat(threadNamePrefix +
"SyncSCMContainerInfo-%d")
.build());
@@ -615,7 +616,7 @@ public void start() {
TimeUnit.MILLISECONDS);
LOG.debug("Started the SCM Container Info sync scheduler (interval={}ms,
initialDelay={}ms).",
syncInterval, syncInitialDelay);
- scheduler.scheduleWithFixedDelay(() -> {
+ scmContainerSyncScheduler.scheduleWithFixedDelay(() -> {
if (!isSyncDataFromSCMRunning.compareAndSet(false, true)) {
LOG.debug("SCM container info sync is already running; skipping this
cycle.");
return;
@@ -661,6 +662,9 @@ public void join() {
*/
@Override
public void stop() {
+ if (scmContainerSyncScheduler != null) {
+ scmContainerSyncScheduler.shutdownNow();
+ }
getDatanodeProtocolServer().stop();
reconScmTasks.forEach(ReconScmTask::stop);
try {
diff --git a/hadoop-ozone/tools/pom.xml b/hadoop-ozone/tools/pom.xml
index bdd362d9ba4..ca04dca1f01 100644
--- a/hadoop-ozone/tools/pom.xml
+++ b/hadoop-ozone/tools/pom.xml
@@ -87,6 +87,10 @@
<groupId>org.apache.ozone</groupId>
<artifactId>ozone-manager</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.ozone</groupId>
+ <artifactId>ozone-recon</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.ozone</groupId>
<artifactId>ozone-s3gateway</artifactId>
diff --git
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
index 8bb057f601b..8484f7c877d 100644
---
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
+++
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
@@ -25,6 +25,12 @@
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE;
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION;
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_ADDRESS_KEY;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_DATANODE_ADDRESS_KEY;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_DATANODE_BIND_HOST_KEY;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_HTTPS_ADDRESS_KEY;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_HTTP_ADDRESS_KEY;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD;
import static
org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_CONTAINER_RATIS_ENABLED_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_DATANODE_DIR_KEY;
import static
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_BLOCK_CLIENT_ADDRESS_KEY;
@@ -74,6 +80,12 @@
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SNAPSHOT_DIFF_DB_DIR;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DB_DIR;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_HTTPS_BIND_HOST_KEY;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_HTTP_BIND_HOST_KEY;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_HTTP_ENABLED_KEY;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_OM_SNAPSHOT_DB_DIR;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_SCM_DB_DIR;
import static
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_ADDRESS_KEY;
import static
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_BIND_HOST_KEY;
import static
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ADDRESS_KEY;
@@ -103,6 +115,11 @@
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.Callable;
+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 java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
@@ -125,6 +142,9 @@
import org.apache.hadoop.ozone.container.replication.ReplicationServer;
import org.apache.hadoop.ozone.om.OMStorage;
import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.recon.ConfigurationProvider;
+import org.apache.hadoop.ozone.recon.ReconServer;
+import org.apache.hadoop.ozone.recon.ReconSqlDbConfig;
import org.apache.hadoop.ozone.s3.Gateway;
import org.apache.hadoop.ozone.s3.OzoneConfigurationHolder;
import org.apache.ratis.util.function.CheckedSupplier;
@@ -132,7 +152,7 @@
import org.slf4j.LoggerFactory;
/**
- * Starts the SCM, OM, datanode, and optional S3 Gateway portion of the {@code
ozone local} runtime.
+ * Starts the SCM, OM, datanode, and optional S3 Gateway and Recon portion of
the {@code ozone local} runtime.
*/
public final class LocalOzoneCluster implements LocalOzoneRuntime {
@@ -162,6 +182,10 @@ public final class LocalOzoneCluster implements
LocalOzoneRuntime {
private static final String OM_RATIS_PORT_KEY = "om.ratis";
private static final String S3G_HTTP_PORT_KEY = "s3g.http";
private static final String S3G_WEBADMIN_HTTP_PORT_KEY = "s3g.web.http";
+ private static final String RECON_HTTP_PORT_KEY = "recon.http";
+ private static final String RECON_HTTPS_PORT_KEY = "recon.https";
+ private static final String RECON_DATANODE_PORT_KEY = "recon.datanode";
+ private static final String RECON_DIR_NAME = "recon";
private static final String DATANODE_PORT_KEY_PREFIX = "dn.";
private static final String DATANODE_HTTP_PORT_KEY_SUFFIX = "http";
private static final String DATANODE_CLIENT_PORT_KEY_SUFFIX = "client";
@@ -223,6 +247,7 @@ public final class LocalOzoneCluster implements
LocalOzoneRuntime {
private StorageContainerManager scm;
private OzoneManager om;
private Gateway s3Gateway;
+ private ReconServer reconServer;
private final List<HddsDatanodeService> datanodes = new ArrayList<>();
private boolean previousMetricsMiniClusterMode;
private boolean metricsMiniClusterModeEnabled;
@@ -254,6 +279,9 @@ public void start() throws Exception {
startOm(prepared.getConfiguration());
startDatanodes(prepared.getDatanodeConfigurations());
waitForClusterReadiness(config.getStartupTimeout());
+ if (config.isReconEnabled()) {
+ startRecon(prepared.getConfiguration());
+ }
if (config.isS3gEnabled()) {
startS3Gateway(prepared.getConfiguration());
}
@@ -282,13 +310,15 @@ PreparedConfiguration prepareConfiguration() throws
IOException {
int scmPort = configureScm(conf, persistedPorts, portAllocator);
int omPort = configureOm(conf, persistedPorts, portAllocator);
int s3gPort = configureS3Gateway(conf, persistedPorts, portAllocator);
+ int reconPort = configureRecon(conf, persistedPorts, portAllocator);
List<OzoneConfiguration> datanodeConfigurations =
configureDatanodes(conf, persistedPorts, portAllocator);
prepareStorageLayout();
createServiceDirectories();
persistedPorts.store();
- preparedConfiguration = new PreparedConfiguration(conf, scmPort, omPort,
s3gPort, datanodeConfigurations);
+ preparedConfiguration = new PreparedConfiguration(conf, scmPort, omPort,
s3gPort, reconPort,
+ datanodeConfigurations);
return preparedConfiguration;
}
@@ -332,6 +362,22 @@ InetSocketAddress getS3gBoundAddress() {
return s3Gateway != null ? s3Gateway.getHttpAddress() : null;
}
+ @Override
+ public int getReconPort() {
+ if (!config.isReconEnabled()) {
+ return -1;
+ }
+ return preparedConfiguration == null ? config.getReconPort() :
preparedConfiguration.getReconPort();
+ }
+
+ @Override
+ public String getReconEndpoint() {
+ if (!config.isReconEnabled()) {
+ return "";
+ }
+ return "http://" + getDisplayHost() + ":" + getReconPort();
+ }
+
@Override
public void close() throws IOException {
if (closed) {
@@ -358,7 +404,7 @@ private void stopServices() {
try {
// Logged rather than closeQuietly: stopServices() is also the start()
rollback, where a silent
// failure hides leaked threads and ports.
- IOUtils.close(LOG, this::stopS3Gateway, this::stopDatanodes,
this::stopOm, this::stopScm);
+ IOUtils.close(LOG, this::stopS3Gateway, this::stopRecon,
this::stopDatanodes, this::stopOm, this::stopScm);
} finally {
restoreSameJvmMetricsMode();
}
@@ -372,6 +418,15 @@ private void stopS3Gateway() throws Exception {
}
}
+ private void stopRecon() {
+ ReconServer service = reconServer;
+ reconServer = null;
+ if (service != null) {
+ service.stop();
+ service.join();
+ }
+ }
+
private void stopDatanodes() {
List<AutoCloseable> stoppers = new ArrayList<>();
for (int i = datanodes.size() - 1; i >= 0; i--) {
@@ -403,6 +458,14 @@ private void stopScm() {
}
private void configureLocalDefaults(OzoneConfiguration conf) throws
IOException {
+ if (config.isS3gEnabled() || config.isReconEnabled()) {
+ // getS3Endpoint() and getReconEndpoint() advertise the HTTP listener,
which a policy without HTTP leaves unbound.
+ HttpConfig.Policy httpPolicy =
+ HttpConfig.Policy.fromString(conf.get(OZONE_HTTP_POLICY_KEY,
OZONE_HTTP_POLICY_DEFAULT));
+ if (httpPolicy != null && !httpPolicy.isHttpEnabled()) {
+ rejectUserConfigured(conf, OZONE_HTTP_POLICY_KEY, "HTTP_ONLY or
HTTP_AND_HTTPS");
+ }
+ }
conf.set(OZONE_METADATA_DIRS, metadataDir().toString());
// SCM and OM share one configuration and JVM, so the Jetty base dir is a
// single service-neutral location; Jetty keeps per-context temp dirs.
@@ -651,12 +714,6 @@ private int configureS3Gateway(OzoneConfiguration conf,
PersistedPortState persi
if (!config.isS3gEnabled()) {
return -1;
}
- // getS3Endpoint() advertises the HTTP listener, which a policy without
HTTP leaves unbound.
- HttpConfig.Policy httpPolicy =
- HttpConfig.Policy.fromString(conf.get(OZONE_HTTP_POLICY_KEY,
OZONE_HTTP_POLICY_DEFAULT));
- if (httpPolicy != null && !httpPolicy.isHttpEnabled()) {
- rejectUserConfigured(conf, OZONE_HTTP_POLICY_KEY, "HTTP_ONLY or
HTTP_AND_HTTPS");
- }
// getS3gPort() reads the port back off this listener.
setLocalOverride(conf, OZONE_S3G_HTTP_ENABLED_KEY, true);
// S3StorageType maps an explicit STANDARD to RATIS/THREE regardless of
the local STAND_ALONE/ONE replication.
@@ -677,6 +734,39 @@ private int configureS3Gateway(OzoneConfiguration conf,
PersistedPortState persi
return s3gHttpPort;
}
+ private int configureRecon(OzoneConfiguration conf, PersistedPortState
persistedPorts,
+ PortAllocator portAllocator) throws IOException {
+ if (!config.isReconEnabled()) {
+ return -1;
+ }
+ // getReconEndpoint() advertises this listener.
+ setLocalOverride(conf, OZONE_RECON_HTTP_ENABLED_KEY, true);
+ Path reconDir = config.getDataDir().resolve(RECON_DIR_NAME);
+ conf.setIfUnset(OZONE_RECON_DB_DIR, reconDir.toString());
+ conf.setIfUnset(OZONE_RECON_OM_SNAPSHOT_DB_DIR, reconDir.toString());
+ conf.setIfUnset(OZONE_RECON_SCM_DB_DIR, reconDir.toString());
+
+ ReconSqlDbConfig dbConfig = conf.getObject(ReconSqlDbConfig.class);
+ dbConfig.setJdbcUrl("jdbc:derby:" +
reconDir.resolve("ozone_recon_derby.db"));
+ conf.setFromObject(dbConfig);
+
+ int reconHttpPort = reservePort(portAllocator, persistedPorts,
RECON_HTTP_PORT_KEY, config.getReconPort());
+ int reconHttpsPort = reservePort(portAllocator, persistedPorts,
RECON_HTTPS_PORT_KEY, 0);
+ int reconDatanodePort = reservePort(portAllocator, persistedPorts,
RECON_DATANODE_PORT_KEY, 0);
+
+ conf.set(OZONE_RECON_ADDRESS_KEY, address(config.getHost(),
reconDatanodePort));
+ conf.set(OZONE_RECON_DATANODE_ADDRESS_KEY, address(config.getHost(),
reconDatanodePort));
+ conf.set(OZONE_RECON_DATANODE_BIND_HOST_KEY, config.getBindHost());
+ conf.set(OZONE_RECON_HTTP_ADDRESS_KEY, address(config.getHost(),
reconHttpPort));
+ conf.set(OZONE_RECON_HTTP_BIND_HOST_KEY, config.getBindHost());
+ conf.set(OZONE_RECON_HTTPS_ADDRESS_KEY, address(config.getHost(),
reconHttpsPort));
+ conf.set(OZONE_RECON_HTTPS_BIND_HOST_KEY, config.getBindHost());
+ // Recon's start-up blocks until it leaves safe mode and an empty local
cluster never leaves
+ // it on its own, so the shipped 300s default would stall every start for
five minutes.
+ setLocalOverrideDuration(conf, OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD,
"10s");
+ return reconHttpPort;
+ }
+
private List<OzoneConfiguration> configureDatanodes(OzoneConfiguration conf,
PersistedPortState persistedPorts, PortAllocator portAllocator)
throws IOException {
@@ -844,6 +934,54 @@ private void startS3Gateway(OzoneConfiguration conf)
throws Exception {
s3Gateway.call();
}
+ /**
+ * Runs a blocking in-JVM service startup under {@code timeout}, so the
launcher fails fast and
+ * rolls back instead of hanging if a startup ever blocks. {@code
startupCleanup} is the only
+ * stopper for an attempt that did not finish: a startup that ignores the
interrupt keeps running
+ * after {@code stopServices()} has rolled back, so on a timeout or an
interrupt the cleanup is
+ * queued behind it on the same single thread rather than run here. The
caller assigns its field
+ * only after this returns, so a failed attempt is never reachable from the
rollback.
+ */
+ static <V> V executeWithinStartupTimeout(String serviceName, Callable<V>
startup,
+ AutoCloseable startupCleanup, Duration timeout) throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
+ Thread thread = new Thread(runnable, "local-startup-" + serviceName);
+ thread.setDaemon(true);
+ return thread;
+ });
+ Future<V> future = executor.submit(startup);
+ try {
+ return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+ } catch (TimeoutException ex) {
+ future.cancel(true);
+ executor.submit(() -> IOUtils.close(LOG, startupCleanup));
+ throw new IOException("Local " + serviceName + " did not start within "
+ timeout + ".", ex);
+ } catch (InterruptedException ex) {
+ future.cancel(true);
+ executor.submit(() -> IOUtils.close(LOG, startupCleanup));
+ Thread.currentThread().interrupt();
+ throw ex;
+ } catch (ExecutionException ex) {
+ IOUtils.close(LOG, startupCleanup);
+ Throwable cause = ex.getCause();
+ throw cause instanceof Exception ? (Exception) cause : ex;
+ } finally {
+ // shutdown(), not shutdownNow(): the timeout path queues the cleanup,
which shutdownNow()
+ // would discard. The worker is a daemon thread, so it cannot keep the
JVM alive.
+ executor.shutdown();
+ }
+ }
+
+ private void startRecon(OzoneConfiguration conf) throws Exception {
+ // ReconServer reads ConfigurationProvider; setConfiguration() keeps the
first value.
+ ConfigurationProvider.resetConfiguration();
+ ConfigurationProvider.setConfiguration(new OzoneConfiguration(conf));
+ ReconServer recon = new ReconServer();
+ recon.getCmd().parseArgs();
+ executeWithinStartupTimeout("Recon", recon::call, recon::stop,
config.getStartupTimeout());
+ reconServer = recon;
+ }
+
private void waitForClusterReadiness(Duration timeout) throws Exception {
waitForReadiness(this::clusterReadinessBlocker, "Ozone cluster", timeout);
}
@@ -974,6 +1112,9 @@ private void createServiceDirectories() throws IOException
{
Files.createDirectories(datanodeDir.resolve(OZONE_METADATA_DIR_NAME));
Files.createDirectories(datanodeDir.resolve(DATA_DIR_NAME));
}
+ if (config.isReconEnabled()) {
+ Files.createDirectories(dataDir.resolve(RECON_DIR_NAME));
+ }
}
private void createBaseLayout() throws IOException {
@@ -1021,6 +1162,11 @@ private String[] requiredPersistedPortKeys() {
keys.add(S3G_HTTP_PORT_KEY);
keys.add(S3G_WEBADMIN_HTTP_PORT_KEY);
}
+ if (config.isReconEnabled()) {
+ keys.add(RECON_HTTP_PORT_KEY);
+ keys.add(RECON_HTTPS_PORT_KEY);
+ keys.add(RECON_DATANODE_PORT_KEY);
+ }
for (int index = 0; index < config.getDatanodes(); index++) {
for (String suffix : DATANODE_PORT_KEY_SUFFIXES) {
keys.add(datanodePortKey(index, suffix));
@@ -1084,15 +1230,17 @@ static final class PreparedConfiguration {
private final int scmPort;
private final int omPort;
private final int s3gPort;
+ private final int reconPort;
private final List<OzoneConfiguration> datanodeConfigurations;
- PreparedConfiguration(OzoneConfiguration configuration, int scmPort, int
omPort, int s3gPort,
+ PreparedConfiguration(OzoneConfiguration configuration, int scmPort, int
omPort, int s3gPort, int reconPort,
List<OzoneConfiguration> datanodeConfigurations) {
this.configuration = Objects.requireNonNull(configuration,
"configuration");
this.scmPort = scmPort;
this.omPort = omPort;
this.s3gPort = s3gPort;
+ this.reconPort = reconPort;
this.datanodeConfigurations = Collections.unmodifiableList(
new ArrayList<>(Objects.requireNonNull(datanodeConfigurations,
"datanodeConfigurations")));
@@ -1114,6 +1262,10 @@ int getS3gPort() {
return s3gPort;
}
+ int getReconPort() {
+ return reconPort;
+ }
+
List<OzoneConfiguration> getDatanodeConfigurations() {
return datanodeConfigurations;
}
diff --git
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
index e2d62d2f860..20c0de8219d 100644
---
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
+++
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneClusterConfig.java
@@ -43,6 +43,7 @@ public final class LocalOzoneClusterConfig {
static final String DEFAULT_DATANODES_VALUE = "1";
static final String DEFAULT_PORT_VALUE = "0";
static final String DEFAULT_S3G_ENABLED_VALUE = "true";
+ static final String DEFAULT_RECON_ENABLED_VALUE = "false";
static final String DEFAULT_EPHEMERAL_VALUE = "false";
static final String DEFAULT_STARTUP_TIMEOUT_VALUE = "PT2M";
@@ -60,6 +61,8 @@ public final class LocalOzoneClusterConfig {
static final int DEFAULT_PORT = Integer.parseInt(DEFAULT_PORT_VALUE);
static final boolean DEFAULT_S3G_ENABLED =
Boolean.parseBoolean(DEFAULT_S3G_ENABLED_VALUE);
+ static final boolean DEFAULT_RECON_ENABLED =
+ Boolean.parseBoolean(DEFAULT_RECON_ENABLED_VALUE);
static final boolean DEFAULT_EPHEMERAL =
Boolean.parseBoolean(DEFAULT_EPHEMERAL_VALUE);
static final Duration DEFAULT_STARTUP_TIMEOUT =
@@ -78,6 +81,8 @@ public final class LocalOzoneClusterConfig {
private final int omPort;
private final int s3gPort;
private final boolean s3gEnabled;
+ private final int reconPort;
+ private final boolean reconEnabled;
private final boolean ephemeral;
private final Duration startupTimeout;
@@ -94,6 +99,8 @@ private LocalOzoneClusterConfig(Builder builder) {
omPort = builder.omPort;
s3gPort = builder.s3gPort;
s3gEnabled = builder.s3gEnabled;
+ reconPort = builder.reconPort;
+ reconEnabled = builder.reconEnabled;
ephemeral = builder.ephemeral;
startupTimeout = Objects.requireNonNull(builder.startupTimeout,
"startupTimeout");
@@ -150,6 +157,18 @@ public boolean isS3gEnabled() {
return s3gEnabled;
}
+ /**
+ * Returns the Recon HTTP port. Port {@code 0} asks the runtime to choose an
+ * available local port.
+ */
+ public int getReconPort() {
+ return reconPort;
+ }
+
+ public boolean isReconEnabled() {
+ return reconEnabled;
+ }
+
/**
* Returns whether the local runtime should remove its data directory when it
* shuts down.
@@ -220,6 +239,8 @@ public static final class Builder {
private int omPort = DEFAULT_PORT;
private int s3gPort = DEFAULT_PORT;
private boolean s3gEnabled = DEFAULT_S3G_ENABLED;
+ private int reconPort = DEFAULT_PORT;
+ private boolean reconEnabled = DEFAULT_RECON_ENABLED;
private boolean ephemeral = DEFAULT_EPHEMERAL;
private Duration startupTimeout = DEFAULT_STARTUP_TIMEOUT;
@@ -267,6 +288,16 @@ public Builder setS3gEnabled(boolean value) {
return this;
}
+ public Builder setReconPort(int value) {
+ reconPort = value;
+ return this;
+ }
+
+ public Builder setReconEnabled(boolean value) {
+ reconEnabled = value;
+ return this;
+ }
+
public Builder setEphemeral(boolean value) {
ephemeral = value;
return this;
diff --git
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
index ed0176bd234..59198818afc 100644
---
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
+++
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneRuntime.java
@@ -71,6 +71,20 @@ public interface LocalOzoneRuntime extends AutoCloseable {
*/
String getS3Endpoint();
+ /**
+ * Returns the Recon HTTP port for this local runtime.
+ *
+ * @return Recon port, or -1 when Recon is disabled
+ */
+ int getReconPort();
+
+ /**
+ * Returns the full Recon endpoint shown to users.
+ *
+ * @return Recon endpoint, including scheme, host, and port; empty when
Recon is disabled
+ */
+ String getReconEndpoint();
+
/**
* Stops the local runtime and releases resources created during startup.
*
diff --git
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
index f2565361148..b0ce83d466f 100644
---
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
+++
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
@@ -60,6 +60,8 @@ public class OzoneLocal extends GenericCli {
static final String ENV_OM_PORT = "OZONE_LOCAL_OM_PORT";
static final String ENV_S3G_ENABLED = "OZONE_LOCAL_S3G_ENABLED";
static final String ENV_S3G_PORT = "OZONE_LOCAL_S3G_PORT";
+ static final String ENV_RECON_ENABLED = "OZONE_LOCAL_RECON_ENABLED";
+ static final String ENV_RECON_PORT = "OZONE_LOCAL_RECON_PORT";
static final String ENV_EPHEMERAL = "OZONE_LOCAL_EPHEMERAL";
static final String ENV_STARTUP_TIMEOUT = "OZONE_LOCAL_STARTUP_TIMEOUT";
@@ -83,6 +85,12 @@ public class OzoneLocal extends GenericCli {
+ LocalOzoneClusterConfig.DEFAULT_S3G_ENABLED_VALUE + "}";
private static final String DEFAULT_S3G_PORT_VALUE = "${env:" + ENV_S3G_PORT
+ ":-" + LocalOzoneClusterConfig.DEFAULT_PORT_VALUE + "}";
+ private static final String DEFAULT_RECON_ENABLED_VALUE = "${env:"
+ + ENV_RECON_ENABLED + ":-"
+ + LocalOzoneClusterConfig.DEFAULT_RECON_ENABLED_VALUE + "}";
+ private static final String DEFAULT_RECON_PORT_VALUE = "${env:"
+ + ENV_RECON_PORT + ":-" + LocalOzoneClusterConfig.DEFAULT_PORT_VALUE
+ + "}";
private static final String DEFAULT_EPHEMERAL_VALUE = "${env:"
+ ENV_EPHEMERAL + ":-"
+ LocalOzoneClusterConfig.DEFAULT_EPHEMERAL_VALUE + "}";
@@ -150,7 +158,7 @@ private StartupFailureException(Throwable cause) {
}
@Command(name = "run",
- description = "Start SCM, OM, datanodes, and optional S3 Gateway in one
local process")
+ description = "Start SCM, OM, datanodes, and optional S3 Gateway and
Recon in one local process")
static class RunCommand extends AbstractSubcommand implements Callable<Void>
{
@Option(names = "--data-dir",
@@ -201,6 +209,18 @@ static class RunCommand extends AbstractSubcommand
implements Callable<Void> {
description = "Enable S3 Gateway")
private boolean s3gEnabled;
+ @Option(names = "--recon-port",
+ defaultValue = DEFAULT_RECON_PORT_VALUE,
+ description = "Recon HTTP port (0 means auto-allocate)")
+ private int reconPort;
+
+ @Option(names = "--recon",
+ negatable = true,
+ defaultValue = DEFAULT_RECON_ENABLED_VALUE,
+ fallbackValue = "true",
+ description = "Enable Recon")
+ private boolean reconEnabled;
+
@Option(names = "--ephemeral",
negatable = true,
defaultValue = DEFAULT_EPHEMERAL_VALUE,
@@ -253,6 +273,9 @@ private void printSummary(LocalOzoneRuntime runtime,
LocalOzoneClusterConfig con
writer.println("Use path-style addressing (AWS CLI: aws configure set
default.s3.addressing_style path).");
writer.println("With security off (the default), the S3 Gateway
accepts any credentials.");
}
+ if (config.isReconEnabled()) {
+ writer.println("Recon endpoint: " + runtime.getReconEndpoint());
+ }
writer.println("Press Ctrl+C to stop.");
writer.flush();
}
@@ -292,6 +315,7 @@ LocalOzoneClusterConfig resolveConfig() {
validatePort(scmPort, "--scm-port");
validatePort(omPort, "--om-port");
validatePort(s3gPort, "--s3g-port");
+ validatePort(reconPort, "--recon-port");
validateStartupTimeout();
return LocalOzoneClusterConfig.builder(dataDir)
@@ -303,6 +327,8 @@ LocalOzoneClusterConfig resolveConfig() {
.setOmPort(omPort)
.setS3gPort(s3gPort)
.setS3gEnabled(s3gEnabled)
+ .setReconPort(reconPort)
+ .setReconEnabled(reconEnabled)
.setEphemeral(ephemeral)
.setStartupTimeout(startupTimeout)
.build();
diff --git
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
index 6da5f287e24..0bdffc4d304 100644
---
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
+++
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
@@ -23,6 +23,8 @@
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE;
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION;
import static
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_HTTP_ADDRESS_KEY;
+import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD;
import static
org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_CONTAINER_RATIS_ENABLED_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_DATANODE_DIR_KEY;
import static
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_ADDRESS_KEY;
@@ -38,6 +40,7 @@
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_MINIMUM_TIMEOUT_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_HTTP_ENABLED_KEY;
import static
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTPS_ADDRESS_KEY;
import static
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_ADDRESS_KEY;
import static
org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_HTTP_BIND_HOST_KEY;
@@ -60,7 +63,10 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hadoop.hdds.client.ReplicationFactor;
import org.apache.hadoop.hdds.client.ReplicationType;
@@ -414,6 +420,150 @@ void freshPortSkipsPersistedPorts() throws Exception {
assertEquals(40000, allocator.reserve(40000));
}
+ /**
+ * configureRecon() runs after configureLocalDefaults(), so the value has to
be rejected where it is applied
+ * rather than by a check that only covers the keys set earlier.
+ */
+ @Test
+ void reconOverrideRejectsConflictingUserValue() {
+ OzoneConfiguration seed = new OzoneConfiguration();
+ seed.set(OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD, "9m",
"test-ozone-site.xml");
+ LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(
+ tempDir.resolve("local-ozone"))
+ .setReconEnabled(true)
+ .build();
+
+ IOException error = assertPrepareFails(config, seed);
+
+ String message = error.getMessage();
+ assertTrue(message.contains(OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD),
message);
+ assertTrue(message.contains("9m"), message);
+ }
+
+ @Test
+ void reconOverrideAcceptsEquivalentDurationSpelling() throws Exception {
+ OzoneConfiguration seed = new OzoneConfiguration();
+ seed.set(OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD, "10000ms",
"test-ozone-site.xml");
+ LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(
+ tempDir.resolve("local-ozone"))
+ .setReconEnabled(true)
+ .build();
+
+ try (LocalOzoneCluster cluster = new LocalOzoneCluster(config, seed)) {
+ assertEquals("10s", cluster.prepareConfiguration().getConfiguration()
+ .get(OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD));
+ }
+ }
+
+ /**
+ * Regression guard: the Recon default ships in ozone-recon-default.xml, so
counting only
+ * ozone-default.xml as shipped would reject every run with Recon enabled.
+ */
+ @Test
+ void reconDefaultFromModuleXmlIsNotTreatedAsUserConfig() throws Exception {
+ LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(
+ tempDir.resolve("local-ozone"))
+ .setReconEnabled(true)
+ .build();
+
+ try (LocalOzoneCluster cluster = new LocalOzoneCluster(config,
+ new OzoneConfiguration())) {
+ assertEquals("10s", cluster.prepareConfiguration().getConfiguration()
+ .get(OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD));
+ }
+ }
+
+ @Test
+ void prepareConfigurationReservesReconPortsWhenEnabled() throws Exception {
+ Path dataDir = tempDir.resolve("local-ozone");
+ LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+ .setReconEnabled(true)
+ .setReconPort(9888)
+ .build();
+
+ try (LocalOzoneCluster cluster = newCluster(config)) {
+ LocalOzoneCluster.PreparedConfiguration prepared =
+ cluster.prepareConfiguration();
+ OzoneConfiguration conf = prepared.getConfiguration();
+
+ assertEquals(9888, prepared.getReconPort());
+ assertEquals(9888, cluster.getReconPort());
+ assertEquals("http://127.0.0.1:9888", cluster.getReconEndpoint());
+ assertTrue(conf.get(OZONE_RECON_HTTP_ADDRESS_KEY).endsWith(":9888"));
+ assertTrue(Files.isDirectory(dataDir.resolve("recon")));
+ }
+ Properties properties = loadPortState(dataDir);
+ assertPositivePort(properties, "recon.http");
+ assertPositivePort(properties, "recon.https");
+ assertPositivePort(properties, "recon.datanode");
+ }
+
+ @Test
+ void prepareConfigurationSkipsReconWhenDisabled() throws Exception {
+ Path dataDir = tempDir.resolve("local-ozone");
+ LocalOzoneClusterConfig config =
+ LocalOzoneClusterConfig.builder(dataDir).build();
+
+ try (LocalOzoneCluster cluster = newCluster(config)) {
+ LocalOzoneCluster.PreparedConfiguration prepared =
+ cluster.prepareConfiguration();
+
+ assertEquals(-1, prepared.getReconPort());
+ assertEquals(-1, cluster.getReconPort());
+ assertEquals("", cluster.getReconEndpoint());
+ }
+ assertFalse(loadPortState(dataDir).stringPropertyNames().stream()
+ .anyMatch(key -> key.startsWith("recon.")));
+ }
+
+ @Test
+ void prepareConfigurationRejectsDisabledReconHttpListener() {
+ OzoneConfiguration seed = new OzoneConfiguration();
+ seed.setBoolean(OZONE_RECON_HTTP_ENABLED_KEY, false);
+ LocalOzoneClusterConfig config =
LocalOzoneClusterConfig.builder(tempDir.resolve("local-ozone"))
+ .setReconEnabled(true)
+ .build();
+
+ IOException error = assertPrepareFails(config, seed);
+
+ assertMessageContains(error, OZONE_RECON_HTTP_ENABLED_KEY);
+ }
+
+ @ParameterizedTest
+ @EnumSource(HttpConfig.Policy.class)
+ void prepareConfigurationChecksHttpPolicyForRecon(HttpConfig.Policy policy)
throws Exception {
+ OzoneConfiguration seed = new OzoneConfiguration();
+ seed.set(OZONE_HTTP_POLICY_KEY, policy.name(), "test-ozone-site.xml");
+ LocalOzoneClusterConfig reconOnly =
LocalOzoneClusterConfig.builder(tempDir.resolve("local-ozone"))
+ .setS3gEnabled(false)
+ .setReconEnabled(true)
+ .build();
+
+ if (policy.isHttpEnabled()) {
+ try (LocalOzoneCluster cluster = new LocalOzoneCluster(reconOnly, seed))
{
+ cluster.prepareConfiguration();
+ }
+ } else {
+ IOException error = assertPrepareFails(reconOnly, seed);
+ assertMessageContains(error, OZONE_HTTP_POLICY_KEY);
+ assertMessageContains(error, "test-ozone-site.xml");
+ }
+ }
+
+ @Test
+ void formatNeverRejectsPortStateMissingReconPorts() throws Exception {
+ Path dataDir = tempDir.resolve("local-ozone");
+ prepare(LocalOzoneClusterConfig.builder(dataDir).build());
+ LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+ .setFormatMode(LocalOzoneClusterConfig.FormatMode.NEVER)
+ .setReconEnabled(true)
+ .build();
+
+ IOException error = assertPrepareFails(config);
+
+ assertMessageContains(error, "recon.");
+ }
+
@Test
void formatNeverRejectsPortStateMissingS3GatewayPorts() throws Exception {
Path dataDir = tempDir.resolve("local-ozone");
@@ -783,6 +933,46 @@ void getDatanodeCountReturnsZeroBeforeStart() throws
Exception {
}
}
+ @Test
+ void executeWithinStartupTimeoutReturnsStartupResult() throws Exception {
+ assertEquals(0, LocalOzoneCluster.executeWithinStartupTimeout("Recon", ()
-> 0,
+ () -> { }, Duration.ofSeconds(30)));
+ }
+
+ @Test
+ void executeWithinStartupTimeoutFailsFastWhenStartupBlocks() throws
Exception {
+ CountDownLatch interrupted = new CountDownLatch(1);
+
+ IOException error =
+ assertThrows(IOException.class, () ->
LocalOzoneCluster.executeWithinStartupTimeout(
+ "Recon", () -> {
+ try {
+ Thread.sleep(TimeUnit.MINUTES.toMillis(5));
+ } catch (InterruptedException e) {
+ interrupted.countDown();
+ Thread.currentThread().interrupt();
+ }
+ return 0;
+ }, () -> { }, Duration.ofMillis(200)));
+
+ assertMessageContains(error, "did not start within");
+ assertTrue(interrupted.await(30, TimeUnit.SECONDS),
+ "blocked startup should be interrupted on timeout");
+ }
+
+ @Test
+ void executeWithinStartupTimeoutPropagatesStartupFailure() {
+ AtomicBoolean cleanedUp = new AtomicBoolean();
+ IOException error =
+ assertThrows(IOException.class, () ->
LocalOzoneCluster.executeWithinStartupTimeout(
+ "Recon", () -> {
+ throw new IOException("startup boom");
+ }, () -> cleanedUp.set(true), Duration.ofSeconds(30)));
+
+ assertMessageContains(error, "startup boom");
+ assertTrue(cleanedUp.get(), "a failed startup should run its cleanup");
+ }
+
private LocalOzoneCluster.PreparedConfiguration prepare(
LocalOzoneClusterConfig config) throws IOException {
try (LocalOzoneCluster cluster = newCluster(config)) {
diff --git
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
index 1f9fddbb965..845249fb3f3 100644
---
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
+++
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneClusterConfig.java
@@ -46,6 +46,8 @@ void builderProvidesLocalClusterDefaults() {
assertEquals(0, config.getOmPort());
assertEquals(0, config.getS3gPort());
assertTrue(config.isS3gEnabled());
+ assertEquals(0, config.getReconPort());
+ assertFalse(config.isReconEnabled());
assertFalse(config.isEphemeral());
assertEquals(Duration.ofMinutes(2), config.getStartupTimeout());
}
@@ -63,6 +65,9 @@ void typedDefaultsMatchSharedFallbackValues() {
assertEquals(Boolean.parseBoolean(
LocalOzoneClusterConfig.DEFAULT_S3G_ENABLED_VALUE),
LocalOzoneClusterConfig.DEFAULT_S3G_ENABLED);
+ assertEquals(Boolean.parseBoolean(
+ LocalOzoneClusterConfig.DEFAULT_RECON_ENABLED_VALUE),
+ LocalOzoneClusterConfig.DEFAULT_RECON_ENABLED);
assertEquals(Boolean.parseBoolean(
LocalOzoneClusterConfig.DEFAULT_EPHEMERAL_VALUE),
LocalOzoneClusterConfig.DEFAULT_EPHEMERAL);
@@ -83,6 +88,8 @@ void builderAcceptsExplicitOverrides() {
.setOmPort(9862)
.setS3gPort(9878)
.setS3gEnabled(false)
+ .setReconPort(9888)
+ .setReconEnabled(true)
.setEphemeral(true)
.setStartupTimeout(Duration.ofSeconds(45))
.build();
@@ -98,6 +105,8 @@ void builderAcceptsExplicitOverrides() {
assertEquals(9862, config.getOmPort());
assertEquals(9878, config.getS3gPort());
assertFalse(config.isS3gEnabled());
+ assertEquals(9888, config.getReconPort());
+ assertTrue(config.isReconEnabled());
assertTrue(config.isEphemeral());
assertEquals(Duration.ofSeconds(45), config.getStartupTimeout());
}
diff --git
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
index 9427be1d502..1efaa89615e 100644
---
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
+++
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
@@ -127,6 +127,40 @@ void runCommandStartsRuntimeAndPrintsStartupSummary()
throws Exception {
assertTrue(text.contains("Press Ctrl+C to stop."), text);
}
+ @Test
+ void runCommandPrintsReconEndpointWhenEnabled() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ StubRuntime runtime = new StubRuntime("localhost", 9860, 9862,
+ "http://localhost:9878");
+ runtime.reconEndpoint = "http://localhost:9888";
+ TestableRunCommand command = new TestableRunCommand(runtime);
+ CommandLine commandLine = new CommandLine(command);
+ commandLine.setOut(new PrintWriter(new OutputStreamWriter(out, UTF_8),
+ true));
+
+ int exitCode = commandLine.execute("--recon");
+
+ assertEquals(0, exitCode);
+ String text = out.toString(UTF_8.name());
+ assertTrue(text.contains("Recon endpoint: http://localhost:9888"), text);
+ }
+
+ @Test
+ void runCommandOmitsReconEndpointWhenDisabled() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ StubRuntime runtime = new StubRuntime("localhost", 9860, 9862,
+ "http://localhost:9878");
+ TestableRunCommand command = new TestableRunCommand(runtime);
+ CommandLine commandLine = new CommandLine(command);
+ commandLine.setOut(new PrintWriter(new OutputStreamWriter(out, UTF_8),
+ true));
+
+ int exitCode = commandLine.execute();
+
+ assertEquals(0, exitCode);
+ assertFalse(out.toString(UTF_8.name()).contains("Recon endpoint:"));
+ }
+
@Test
void runCommandOmitsS3SummaryWhenS3gDisabled() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -277,6 +311,10 @@ void runCommandOptionsUseEnvironmentDefaults() throws
Exception {
LocalOzoneClusterConfig.DEFAULT_S3G_ENABLED_VALUE);
assertEnvDefault("s3gPort", OzoneLocal.ENV_S3G_PORT,
LocalOzoneClusterConfig.DEFAULT_PORT_VALUE);
+ assertEnvDefault("reconEnabled", OzoneLocal.ENV_RECON_ENABLED,
+ LocalOzoneClusterConfig.DEFAULT_RECON_ENABLED_VALUE);
+ assertEnvDefault("reconPort", OzoneLocal.ENV_RECON_PORT,
+ LocalOzoneClusterConfig.DEFAULT_PORT_VALUE);
assertEnvDefault("ephemeral", OzoneLocal.ENV_EPHEMERAL,
LocalOzoneClusterConfig.DEFAULT_EPHEMERAL_VALUE);
assertEnvDefault("startupTimeout", OzoneLocal.ENV_STARTUP_TIMEOUT,
@@ -298,6 +336,8 @@ void resolveConfigUsesPicocliDefaults() {
assertEquals(0, config.getOmPort());
assertEquals(0, config.getS3gPort());
assertTrue(config.isS3gEnabled());
+ assertEquals(0, config.getReconPort());
+ assertFalse(config.isReconEnabled());
assertFalse(config.isEphemeral());
assertEquals(Duration.ofMinutes(2), config.getStartupTimeout());
}
@@ -319,6 +359,8 @@ void resolveConfigUsesCliOverrides() {
"--om-port", "201",
"--s3g-port", "202",
"--no-s3g",
+ "--recon-port", "203",
+ "--recon",
"--ephemeral",
"--startup-timeout", "45s");
@@ -333,6 +375,8 @@ void resolveConfigUsesCliOverrides() {
assertEquals(201, config.getOmPort());
assertEquals(202, config.getS3gPort());
assertFalse(config.isS3gEnabled());
+ assertEquals(203, config.getReconPort());
+ assertTrue(config.isReconEnabled());
assertTrue(config.isEphemeral());
assertEquals(Duration.ofSeconds(45), config.getStartupTimeout());
}
@@ -352,6 +396,18 @@ void resolveConfigAllowsS3gAndEphemeralToBeNegated() {
assertFalse(config.isEphemeral());
}
+ @Test
+ void resolveConfigAllowsReconToBeNegated() {
+ LocalOzoneClusterConfig config = resolve("--no-recon");
+
+ assertFalse(config.isReconEnabled());
+ }
+
+ @Test
+ void resolveConfigRejectsInvalidReconPort() {
+ assertConfigError("--recon-port", "65536", "--recon-port");
+ }
+
@Test
void resolveConfigRejectsInvalidFormat() {
assertParseError("--format", "sometimes", "sometimes");
@@ -499,6 +555,7 @@ private static final class StubRuntime implements
LocalOzoneRuntime {
private final int scmPort;
private final int omPort;
private final String s3Endpoint;
+ private String reconEndpoint = "";
private boolean failStart;
private boolean started;
private boolean closed;
@@ -543,6 +600,16 @@ public String getS3Endpoint() {
return s3Endpoint;
}
+ @Override
+ public int getReconPort() {
+ return 0;
+ }
+
+ @Override
+ public String getReconEndpoint() {
+ return reconEndpoint;
+ }
+
@Override
public void close() {
closed = true;
@@ -570,8 +637,12 @@ public String defaultValue(ArgSpec argSpec) {
|| "--om-port".equals(option)
|| "--s3g-port".equals(option)) {
return LocalOzoneClusterConfig.DEFAULT_PORT_VALUE;
+ } else if ("--recon-port".equals(option)) {
+ return LocalOzoneClusterConfig.DEFAULT_PORT_VALUE;
} else if ("--s3g".equals(option)) {
return LocalOzoneClusterConfig.DEFAULT_S3G_ENABLED_VALUE;
+ } else if ("--recon".equals(option)) {
+ return LocalOzoneClusterConfig.DEFAULT_RECON_ENABLED_VALUE;
} else if ("--ephemeral".equals(option)) {
return LocalOzoneClusterConfig.DEFAULT_EPHEMERAL_VALUE;
} else if ("--startup-timeout".equals(option)) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]