This is an automated email from the ASF dual-hosted git repository.
sumitagrawal 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 91d41a0d1e HDDS-11465. Introducing Schema Versioning for Recon to
Handle Fresh Installs and Upgrades. (#7213)
91d41a0d1e is described below
commit 91d41a0d1ee276b8f73aa66a0ef8d09a3808d5af
Author: Arafat2198 <[email protected]>
AuthorDate: Mon Oct 28 13:21:22 2024 +0530
HDDS-11465. Introducing Schema Versioning for Recon to Handle Fresh
Installs and Upgrades. (#7213)
---
.../recon/codegen/ReconSchemaGenerationModule.java | 2 +
.../recon/schema/SchemaVersionTableDefinition.java | 69 +++++
.../apache/hadoop/ozone/recon/ReconContext.java | 5 +-
.../recon/ReconSchemaVersionTableManager.java | 108 ++++++++
.../org/apache/hadoop/ozone/recon/ReconServer.java | 14 +
.../ozone/recon/upgrade/ReconLayoutFeature.java | 103 ++++++++
.../recon/upgrade/ReconLayoutVersionManager.java | 141 ++++++++++
.../ozone/recon/upgrade/ReconUpgradeAction.java | 49 ++++
.../ozone/recon/upgrade/UpgradeActionRecon.java | 70 +++++
.../hadoop/ozone/recon/upgrade/package-info.java | 29 +++
.../TestSchemaVersionTableDefinition.java | 123 +++++++++
.../upgrade/TestReconLayoutVersionManager.java | 288 +++++++++++++++++++++
12 files changed, 1000 insertions(+), 1 deletion(-)
diff --git
a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java
b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java
index 8272c2bd6d..d59ab8acd6 100644
---
a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java
+++
b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java
@@ -22,6 +22,7 @@ import
org.hadoop.ozone.recon.schema.ReconTaskSchemaDefinition;
import org.hadoop.ozone.recon.schema.ReconSchemaDefinition;
import org.hadoop.ozone.recon.schema.StatsSchemaDefinition;
import org.hadoop.ozone.recon.schema.UtilizationSchemaDefinition;
+import org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
@@ -40,5 +41,6 @@ public class ReconSchemaGenerationModule extends
AbstractModule {
schemaBinder.addBinding().to(ContainerSchemaDefinition.class);
schemaBinder.addBinding().to(ReconTaskSchemaDefinition.class);
schemaBinder.addBinding().to(StatsSchemaDefinition.class);
+ schemaBinder.addBinding().to(SchemaVersionTableDefinition.class);
}
}
diff --git
a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java
b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java
new file mode 100644
index 0000000000..f7e538f31a
--- /dev/null
+++
b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java
@@ -0,0 +1,69 @@
+/*
+ * 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.hadoop.ozone.recon.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.jooq.DSLContext;
+import org.jooq.impl.DSL;
+import org.jooq.impl.SQLDataType;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import static org.hadoop.ozone.recon.codegen.SqlDbUtils.TABLE_EXISTS_CHECK;
+
+/**
+ * Class for managing the schema of the SchemaVersion table.
+ */
+@Singleton
+public class SchemaVersionTableDefinition implements ReconSchemaDefinition {
+
+ public static final String SCHEMA_VERSION_TABLE_NAME =
"RECON_SCHEMA_VERSION";
+ private final DataSource dataSource;
+ private DSLContext dslContext;
+
+ @Inject
+ public SchemaVersionTableDefinition(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ public void initializeSchema() throws SQLException {
+ Connection conn = dataSource.getConnection();
+ dslContext = DSL.using(conn);
+
+ if (!TABLE_EXISTS_CHECK.test(conn, SCHEMA_VERSION_TABLE_NAME)) {
+ createSchemaVersionTable();
+ }
+ }
+
+ /**
+ * Create the Schema Version table.
+ */
+ private void createSchemaVersionTable() throws SQLException {
+ dslContext.createTableIfNotExists(SCHEMA_VERSION_TABLE_NAME)
+ .column("version_number", SQLDataType.INTEGER.nullable(false))
+ .column("applied_on",
SQLDataType.TIMESTAMP.defaultValue(DSL.currentTimestamp()))
+ .execute();
+ }
+
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java
index c9875cb826..a98603a7e9 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java
@@ -128,7 +128,10 @@ public final class ReconContext {
Arrays.asList("Overview (OM Data)", "OM DB Insights")),
GET_SCM_DB_SNAPSHOT_FAILED(
"SCM DB Snapshot sync failed !!!",
- Arrays.asList("Containers", "Pipelines"));
+ Arrays.asList("Containers", "Pipelines")),
+ UPGRADE_FAILURE(
+ "Schema upgrade failed. Recon encountered an issue while finalizing
the layout upgrade.",
+ Arrays.asList("Recon startup", "Metadata Layout Version"));
private final String message;
private final List<String> impacts;
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java
new file mode 100644
index 0000000000..d7c3c65f2c
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java
@@ -0,0 +1,108 @@
+/*
+ * 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.recon;
+
+import com.google.inject.Inject;
+import org.jooq.DSLContext;
+import org.jooq.impl.DSL;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.SQLException;
+
+import static org.jooq.impl.DSL.name;
+
+/**
+ * Manager for handling the Recon Schema Version table.
+ * This class provides methods to get and update the current schema version.
+ */
+public class ReconSchemaVersionTableManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ReconSchemaVersionTableManager.class);
+ public static final String RECON_SCHEMA_VERSION_TABLE_NAME =
"RECON_SCHEMA_VERSION";
+ private final DSLContext dslContext;
+ private final DataSource dataSource;
+
+ @Inject
+ public ReconSchemaVersionTableManager(DataSource dataSource) throws
SQLException {
+ this.dataSource = dataSource;
+ this.dslContext = DSL.using(dataSource.getConnection());
+ }
+
+ /**
+ * Get the current schema version from the RECON_SCHEMA_VERSION table.
+ * If the table is empty, or if it does not exist, it will return 0.
+ * @return The current schema version.
+ */
+ public int getCurrentSchemaVersion() throws SQLException {
+ try {
+ return dslContext.select(DSL.field(name("version_number")))
+ .from(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME))
+ .fetchOptional()
+ .map(record -> record.get(
+ DSL.field(name("version_number"), Integer.class)))
+ .orElse(-1); // Return -1 if no version is found
+ } catch (Exception e) {
+ LOG.error("Failed to fetch the current schema version.", e);
+ throw new SQLException("Unable to read schema version from the table.",
e);
+ }
+ }
+
+ /**
+ * Update the schema version in the RECON_SCHEMA_VERSION table after all
tables are upgraded.
+ *
+ * @param newVersion The new version to set.
+ */
+ public void updateSchemaVersion(int newVersion) throws SQLException {
+ try {
+ boolean recordExists = dslContext.fetchExists(dslContext.selectOne()
+ .from(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME)));
+
+ if (recordExists) {
+ // Update the existing schema version record
+ dslContext.update(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME))
+ .set(DSL.field(name("version_number")), newVersion)
+ .set(DSL.field(name("applied_on")), DSL.currentTimestamp())
+ .execute();
+ LOG.info("Updated schema version to '{}'.", newVersion);
+ } else {
+ // Insert a new schema version record
+ dslContext.insertInto(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME))
+ .columns(DSL.field(name("version_number")),
+ DSL.field(name("applied_on")))
+ .values(newVersion, DSL.currentTimestamp())
+ .execute();
+ LOG.info("Inserted new schema version '{}'.", newVersion);
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to update schema version to '{}'.", newVersion, e);
+ throw new SQLException("Unable to update schema version in the table.",
e);
+ }
+ }
+
+ /**
+ * Provides the data source used by this manager.
+ * @return The DataSource instance.
+ */
+ public DataSource getDataSource() {
+ return dataSource;
+ }
+}
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 3295eb4524..7c9564c23b 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
@@ -42,6 +42,7 @@ import
org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
import org.apache.hadoop.ozone.recon.spi.impl.ReconDBProvider;
+import org.apache.hadoop.ozone.recon.upgrade.ReconLayoutVersionManager;
import org.apache.hadoop.ozone.util.OzoneNetUtils;
import org.apache.hadoop.ozone.util.OzoneVersionInfo;
import org.apache.hadoop.ozone.util.ShutdownHookManager;
@@ -105,6 +106,7 @@ public class ReconServer extends GenericCli {
ReconServer.class, originalArgs, LOG, configuration);
ConfigurationProvider.setConfiguration(configuration);
+
injector = Guice.createInjector(new ReconControllerModule(),
new ReconRestServletModule(configuration),
new ReconSchemaGenerationModule());
@@ -136,8 +138,11 @@ public class ReconServer extends GenericCli {
this.reconNamespaceSummaryManager =
injector.getInstance(ReconNamespaceSummaryManager.class);
+ ReconContext reconContext = injector.getInstance(ReconContext.class);
+
ReconSchemaManager reconSchemaManager =
injector.getInstance(ReconSchemaManager.class);
+
LOG.info("Creating Recon Schema.");
reconSchemaManager.createReconSchema();
LOG.debug("Recon schema creation done.");
@@ -153,6 +158,15 @@ public class ReconServer extends GenericCli {
this.reconTaskStatusMetrics =
injector.getInstance(ReconTaskStatusMetrics.class);
+ // Handle Recon Schema Versioning
+ ReconSchemaVersionTableManager versionTableManager =
+ injector.getInstance(ReconSchemaVersionTableManager.class);
+
+ ReconLayoutVersionManager layoutVersionManager =
+ new ReconLayoutVersionManager(versionTableManager, reconContext);
+ // Run the upgrade framework to finalize layout features if needed
+ layoutVersionManager.finalizeLayoutFeatures();
+
LOG.info("Initializing support of Recon Features...");
FeatureProvider.initFeatureSupport(configuration);
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java
new file mode 100644
index 0000000000..96969c9f3d
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java
@@ -0,0 +1,103 @@
+/*
+ * 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.recon.upgrade;
+
+import org.reflections.Reflections;
+
+import java.util.EnumMap;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Enum representing Recon layout features with their version, description,
+ * and associated upgrade action to be executed during an upgrade.
+ */
+public enum ReconLayoutFeature {
+ // Represents the starting point for Recon's layout versioning system.
+ INITIAL_VERSION(0, "Recon Layout Versioning Introduction");
+
+ private final int version;
+ private final String description;
+ private final EnumMap<ReconUpgradeAction.UpgradeActionType,
ReconUpgradeAction> actions =
+ new EnumMap<>(ReconUpgradeAction.UpgradeActionType.class);
+
+ ReconLayoutFeature(final int version, String description) {
+ this.version = version;
+ this.description = description;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Retrieves the upgrade action for the specified {@link
ReconUpgradeAction.UpgradeActionType}.
+ *
+ * @param type The type of the upgrade action (e.g., FINALIZE).
+ * @return An {@link Optional} containing the upgrade action if present.
+ */
+ public Optional<ReconUpgradeAction>
getAction(ReconUpgradeAction.UpgradeActionType type) {
+ return Optional.ofNullable(actions.get(type));
+ }
+
+ /**
+ * Associates a given upgrade action with a specific upgrade phase for this
feature.
+ *
+ * @param type The phase/type of the upgrade action.
+ * @param action The upgrade action to associate with this feature.
+ */
+ public void addAction(ReconUpgradeAction.UpgradeActionType type,
ReconUpgradeAction action) {
+ actions.put(type, action);
+ }
+
+ /**
+ * Scans the classpath for all classes annotated with {@link
UpgradeActionRecon}
+ * and registers their upgrade actions for the corresponding feature and
phase.
+ * This method dynamically loads and registers all upgrade actions based on
their
+ * annotations.
+ */
+ public static void registerUpgradeActions() {
+ Reflections reflections = new
Reflections("org.apache.hadoop.ozone.recon.upgrade");
+ Set<Class<?>> actionClasses =
reflections.getTypesAnnotatedWith(UpgradeActionRecon.class);
+
+ for (Class<?> actionClass : actionClasses) {
+ try {
+ ReconUpgradeAction action = (ReconUpgradeAction)
actionClass.getDeclaredConstructor().newInstance();
+ UpgradeActionRecon annotation =
actionClass.getAnnotation(UpgradeActionRecon.class);
+ annotation.feature().addAction(annotation.type(), action);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to register upgrade action: " +
actionClass.getSimpleName(), e);
+ }
+ }
+ }
+
+ /**
+ * Returns the list of all layout feature values.
+ *
+ * @return An array of all {@link ReconLayoutFeature} values.
+ */
+ public static ReconLayoutFeature[] getValues() {
+ return ReconLayoutFeature.values();
+ }
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java
new file mode 100644
index 0000000000..b646f6d9a8
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java
@@ -0,0 +1,141 @@
+/**
+ * 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.recon.upgrade;
+
+import org.apache.hadoop.ozone.recon.ReconContext;
+import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+/**
+ * ReconLayoutVersionManager is responsible for managing the layout version of
the Recon service.
+ * It determines the current Metadata Layout Version (MLV) and Software Layout
Version (SLV) of the
+ * Recon service, and finalizes the layout features that need to be upgraded.
+ */
+public class ReconLayoutVersionManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ReconLayoutVersionManager.class);
+
+ private final ReconSchemaVersionTableManager schemaVersionTableManager;
+ private final ReconContext reconContext;
+
+ // Metadata Layout Version (MLV) of the Recon Metadata on disk
+ private int currentMLV;
+
+ public ReconLayoutVersionManager(ReconSchemaVersionTableManager
schemaVersionTableManager,
+ ReconContext reconContext)
+ throws SQLException {
+ this.schemaVersionTableManager = schemaVersionTableManager;
+ this.currentMLV = determineMLV();
+ this.reconContext = reconContext;
+ ReconLayoutFeature.registerUpgradeActions(); // Register actions via
annotation
+ }
+
+ /**
+ * Determines the current Metadata Layout Version (MLV) from the version
table.
+ * @return The current Metadata Layout Version (MLV).
+ */
+ private int determineMLV() throws SQLException {
+ return schemaVersionTableManager.getCurrentSchemaVersion();
+ }
+
+ /**
+ * Determines the Software Layout Version (SLV) based on the latest feature
version.
+ * @return The Software Layout Version (SLV).
+ */
+ private int determineSLV() {
+ return Arrays.stream(ReconLayoutFeature.values())
+ .mapToInt(ReconLayoutFeature::getVersion)
+ .max()
+ .orElse(0); // Default to 0 if no features are defined
+ }
+
+ /**
+ * Finalizes the layout features that need to be upgraded, by executing the
upgrade action for each
+ * feature that is registered for finalization.
+ */
+ public void finalizeLayoutFeatures() {
+ // Get features that need finalization, sorted by version
+ List<ReconLayoutFeature> featuresToFinalize = getRegisteredFeatures();
+
+ for (ReconLayoutFeature feature : featuresToFinalize) {
+ try {
+ // Fetch only the FINALIZE action for the feature
+ Optional<ReconUpgradeAction> action =
feature.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE);
+ if (action.isPresent()) {
+ // Execute the upgrade action & update the schema version in the DB
+ action.get().execute();
+ updateSchemaVersion(feature.getVersion());
+ LOG.info("Feature versioned {} finalized successfully.",
feature.getVersion());
+ }
+ } catch (Exception e) {
+ // Log the error to both logs and ReconContext
+ LOG.error("Failed to finalize feature {}: {}", feature.getVersion(),
e.getMessage());
+ reconContext.updateErrors(ReconContext.ErrorCode.UPGRADE_FAILURE);
+ reconContext.updateHealthStatus(new AtomicBoolean(false));
+ // Stop further upgrades as an error occurred
+ throw new RuntimeException("Recon failed to finalize layout feature.
Startup halted.");
+ }
+ }
+ }
+
+ /**
+ * Returns a list of ReconLayoutFeature objects that are registered for
finalization.
+ */
+ protected List<ReconLayoutFeature> getRegisteredFeatures() {
+ List<ReconLayoutFeature> allFeatures =
+ Arrays.asList(ReconLayoutFeature.values());
+
+ LOG.info("Current MLV: {}. SLV: {}. Checking features for
registration...", currentMLV, determineSLV());
+
+ List<ReconLayoutFeature> registeredFeatures = allFeatures.stream()
+ .filter(feature -> feature.getVersion() > currentMLV)
+ .sorted((a, b) -> Integer.compare(a.getVersion(), b.getVersion())) //
Sort by version in ascending order
+ .collect(Collectors.toList());
+
+ return registeredFeatures;
+ }
+
+ /**
+ * Updates the Metadata Layout Version (MLV) in the database after
finalizing a feature.
+ * @param newVersion The new Metadata Layout Version (MLV) to set.
+ */
+ private void updateSchemaVersion(int newVersion) throws SQLException {
+ schemaVersionTableManager.updateSchemaVersion(newVersion);
+ this.currentMLV = newVersion;
+ LOG.info("MLV updated to: " + newVersion);
+ }
+
+ public int getCurrentMLV() {
+ return currentMLV;
+ }
+
+ public int getCurrentSLV() {
+ return determineSLV();
+ }
+
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java
new file mode 100644
index 0000000000..f09cdf8e1f
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java
@@ -0,0 +1,49 @@
+/*
+ * 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.recon.upgrade;
+
+/**
+ * ReconUpgradeAction is an interface for executing upgrade actions in Recon.
+ */
+public interface ReconUpgradeAction {
+
+ /**
+ * Defines the different phases during which upgrade actions can be executed.
+ * Each action type corresponds to a specific point in the upgrade process:
+ *
+ * - FINALIZE: This action is executed automatically during the startup
+ * of Recon when it finalizes the layout upgrade. It ensures that all
necessary
+ * upgrades or schema changes are applied to bring the system in sync with
+ * the latest version.
+ */
+ enum UpgradeActionType {
+ FINALIZE
+ }
+
+ /**
+ * Execute the upgrade action.
+ */
+ void execute() throws Exception;
+
+ /**
+ * Provides the type of upgrade phase (e.g., FINALIZE).
+ */
+ UpgradeActionType getType();
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UpgradeActionRecon.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UpgradeActionRecon.java
new file mode 100644
index 0000000000..749e5f5c06
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UpgradeActionRecon.java
@@ -0,0 +1,70 @@
+/*
+ * 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.recon.upgrade;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The {@code UpgradeActionRecon} annotation is used to specify
+ * upgrade actions that should be executed during particular phases
+ * of the Recon service layout upgrade process.
+ *
+ * <p>This annotation can be used to associate an upgrade action
+ * class with a specific layout feature and upgrade phase. The
+ * framework will dynamically discover these annotated upgrade
+ * actions and execute them based on the feature's version and
+ * the defined action type (e.g., {@link
ReconUpgradeAction.UpgradeActionType#FINALIZE}).
+ *
+ * <p>The annotation is retained at runtime, allowing the reflection-based
+ * mechanism to scan for annotated classes, register the associated actions,
+ * and execute them as necessary during the layout upgrade process.
+ *
+ * Example usage:
+ *
+ * <pre>
+ * {@code
+ *
+ * @UpgradeActionRecon(feature = FEATURE_NAME, type = FINALIZE)
+ * public class FeatureNameUpgradeAction implements ReconUpgradeAction {
+ * @Override
+ * public void execute() throws Exception {
+ * // Custom upgrade logic for FEATURE_1
+ * }
+ * }
+ * }
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface UpgradeActionRecon {
+
+ /**
+ * Defines the layout feature this upgrade action is associated with.
+ */
+ ReconLayoutFeature feature();
+
+ /**
+ * Defines the type of upgrade phase during which the action should be
executed.
+ */
+ ReconUpgradeAction.UpgradeActionType type();
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/package-info.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/package-info.java
new file mode 100644
index 0000000000..56a94b1f84
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * 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 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.
+ */
+
+/**
+ * This package contains classes and interfaces for handling
+ * upgrade actions in Apache Ozone Recon.
+ *
+ * The main interface {@link
org.apache.hadoop.ozone.recon.upgrade.ReconUpgradeAction}
+ * defines the structure for actions that need to be executed during an upgrade
+ * process in Recon. The actions can be triggered automatically
+ * during startup to ensure the correct version of the schema or
+ * layout is applied.
+ */
+package org.apache.hadoop.ozone.recon.upgrade;
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java
new file mode 100644
index 0000000000..ab3c4f8e6e
--- /dev/null
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java
@@ -0,0 +1,123 @@
+/*
+ * 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.recon.persistence;
+
+import static
org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME;
+import static org.jooq.impl.DSL.name;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+import org.jooq.DSLContext;
+import org.jooq.Record1;
+import org.jooq.impl.DSL;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for SchemaVersionTableDefinition.
+ */
+public class TestSchemaVersionTableDefinition extends AbstractReconSqlDBTest {
+
+ public TestSchemaVersionTableDefinition() {
+ super();
+ }
+
+ @Test
+ public void testSchemaVersionTableCreation() throws Exception {
+ Connection connection = getConnection();
+ // Verify table definition
+ DatabaseMetaData metaData = connection.getMetaData();
+ ResultSet resultSet = metaData.getColumns(null, null,
+ SCHEMA_VERSION_TABLE_NAME, null);
+
+ List<Pair<String, Integer>> expectedPairs = new ArrayList<>();
+
+ expectedPairs.add(new ImmutablePair<>("version_number", Types.INTEGER));
+ expectedPairs.add(new ImmutablePair<>("applied_on", Types.TIMESTAMP));
+
+ List<Pair<String, Integer>> actualPairs = new ArrayList<>();
+
+ while (resultSet.next()) {
+ actualPairs.add(new ImmutablePair<>(resultSet.getString("COLUMN_NAME"),
+ resultSet.getInt("DATA_TYPE")));
+ }
+
+ assertEquals(2, actualPairs.size(), "Unexpected number of columns");
+ assertEquals(expectedPairs, actualPairs, "Column definitions do not match
expected values.");
+ }
+
+ @Test
+ public void testSchemaVersionCRUDOperations() throws SQLException {
+ Connection connection = getConnection();
+
+ DatabaseMetaData metaData = connection.getMetaData();
+ ResultSet resultSet = metaData.getTables(null, null,
+ SCHEMA_VERSION_TABLE_NAME, null);
+
+ while (resultSet.next()) {
+ assertEquals(SCHEMA_VERSION_TABLE_NAME,
+ resultSet.getString("TABLE_NAME"));
+ }
+
+ DSLContext dslContext = DSL.using(connection);
+
+ // Insert a new version record
+ dslContext.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+ .columns(DSL.field(name("version_number")),
DSL.field(name("applied_on")))
+ .values(1, new Timestamp(System.currentTimeMillis()))
+ .execute();
+
+ // Read the inserted record
+ Record1<Integer> result =
dslContext.select(DSL.field(name("version_number"), Integer.class))
+ .from(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+ .fetchOne();
+
+ assertEquals(1, result.value1(), "The version number does not match the
expected value.");
+
+ // Update the version record
+ dslContext.update(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+ .set(DSL.field(name("version_number")), 2)
+ .execute();
+
+ // Read the updated record
+ result = dslContext.select(DSL.field(name("version_number"),
Integer.class))
+ .from(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+ .fetchOne();
+
+ assertEquals(2, result.value1(), "The updated version number does not
match the expected value.");
+
+ // Delete the version record
+ dslContext.deleteFrom(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+ .execute();
+
+ // Verify deletion
+ int count = dslContext.fetchCount(DSL.table(SCHEMA_VERSION_TABLE_NAME));
+ assertEquals(0, count, "The table should be empty after deletion.");
+ }
+}
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java
new file mode 100644
index 0000000000..1da4c48a94
--- /dev/null
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java
@@ -0,0 +1,288 @@
+/*
+ * 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.recon.upgrade;
+
+import org.apache.hadoop.ozone.recon.ReconContext;
+import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager;
+import org.mockito.InOrder;
+import org.mockito.MockedStatic;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.AfterEach;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+
+
+/**
+ * Tests for ReconLayoutVersionManager.
+ */
+public class TestReconLayoutVersionManager {
+
+ private ReconSchemaVersionTableManager schemaVersionTableManager;
+ private ReconLayoutVersionManager layoutVersionManager;
+ private MockedStatic<ReconLayoutFeature> mockedEnum;
+ private MockedStatic<ReconUpgradeAction.UpgradeActionType>
mockedEnumUpgradeActionType;
+
+ @BeforeEach
+ public void setUp() throws SQLException {
+ schemaVersionTableManager = mock(ReconSchemaVersionTableManager.class);
+ when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(0);
+
+ // Mocking ReconLayoutFeature.values() to return custom enum instances
+ mockedEnum = mockStatic(ReconLayoutFeature.class);
+ mockedEnumUpgradeActionType =
mockStatic(ReconUpgradeAction.UpgradeActionType.class);
+
+ ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class);
+ when(feature1.getVersion()).thenReturn(1);
+ ReconUpgradeAction action1 = mock(ReconUpgradeAction.class);
+ when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action1));
+
+ ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class);
+ when(feature2.getVersion()).thenReturn(2);
+ ReconUpgradeAction action2 = mock(ReconUpgradeAction.class);
+ when(feature2.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action2));
+
+ // Define the custom features to be returned
+ mockedEnum.when(ReconLayoutFeature::values).thenReturn(new
ReconLayoutFeature[]{feature1, feature2});
+
+ layoutVersionManager = new
ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class));
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // Close the static mock after each test to deregister it
+ mockedEnum.close();
+ if (mockedEnumUpgradeActionType != null) {
+ mockedEnumUpgradeActionType.close();
+ }
+ }
+
+ /**
+ * Tests the initialization of layout version manager to ensure
+ * that the MLV (Metadata Layout Version) is set correctly to 0,
+ * and SLV (Software Layout Version) reflects the maximum available version.
+ */
+ @Test
+ public void testInitializationWithMockedValues() {
+ assertEquals(0, layoutVersionManager.getCurrentMLV());
+ assertEquals(2, layoutVersionManager.getCurrentSLV());
+ }
+
+ /**
+ * Tests the finalization of layout features and ensure that the
updateSchemaVersion for
+ * the schemaVersionTable is triggered for each feature version.
+ */
+ @Test
+ public void testFinalizeLayoutFeaturesWithMockedValues() throws SQLException
{
+ layoutVersionManager.finalizeLayoutFeatures();
+
+ // Verify that schema versions are updated for our custom features
+ verify(schemaVersionTableManager, times(1)).updateSchemaVersion(1);
+ verify(schemaVersionTableManager, times(1)).updateSchemaVersion(2);
+ }
+
+ /**
+ * Tests the retrieval of registered features to ensure that the correct
+ * layout features are returned according to the mocked values.
+ */
+ @Test
+ public void testGetRegisteredFeaturesWithMockedValues() {
+ // Fetch the registered features
+ List<ReconLayoutFeature> registeredFeatures =
layoutVersionManager.getRegisteredFeatures();
+
+ // Verify that the registered features match the mocked ones
+ ReconLayoutFeature feature1 = ReconLayoutFeature.values()[0];
+ ReconLayoutFeature feature2 = ReconLayoutFeature.values()[1];
+ List<ReconLayoutFeature> expectedFeatures = Arrays.asList(feature1,
feature2);
+ assertEquals(expectedFeatures, registeredFeatures);
+ }
+
+ /**
+ * Tests the scenario where no layout features are present. Ensures that no
schema
+ * version updates are attempted when there are no features to finalize.
+ */
+ @Test
+ public void testNoLayoutFeatures() throws SQLException {
+ mockedEnum.when(ReconLayoutFeature::values).thenReturn(new
ReconLayoutFeature[]{});
+ layoutVersionManager.finalizeLayoutFeatures();
+ verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt());
+ }
+
+ /**
+ * Tests the scenario where an upgrade action fails. Ensures that if an
upgrade action
+ * throws an exception, the schema version is not updated.
+ */
+ @Test
+ public void testUpgradeActionFailure() throws Exception {
+ // Reset existing mocks and set up new features for this specific test
+ mockedEnum.reset();
+
+ // Mock ReconLayoutFeature instances
+ ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class);
+ when(feature1.getVersion()).thenReturn(1);
+ ReconUpgradeAction action1 = mock(ReconUpgradeAction.class);
+
+ // Simulate an exception being thrown during the upgrade action execution
+ doThrow(new RuntimeException("Upgrade failed")).when(action1).execute();
+ when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action1));
+
+ // Mock the static values method to return the custom feature
+ mockedEnum.when(ReconLayoutFeature::values).thenReturn(new
ReconLayoutFeature[]{feature1});
+
+ // Execute the layout feature finalization
+ try {
+ layoutVersionManager.finalizeLayoutFeatures();
+ } catch (Exception e) {
+ }
+ // Verify that schema version update was never called due to the exception
+ verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt());
+ }
+
+ /**
+ * Tests the order of execution for the upgrade actions to ensure that
+ * they are executed sequentially according to their version numbers.
+ */
+ @Test
+ public void testUpgradeActionExecutionOrder() throws Exception {
+ // Reset the existing static mock for this specific test
+ mockedEnum.reset();
+
+ // Mock ReconLayoutFeature instances
+ ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class);
+ when(feature1.getVersion()).thenReturn(1);
+ ReconUpgradeAction action1 = mock(ReconUpgradeAction.class);
+ when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action1));
+
+ ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class);
+ when(feature2.getVersion()).thenReturn(2);
+ ReconUpgradeAction action2 = mock(ReconUpgradeAction.class);
+ when(feature2.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action2));
+
+ ReconLayoutFeature feature3 = mock(ReconLayoutFeature.class);
+ when(feature3.getVersion()).thenReturn(3);
+ ReconUpgradeAction action3 = mock(ReconUpgradeAction.class);
+ when(feature3.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action3));
+
+ // Mock the static values method to return custom features in a jumbled
order
+ mockedEnum.when(ReconLayoutFeature::values).thenReturn(new
ReconLayoutFeature[]{feature2, feature3, feature1});
+
+ // Execute the layout feature finalization
+ layoutVersionManager.finalizeLayoutFeatures();
+
+ // Verify that the actions were executed in the correct order using InOrder
+ InOrder inOrder = inOrder(action1, action2, action3);
+ inOrder.verify(action1).execute(); // Should be executed first
+ inOrder.verify(action2).execute(); // Should be executed second
+ inOrder.verify(action3).execute(); // Should be executed third
+ }
+
+ /**
+ * Tests the scenario where no upgrade actions are needed. Ensures that if
the current
+ * schema version matches the maximum layout version, no upgrade actions are
executed.
+ */
+ @Test
+ public void testNoUpgradeActionsNeeded() throws SQLException {
+ when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(2);
+ layoutVersionManager = new
ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class));
+ layoutVersionManager.finalizeLayoutFeatures();
+
+ verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt());
+ }
+
+ /**
+ * Tests the scenario where the first two features are finalized,
+ * and then a third feature is introduced. Ensures that only the
+ * newly introduced feature is finalized while the previously
+ * finalized features are skipped.
+ */
+ @Test
+ public void testFinalizingNewFeatureWithoutReFinalizingPreviousFeatures()
throws Exception {
+ // Step 1: Finalize the first two features.
+ when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(0);
+
+ // Mock the first two features.
+ ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class);
+ when(feature1.getVersion()).thenReturn(1);
+ ReconUpgradeAction action1 = mock(ReconUpgradeAction.class);
+ when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action1));
+
+ ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class);
+ when(feature2.getVersion()).thenReturn(2);
+ ReconUpgradeAction action2 = mock(ReconUpgradeAction.class);
+ when(feature2.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action2));
+
+ mockedEnum.when(ReconLayoutFeature::values).thenReturn(new
ReconLayoutFeature[]{feature1, feature2});
+
+ // Finalize the first two features.
+ layoutVersionManager.finalizeLayoutFeatures();
+
+ // Verify that the schema versions for the first two features were updated.
+ verify(schemaVersionTableManager, times(1)).updateSchemaVersion(1);
+ verify(schemaVersionTableManager, times(1)).updateSchemaVersion(2);
+
+ // Step 2: Introduce a new feature (Feature 3).
+ ReconLayoutFeature feature3 = mock(ReconLayoutFeature.class);
+ when(feature3.getVersion()).thenReturn(3);
+ ReconUpgradeAction action3 = mock(ReconUpgradeAction.class);
+ when(feature3.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE))
+ .thenReturn(Optional.of(action3));
+
+ mockedEnum.when(ReconLayoutFeature::values).thenReturn(new
ReconLayoutFeature[]{feature1, feature2, feature3});
+
+ // Update schema version to simulate that features 1 and 2 have already
been finalized.
+ when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(2);
+
+ // Finalize again, but only feature 3 should be finalized.
+ layoutVersionManager.finalizeLayoutFeatures();
+
+ // Verify that the schema version for feature 3 was updated.
+ verify(schemaVersionTableManager, times(1)).updateSchemaVersion(3);
+
+ // Verify that action1 and action2 were not executed again.
+ verify(action1, times(1)).execute(); // Still should have been executed
only once
+ verify(action2, times(1)).execute(); // Still should have been executed
only once
+
+ // Verify that the upgrade action for feature 3 was executed.
+ verify(action3, times(1)).execute();
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]