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]


Reply via email to