This is an automated email from the ASF dual-hosted git repository.

errose28 pushed a commit to branch HDDS-14496-zdu
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-14496-zdu by this push:
     new 5361f6e0d1e HDDS-15374. Switch Recon to the new versioning framework 
(#10443)
5361f6e0d1e is described below

commit 5361f6e0d1e2310333604596d596bdeee4d03de8
Author: Ethan Rose <[email protected]>
AuthorDate: Mon Jun 15 11:11:49 2026 -0400

    HDDS-15374. Switch Recon to the new versioning framework (#10443)
    
    Co-authored-by: Cursor <[email protected]>
---
 .../java/org/apache/hadoop/hdds/HDDSVersion.java   |   2 +-
 .../docs/content/design/upgrade-dev-primer.md      |   4 +-
 .../ozone/upgrade/ComponentVersionManager.java     |   1 -
 .../hdds/scm/server/upgrade/ScmVersionManager.java |   2 +-
 .../recon/schema/SchemaVersionTableDefinition.java |  26 +-
 hadoop-ozone/recon/pom.xml                         |   6 +
 .../hadoop/ozone/recon/ReconSchemaManager.java     |  16 +-
 .../recon/ReconSchemaVersionTableManager.java      | 102 ------
 .../org/apache/hadoop/ozone/recon/ReconServer.java |  50 +--
 .../scm/ReconStorageContainerManagerFacade.java    |  20 +-
 .../upgrade/InitialConstraintUpgradeAction.java    |   5 +-
 .../upgrade/NSSummaryAggregatedTotalsUpgrade.java  |   2 +-
 .../ozone/recon/upgrade/ReconLayoutFeature.java    | 119 -------
 .../recon/upgrade/ReconLayoutVersionManager.java   | 164 ----------
 .../upgrade/ReconTaskStatusTableUpgradeAction.java |   2 +-
 .../ozone/recon/upgrade/ReconUpgradeAction.java    |  10 +-
 ...Action.java => ReconUpgradeActionProvider.java} |  22 +-
 .../hadoop/ozone/recon/upgrade/ReconVersion.java   |  83 ++---
 .../ozone/recon/upgrade/ReconVersionManager.java   | 151 +++++++++
 .../ReplicatedSizeOfFilesUpgradeAction.java        |   2 +-
 .../UnhealthyContainerReplicaMismatchAction.java   |   5 +-
 ...ntainersStateContainerIdIndexUpgradeAction.java |   7 +-
 .../ozone/recon/upgrade/UpgradeActionRecon.java    |   4 +-
 .../TestSchemaVersionTableDefinition.java          |  75 ++---
 .../upgrade/TestReconLayoutVersionManager.java     | 356 ---------------------
 .../ozone/recon/upgrade/TestReconVersion.java      |  59 ++++
 .../recon/upgrade/TestReconVersionManager.java     | 237 ++++++++++++++
 27 files changed, 613 insertions(+), 919 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java
index 2cd2720d720..b9de634c48e 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java
@@ -25,7 +25,7 @@
 import java.util.TreeMap;
 
 /**
- * Versioning for datanode.
+ * Upgrade and downgrade version handling for SCM and Datanode.
  */
 public enum HDDSVersion implements ComponentVersion {
 
diff --git a/hadoop-hdds/docs/content/design/upgrade-dev-primer.md 
b/hadoop-hdds/docs/content/design/upgrade-dev-primer.md
index 4daf13a097f..b19199bbbc2 100644
--- a/hadoop-hdds/docs/content/design/upgrade-dev-primer.md
+++ b/hadoop-hdds/docs/content/design/upgrade-dev-primer.md
@@ -44,13 +44,15 @@ Class to add  a new layout feature being brought in. Layout 
version is typically
 
 **DataNode** uses 
[`org.apache.hadoop.ozone.container.upgrade.DatanodeVersionManager`](https://github.com/apache/ozone/blob/master/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/upgrade/DatanodeVersionManager.java)
 with upgrade actions via 
[`org.apache.hadoop.ozone.container.upgrade.DatanodeUpgradeActionProvider`](https://github.com/apache/ozone/blob/master/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/upgrade/DatanodeUpgrade
 [...]
 
+**Recon** uses 
[`org.apache.hadoop.ozone.recon.upgrade.ReconVersionManager`](https://github.com/apache/ozone/blob/master/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java)
 for Derby SQL schema versioning, with versions defined in 
[`ReconVersion`](https://github.com/apache/ozone/blob/master/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java)
 and upgrade actions via [`ReconUpgradeActionProvider`](https://gi [...]
+
 ## @DisallowedUntilLayoutVersion Annotation
 Method level annotation used to "disallow" an API if current layout version 
does not include the associated layout feature. Currently it is added only to 
the OM module, but can easily be moved down to a common module based on need on 
the HDDS layer.
 
 ## @BelongsToLayoutVersion Annotation
 Annotation to mark an OM request class that it belongs to a specific Layout 
Version. Until that version is available post finalize, this request will not 
be supported. A newer version of an existing OM request can be created (by 
inheritance or a fully new class) and marked with a newer layout version. Until 
finalizing this layout version, the older request class is used. Post 
finalizing, the newer version of the request class is used.
 
-## Upgrade Action (UpgradeActionOm, UpgradeActionScm & UpgradeActionDatanode)
+## Upgrade Action (UpgradeActionOm, UpgradeActionScm, UpgradeActionDatanode & 
UpgradeActionRecon)
 Annotation to specify upgrade action run during finalization. Each layout 
feature can optionally define a single upgrade action that will be executed 
when the feature is finalized. This action should be idempotent and execute 
quickly. The action must complete for the feature to finish
 finalizing, so if there is an error executing the action it will be retried. 
This partial failure should not leave the component inoperable.
 
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
index 5ab47c4e7f1..516158f76c6 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
@@ -99,7 +99,6 @@ public void finalizeUpgrade() throws UpgradeException {
 
       LOG.info("Version {} has been finalized.", newVersion);
     }
-    LOG.info("Finalization is complete.");
   }
 
   /**
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
index 18e5dbe3179..c5a44fb249d 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
@@ -44,7 +44,7 @@ public ScmVersionManager(SCMStorageConfig storage, 
OzoneStorageContainerManager
     this(storage, upgradeActionArg, new ScmUpgradeActionProvider());
   }
 
-  @VisibleForTesting
+  // Used by Recon's node manager to track Datanode versions without running 
SCM specific upgrade actions.
   public ScmVersionManager(SCMStorageConfig storage,
       OzoneStorageContainerManager upgradeActionArg,
       ComponentUpgradeActionProvider<ScmUpgradeAction> upgradeActionProvider)
diff --git 
a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SchemaVersionTableDefinition.java
 
b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SchemaVersionTableDefinition.java
index 1ee831963ad..59de2845e48 100644
--- 
a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SchemaVersionTableDefinition.java
+++ 
b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SchemaVersionTableDefinition.java
@@ -42,7 +42,7 @@ public class SchemaVersionTableDefinition implements 
ReconSchemaDefinition {
 
   public static final String SCHEMA_VERSION_TABLE_NAME = 
"RECON_SCHEMA_VERSION";
   private final DataSource dataSource;
-  private int latestSLV;
+  private int softwareVersion;
 
   @Inject
   public SchemaVersionTableDefinition(DataSource dataSource) {
@@ -61,8 +61,7 @@ public void initializeSchema() throws SQLException {
         createSchemaVersionTable(localDslContext);
 
         if (isFreshInstall) {
-          // Fresh install: Set the SLV to the latest version
-          insertInitialSLV(localDslContext, latestSLV);
+          insertApparentVersion(localDslContext, softwareVersion);
         }
       }
     }
@@ -80,27 +79,16 @@ private void createSchemaVersionTable(DSLContext 
dslContext) {
         .execute();
   }
 
-  /**
-   * Inserts the initial SLV into the Schema Version table.
-   *
-   * @param dslContext The DSLContext to use for the operation.
-   * @param slv        The initial SLV value.
-   */
-  private void insertInitialSLV(DSLContext dslContext, int slv) {
+  private void insertApparentVersion(DSLContext dslContext, int 
apparentVersion) {
     dslContext.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME))
         .columns(DSL.field(name("version_number")),
             DSL.field(name("applied_on")))
-        .values(slv, DSL.currentTimestamp())
+        .values(apparentVersion, DSL.currentTimestamp())
         .execute();
-    LOG.info("Inserted initial SLV '{}' into SchemaVersion table.", slv);
+    LOG.info("Inserted initial apparent version '{}' into SchemaVersion 
table.", apparentVersion);
   }
 
-  /**
-   * Set the latest SLV.
-   *
-   * @param slv The latest Software Layout Version.
-   */
-  public void setLatestSLV(int slv) {
-    this.latestSLV = slv;
+  public void setSoftwareVersion(int version) {
+    this.softwareVersion = version;
   }
 }
diff --git a/hadoop-ozone/recon/pom.xml b/hadoop-ozone/recon/pom.xml
index 9dacdf1f8ca..bf34549a756 100644
--- a/hadoop-ozone/recon/pom.xml
+++ b/hadoop-ozone/recon/pom.xml
@@ -296,6 +296,12 @@
       <type>test-jar</type>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.ozone</groupId>
+      <artifactId>hdds-server-framework</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.ozone</groupId>
       <artifactId>hdds-server-scm</artifactId>
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java
index 724b15c96a9..dbfa66551cf 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java
@@ -22,7 +22,7 @@
 import java.sql.SQLException;
 import java.util.HashSet;
 import java.util.Set;
-import org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature;
+import org.apache.hadoop.ozone.recon.upgrade.ReconVersion;
 import org.apache.ozone.recon.schema.ReconSchemaDefinition;
 import org.apache.ozone.recon.schema.SchemaVersionTableDefinition;
 import org.slf4j.Logger;
@@ -44,9 +44,6 @@ public ReconSchemaManager(Set<ReconSchemaDefinition> 
reconSchemaDefinitions) {
 
   @VisibleForTesting
   public void createReconSchema() {
-    // Calculate the latest SLV from ReconLayoutFeature
-    int latestSLV = calculateLatestSLV();
-
     try {
       // Initialize the schema version table first
       reconSchemaDefinitions.stream()
@@ -54,7 +51,7 @@ public void createReconSchema() {
           .findFirst()
           .ifPresent(schemaDefinition -> {
             SchemaVersionTableDefinition schemaVersionTable = 
(SchemaVersionTableDefinition) schemaDefinition;
-            schemaVersionTable.setLatestSLV(latestSLV);
+            
schemaVersionTable.setSoftwareVersion(ReconVersion.SOFTWARE_VERSION.serialize());
             try {
               schemaVersionTable.initializeSchema();
             } catch (SQLException e) {
@@ -77,13 +74,4 @@ public void createReconSchema() {
       LOG.error("Error creating Recon schema.", e);
     }
   }
-
-  /**
-   * Calculate the latest SLV by iterating over ReconLayoutFeature.
-   *
-   * @return The latest SLV.
-   */
-  private int calculateLatestSLV() {
-    return ReconLayoutFeature.determineSLV();
-  }
 }
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
deleted file mode 100644
index 06d55b0a1c8..00000000000
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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 static org.jooq.impl.DSL.name;
-
-import com.google.inject.Inject;
-import java.sql.Connection;
-import java.sql.SQLException;
-import javax.sql.DataSource;
-import org.jooq.DSLContext;
-import org.jooq.impl.DSL;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * 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 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, Connection conn) {
-    dslContext = DSL.using(conn);
-    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);
-    }
-  }
-
-  /**
-   * 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 78a4938166a..1c2e0325f4c 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
@@ -34,7 +34,6 @@
 import java.util.Collection;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicBoolean;
-import javax.sql.DataSource;
 import org.apache.hadoop.hdds.cli.GenericCli;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import 
org.apache.hadoop.hdds.protocolPB.SCMSecurityProtocolClientSideTranslatorPB;
@@ -50,6 +49,7 @@
 import org.apache.hadoop.ozone.recon.metrics.ReconTaskStatusMetrics;
 import org.apache.hadoop.ozone.recon.scm.ReconSafeModeManager;
 import org.apache.hadoop.ozone.recon.scm.ReconStorageConfig;
+import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
 import org.apache.hadoop.ozone.recon.security.ReconCertificateClient;
 import org.apache.hadoop.ozone.recon.spi.OzoneManagerServiceProvider;
 import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
@@ -57,7 +57,8 @@
 import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
 import org.apache.hadoop.ozone.recon.spi.impl.ReconDBProvider;
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
-import org.apache.hadoop.ozone.recon.upgrade.ReconLayoutVersionManager;
+import org.apache.hadoop.ozone.recon.upgrade.ReconVersionManager;
+import org.apache.hadoop.ozone.upgrade.UpgradeException;
 import org.apache.hadoop.ozone.util.OzoneNetUtils;
 import org.apache.hadoop.ozone.util.OzoneVersionInfo;
 import org.apache.hadoop.ozone.util.ShutdownHookManager;
@@ -83,7 +84,7 @@ public class ReconServer extends GenericCli implements 
Callable<Void> {
   private OzoneManagerServiceProvider ozoneManagerServiceProvider;
   private ReconDBProvider reconDBProvider;
   private ReconNamespaceSummaryManager reconNamespaceSummaryManager;
-  private OzoneStorageContainerManager reconStorageContainerManager;
+  private ReconStorageContainerManagerFacade reconStorageContainerManager;
   private OzoneConfiguration configuration;
   private ReconStorageConfig reconStorage;
   private CertificateClient certClient;
@@ -163,8 +164,7 @@ public Void call() throws Exception {
       httpServer = injector.getInstance(ReconHttpServer.class);
       this.ozoneManagerServiceProvider =
           injector.getInstance(OzoneManagerServiceProvider.class);
-      this.reconStorageContainerManager =
-          injector.getInstance(OzoneStorageContainerManager.class);
+      this.reconStorageContainerManager = 
injector.getInstance(ReconStorageContainerManagerFacade.class);
 
       this.reconTaskStatusMetrics =
           injector.getInstance(ReconTaskStatusMetrics.class);
@@ -172,24 +172,13 @@ public Void call() throws Exception {
       LOG.info("Initializing support of Recon Features...");
       FeatureProvider.initFeatureSupport(configuration);
 
+      finalizeUpgrade();
+
       LOG.debug("Now starting all services of Recon...");
       // Start all services
       start();
       isStarted = true;
 
-      LOG.info("Finalizing Layout Features.");
-      // Handle Recon Schema Versioning
-      ReconSchemaVersionTableManager versionTableManager =
-          injector.getInstance(ReconSchemaVersionTableManager.class);
-      DataSource dataSource = injector.getInstance(DataSource.class);
-
-      ReconLayoutVersionManager layoutVersionManager =
-          new ReconLayoutVersionManager(versionTableManager, reconContext, 
dataSource);
-      // Run the upgrade framework to finalize layout features if needed
-      layoutVersionManager.finalizeLayoutFeatures();
-
-      LOG.info("Recon schema versioning completed.");
-
       // Register ReconTaskStatusMetrics after schema upgrade completes
       // This ensures the RECON_TASK_STATUS table has all required columns
       if (reconTaskStatusMetrics != null) {
@@ -214,6 +203,31 @@ public Void call() throws Exception {
     return null;
   }
 
+  private void finalizeUpgrade() {
+    LOG.info("Finalizing Recon versions.");
+    ReconContext reconContext = injector.getInstance(ReconContext.class);
+    ReconVersionManager reconVersionManager = 
injector.getInstance(ReconVersionManager.class);
+    try {
+      reconVersionManager.finalizeUpgrade();
+    } catch (UpgradeException e) {
+      LOG.error("Failed to finalize Recon versions.", e);
+      reconContext.updateErrors(ReconContext.ErrorCode.UPGRADE_FAILURE);
+      reconContext.updateHealthStatus(new AtomicBoolean(false));
+      throw new RuntimeException("Recon failed to finalize schema versions. 
Startup halted.", e);
+    }
+
+    try {
+      reconStorageContainerManager.finalizeScmVersionUpgrade();
+    } catch (UpgradeException e) {
+      LOG.error("Failed to finalize Recon SCM apparent version.", e);
+      reconContext.updateErrors(ReconContext.ErrorCode.UPGRADE_FAILURE);
+      reconContext.updateHealthStatus(new AtomicBoolean(false));
+      throw new RuntimeException("Recon failed to finalize SCM version. 
Startup halted.", e);
+    }
+
+    LOG.info("Recon upgrade finalization completed.");
+  }
+
   private void updateAndLogReconHealthStatus() {
     ReconContext reconContext = injector.getInstance(ReconContext.class);
     assert reconContext != null;
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
index 466bc50878a..686c64cc1c4 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconStorageContainerManagerFacade.java
@@ -45,6 +45,7 @@
 import java.net.InetSocketAddress;
 import java.time.Clock;
 import java.time.ZoneId;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -97,7 +98,6 @@
 import 
org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher.ContainerReport;
 import 
org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher.ContainerReportFromDatanode;
 import 
org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher.IncrementalContainerReportFromDatanode;
-import org.apache.hadoop.hdds.scm.server.SCMStorageConfig;
 import org.apache.hadoop.hdds.scm.server.upgrade.ScmVersionManager;
 import org.apache.hadoop.hdds.server.events.EventQueue;
 import 
org.apache.hadoop.hdds.server.events.FixedThreadPoolWithAffinityExecutor;
@@ -120,6 +120,7 @@
 import org.apache.hadoop.ozone.recon.tasks.ContainerSizeCountTask;
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig;
 import 
org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdaterManager;
+import org.apache.hadoop.ozone.upgrade.UpgradeException;
 import org.apache.ozone.recon.schema.UtilizationSchemaDefinition;
 import 
org.apache.ozone.recon.schema.generated.tables.daos.ContainerCountBySizeDao;
 import org.apache.ratis.util.ExitUtils;
@@ -145,7 +146,8 @@ public class ReconStorageContainerManagerFacade
   // This will hold the recon related information like health status and 
errors in initialization of modules if any,
   // which can later be used for alerts integration or displaying some 
meaningful info to user on Recon UI.
   private final ReconContext reconContext;
-  private final SCMStorageConfig scmStorageConfig;
+  private final ReconStorageConfig scmStorageConfig;
+  private final ScmVersionManager scmVersionManager;
   private final SCMNodeDetails reconNodeDetails;
   private final SCMHAManager scmhaManager;
   private final SequenceIdGenerator sequenceIdGen;
@@ -180,6 +182,7 @@ public 
ReconStorageContainerManagerFacade(OzoneConfiguration conf,
                                             ReconUtils reconUtils,
                                             ReconSafeModeManager 
safeModeManager,
                                             ReconContext reconContext,
+                                            ReconStorageConfig 
reconStorageConfig,
                                             DataSource dataSource,
                                             ReconTaskStatusUpdaterManager 
taskStatusUpdaterManager,
                                             ContainerHealthSchemaManager 
containerHealthSchemaManager)
@@ -211,12 +214,13 @@ public 
ReconStorageContainerManagerFacade(OzoneConfiguration conf,
     conf.setLong(HDDS_SCM_CLIENT_FAILOVER_MAX_RETRY,
         scmClientFailOverMaxRetryCount);
 
-    this.scmStorageConfig = new ReconStorageConfig(conf, reconUtils);
+    this.scmStorageConfig = reconStorageConfig;
     NetworkTopology clusterMap = new NetworkTopologyImpl(conf);
     this.dbStore = DBStoreBuilder.createDBStore(ozoneConfiguration, 
ReconSCMDBDefinition.get());
 
-    // TODO HDDS-15374 Fully switch recon to the new versioning framework.
-    ScmVersionManager versionManager = new ScmVersionManager(scmStorageConfig, 
this);
+    // Use a version manager with no upgrade actions. The version will only be 
used to track Datanode versions,
+    // not run SCM specific reformatting on upgrade.
+    this.scmVersionManager = new ScmVersionManager(scmStorageConfig, this, 
HashMap::new);
     this.scmhaManager = SCMHAManagerStub.getInstance(
         true, new SCMDBTransactionBufferImpl());
     this.sequenceIdGen = new SequenceIdGenerator(
@@ -225,7 +229,7 @@ public 
ReconStorageContainerManagerFacade(OzoneConfiguration conf,
     this.nodeManager =
         new ReconNodeManager(conf, scmStorageConfig, eventQueue, clusterMap,
             ReconSCMDBDefinition.NODES.getTable(dbStore),
-            versionManager, reconContext);
+            scmVersionManager, reconContext);
     SCMContainerPlacementMetrics placementMetrics = 
SCMContainerPlacementMetrics.create();
     PlacementPolicy containerPlacementPolicy = 
ContainerPlacementPolicyFactory.getPolicy(conf, nodeManager,
         clusterMap, true, placementMetrics);
@@ -717,4 +721,8 @@ public ReconContext getReconContext() {
   public DataSource getDataSource() {
     return dataSource;
   }
+
+  public void finalizeScmVersionUpgrade() throws UpgradeException {
+    scmVersionManager.finalizeUpgrade();
+  }
 }
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java
index ea8af99d96e..6f632de180b 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java
@@ -17,11 +17,10 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
-import static 
org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature.INITIAL_VERSION;
+import static 
org.apache.hadoop.ozone.recon.upgrade.ReconVersion.INITIAL_VERSION;
 import static 
org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME;
 import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK;
 import static org.jooq.impl.DSL.field;
-import static org.jooq.impl.DSL.name;
 
 import com.google.common.annotations.VisibleForTesting;
 import java.sql.Connection;
@@ -82,7 +81,7 @@ private void addUpdatedConstraint() {
 
     
dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME)
         
.add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + 
"ck1")
-        .check(field(name("container_state"))
+        .check(field(DSL.name("container_state"))
         .in(enumStates)))
         .execute();
 
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/NSSummaryAggregatedTotalsUpgrade.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/NSSummaryAggregatedTotalsUpgrade.java
index d4ee78c1a6a..f153c4a35ac 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/NSSummaryAggregatedTotalsUpgrade.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/NSSummaryAggregatedTotalsUpgrade.java
@@ -33,7 +33,7 @@
  * Recon startup is not blocked. During rebuild, APIs that depend on
  * the tree may return initializing responses as designed.
  */
-@UpgradeActionRecon(feature = ReconLayoutFeature.NSSUMMARY_AGGREGATED_TOTALS)
+@UpgradeActionRecon(feature = ReconVersion.NSSUMMARY_AGGREGATED_TOTALS)
 public class NSSummaryAggregatedTotalsUpgrade implements ReconUpgradeAction {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(NSSummaryAggregatedTotalsUpgrade.class);
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
deleted file mode 100644
index a1e8abf8d0c..00000000000
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.util.Arrays;
-import java.util.Optional;
-import java.util.Set;
-import org.reflections.Reflections;
-
-/**
- * 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"),
-  TASK_STATUS_STATISTICS(1, "Recon Task Status Statistics Tracking 
Introduced"),
-  UNHEALTHY_CONTAINER_REPLICA_MISMATCH(2, "Adding replica mismatch state to 
the unhealthy container table"),
-
-  // HDDS-13432: Materialize NSSummary totals and rebuild tree on upgrade
-  NSSUMMARY_AGGREGATED_TOTALS(3, "Aggregated totals for NSSummary and 
auto-rebuild on upgrade"),
-  REPLICATED_SIZE_OF_FILES(4, "Adds replicatedSizeOfFiles to NSSummary"),
-  UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX(5,
-      "Adds idx_state_container_id index on UNHEALTHY_CONTAINERS for 
upgrades");
-
-  private final int version;
-  private final String description;
-  private ReconUpgradeAction action;
-
-  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 this feature.
-   *
-   * @return An {@link Optional} containing the upgrade action if present.
-   */
-  public Optional<ReconUpgradeAction> getAction() {
-    return Optional.ofNullable(action);
-  }
-
-  /**
-   * Associates a given upgrade action with this feature. Only the first 
upgrade action registered will be used.
-   *
-   * @param upgradeAction The upgrade action to associate with this feature.
-   */
-  public void addAction(ReconUpgradeAction upgradeAction) {
-    // Required by SpotBugs since this setter exists in an enum.
-    if (this.action == null) {
-      this.action = upgradeAction;
-    }
-  }
-
-  /**
-   * Scans the classpath for all classes annotated with {@link 
UpgradeActionRecon}
-   * and registers their upgrade actions for the corresponding feature.
-   * 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(action);
-      } catch (Exception e) {
-        throw new RuntimeException("Failed to register upgrade action: " + 
actionClass.getSimpleName(), e);
-      }
-    }
-  }
-
-  /**
-   * Determines the Software Layout Version (SLV) based on the latest feature 
version.
-   * @return The Software Layout Version (SLV).
-   */
-  public static int determineSLV() {
-    return Arrays.stream(ReconLayoutFeature.values())
-        .mapToInt(ReconLayoutFeature::getVersion)
-        .max()
-        .orElse(0); // Default to 0 if no features are defined
-  }
-
-  /**
-   * 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
deleted file mode 100644
index cb12546771e..00000000000
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * 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.sql.Connection;
-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;
-import javax.sql.DataSource;
-import org.apache.hadoop.ozone.recon.ReconContext;
-import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * 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;
-  private final DataSource dataSource;
-
-  // Metadata Layout Version (MLV) of the Recon Metadata on disk
-  private int currentMLV;
-
-  public ReconLayoutVersionManager(ReconSchemaVersionTableManager 
schemaVersionTableManager,
-                                   ReconContext reconContext, DataSource 
dataSource)
-      throws SQLException {
-    this.schemaVersionTableManager = schemaVersionTableManager;
-    this.currentMLV = determineMLV();
-    this.reconContext = reconContext;
-    this.dataSource = dataSource;
-    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);
-  }
-
-  /**
-   * 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();
-    LOG.debug("Starting finalization of {} features.", 
featuresToFinalize.size());
-
-    try (Connection connection = dataSource.getConnection()) {
-      connection.setAutoCommit(false); // Turn off auto-commit for 
transactional control
-
-      for (ReconLayoutFeature feature : featuresToFinalize) {
-        LOG.debug("Processing feature version: {}", feature.getVersion());
-        try {
-          // Fetch the action for the feature
-          Optional<ReconUpgradeAction> action = feature.getAction();
-          if (action.isPresent()) {
-            LOG.debug("Finalize action found for feature version: {}", 
feature.getVersion());
-            // Update the schema version in the database
-            updateSchemaVersion(feature.getVersion(), connection);
-
-            // Execute the upgrade action
-            action.get().execute(dataSource);
-
-            // Commit the transaction only if both operations succeed
-            connection.commit();
-            LOG.info("Feature versioned {} finalized successfully.", 
feature.getVersion());
-          } else {
-            LOG.info("No finalize action found for feature version: {}", 
feature.getVersion());
-          }
-        } catch (Exception e) {
-          // Rollback any pending changes for the current feature due to 
failure
-          connection.rollback();
-          currentMLV = determineMLV(); // Rollback the MLV to the original 
value
-          LOG.error("Failed to finalize feature {}. Rolling back changes.", 
feature.getVersion(), e);
-          throw e;
-        }
-      }
-    } catch (Exception e) {
-      // Log the error to both logs and ReconContext
-      LOG.error("Failed to finalize layout features: {}", e.getMessage());
-      reconContext.updateErrors(ReconContext.ErrorCode.UPGRADE_FAILURE);
-      reconContext.updateHealthStatus(new AtomicBoolean(false));
-      throw new RuntimeException("Recon failed to finalize layout features. 
Startup halted.", e);
-    }
-  }
-
-  /**
-   * 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.
-   * This method uses the provided connection to ensure transactional 
consistency.
-   *
-   * @param newVersion The new Metadata Layout Version (MLV) to set.
-   * @param connection The database connection to use for the update operation.
-   */
-  private void updateSchemaVersion(int newVersion, Connection connection) {
-    schemaVersionTableManager.updateSchemaVersion(newVersion, connection);
-    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/ReconTaskStatusTableUpgradeAction.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java
index 17d64abb9c5..49e86b0d55e 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java
@@ -36,7 +36,7 @@
  * <code>last_task_run_status</code> and <code>current_task_run_status</code> 
columns to
  * {@link ReconTaskSchemaDefinition} in case it is missing .
  */
-@UpgradeActionRecon(feature = ReconLayoutFeature.TASK_STATUS_STATISTICS)
+@UpgradeActionRecon(feature = ReconVersion.TASK_STATUS_STATISTICS)
 public class ReconTaskStatusTableUpgradeAction implements ReconUpgradeAction {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(ReconTaskStatusTableUpgradeAction.class);
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
index 67aa0e9d569..3994223447f 100644
--- 
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
@@ -18,14 +18,10 @@
 package org.apache.hadoop.ozone.recon.upgrade;
 
 import javax.sql.DataSource;
+import org.apache.hadoop.ozone.upgrade.UpgradeAction;
 
 /**
- * ReconUpgradeAction is an interface for executing upgrade actions in Recon.
+ * Recon upgrade action executed during finalization of a {@link ReconVersion}.
  */
-public interface ReconUpgradeAction {
-
-  /**
-   * Execute the upgrade action during finalization.
-   */
-  void execute(DataSource source) throws Exception;
+public interface ReconUpgradeAction extends UpgradeAction<DataSource> {
 }
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/ReconUpgradeActionProvider.java
similarity index 53%
copy from 
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java
copy to 
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeActionProvider.java
index 67aa0e9d569..76fb392d3bb 100644
--- 
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/ReconUpgradeActionProvider.java
@@ -17,15 +17,23 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
-import javax.sql.DataSource;
+import org.apache.hadoop.hdds.ComponentVersion;
+import org.apache.hadoop.ozone.upgrade.AbstractUpgradeActionProvider;
 
 /**
- * ReconUpgradeAction is an interface for executing upgrade actions in Recon.
+ * Loads {@link ReconUpgradeAction} implementations annotated with {@link 
UpgradeActionRecon}.
  */
-public interface ReconUpgradeAction {
+public final class ReconUpgradeActionProvider extends 
AbstractUpgradeActionProvider<ReconUpgradeAction> {
 
-  /**
-   * Execute the upgrade action during finalization.
-   */
-  void execute(DataSource source) throws Exception;
+  public static final String RECON_UPGRADE_CLASS_PACKAGE = 
"org.apache.hadoop.ozone.recon.upgrade";
+
+  public ReconUpgradeActionProvider() {
+    super(UpgradeActionRecon.class, ReconUpgradeAction.class, 
RECON_UPGRADE_CLASS_PACKAGE);
+  }
+
+  @Override
+  protected ComponentVersion extractVersion(Class<?> clazz) {
+    UpgradeActionRecon annotation = 
clazz.getAnnotation(UpgradeActionRecon.class);
+    return annotation.feature();
+  }
 }
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
similarity index 51%
copy from 
hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java
copy to 
hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
index 2cd2720d720..0e79f2a2a86 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HDDSVersion.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.hdds;
+package org.apache.hadoop.ozone.recon.upgrade;
 
 import static java.util.function.Function.identity;
 import static java.util.stream.Collectors.toMap;
@@ -23,43 +23,58 @@
 import java.util.Arrays;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import org.apache.hadoop.hdds.ComponentVersion;
 
 /**
- * Versioning for datanode.
+ * Upgrade version handling for Recon. Currently Recon is finalized on startup 
and does not support downgrade,
+ * so versioning is only used to execute reformatting actions on upgrade.
+ *
+ * <p>{@link #INITIAL_VERSION} is the starting state when no version is 
recorded; it has no upgrade
+ * action. Finalization advances one version at a time via {@link 
ComponentVersion#nextVersion()}.
  */
-public enum HDDSVersion implements ComponentVersion {
-
-  //////////////////////////////  //////////////////////////////
-
-  DEFAULT_VERSION(0, "Initial version"),
-
-  SEPARATE_RATIS_PORTS_AVAILABLE(1, "Version with separated Ratis port."),
-  COMBINED_PUTBLOCK_WRITECHUNK_RPC(2, "WriteChunk can optionally support " +
-          "a PutBlock request"),
-  STREAM_BLOCK_SUPPORT(3,
-      "This version has support for reading a block by streaming chunks."),
-
-  ZDU(100, "Version that supports zero downtime upgrade"),
-
-  UNKNOWN_VERSION(-1, "Used when a version cannot be deserialized to any 
version recognized by this" +
-      " component, which may indicate it came from a component in a newer 
version");
-
-  //////////////////////////////  //////////////////////////////
-
-  private static final SortedMap<Integer, HDDSVersion> BY_VALUE =
+public enum ReconVersion implements ComponentVersion {
+  /** Starting point for Recon versioning; not a transition target for upgrade 
actions. */
+  INITIAL_VERSION(0, "Recon Layout Versioning Introduction"),
+  TASK_STATUS_STATISTICS(1, "Recon Task Status Statistics Tracking 
Introduced"),
+  UNHEALTHY_CONTAINER_REPLICA_MISMATCH(2, "Adding replica mismatch state to 
the unhealthy container table"),
+
+  // HDDS-13432: Materialize NSSummary totals and rebuild tree on upgrade
+  NSSUMMARY_AGGREGATED_TOTALS(3, "Aggregated totals for NSSummary and 
auto-rebuild on upgrade"),
+  REPLICATED_SIZE_OF_FILES(4, "Adds replicatedSizeOfFiles to NSSummary"),
+  UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX(5,
+      "Adds idx_state_container_id index on UNHEALTHY_CONTAINERS for 
upgrades"),
+
+  UNKNOWN_VERSION(-1, "Used when a version cannot be deserialized to any 
version recognized by this"
+      + " component, which may indicate it came from a component in a newer 
version");
+
+  private static final SortedMap<Integer, ReconVersion> BY_VALUE =
       Arrays.stream(values())
-          .collect(toMap(HDDSVersion::serialize, identity(), (v1, v2) -> v1, 
TreeMap::new));
+          .collect(toMap(ReconVersion::serialize, identity(), (v1, v2) -> v1, 
TreeMap::new));
 
-  public static final HDDSVersion SOFTWARE_VERSION = 
BY_VALUE.get(BY_VALUE.lastKey());
+  public static final ReconVersion SOFTWARE_VERSION = 
BY_VALUE.get(BY_VALUE.lastKey());
 
   private final int version;
   private final String description;
 
-  HDDSVersion(int version, String description) {
+  ReconVersion(final int version, String description) {
     this.version = version;
     this.description = description;
   }
 
+  @Override
+  public int serialize() {
+    return version;
+  }
+
+  /**
+   * @param value The serialized version to convert.
+   * @return The version corresponding to this serialized value, or {@link 
#UNKNOWN_VERSION} if no matching version is
+   *     found.
+   */
+  public static ReconVersion deserialize(int value) {
+    return BY_VALUE.getOrDefault(value, UNKNOWN_VERSION);
+  }
+
   @Override
   public String description() {
     return description;
@@ -67,10 +82,10 @@ public String description() {
 
   /**
    * @return The next version immediately following this one and excluding 
{@link #UNKNOWN_VERSION},
-   *    or null if there is no such version.
+   *     or null if there is no such version.
    */
   @Override
-  public HDDSVersion nextVersion() {
+  public ReconVersion nextVersion() {
     int nextOrdinal = ordinal() + 1;
     if (nextOrdinal >= values().length - 1) {
       return null;
@@ -78,20 +93,6 @@ public HDDSVersion nextVersion() {
     return values()[nextOrdinal];
   }
 
-  @Override
-  public int serialize() {
-    return version;
-  }
-
-  /**
-   * @param value The serialized version to convert.
-   * @return The version corresponding to this serialized value, or {@link 
#UNKNOWN_VERSION} if no matching version is
-   *    found.
-   */
-  public static HDDSVersion deserialize(int value) {
-    return BY_VALUE.getOrDefault(value, UNKNOWN_VERSION);
-  }
-
   @Override
   public String toString() {
     return name() + " (" + serialize() + ")";
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
new file mode 100644
index 00000000000..eeffc03e443
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
@@ -0,0 +1,151 @@
+/*
+ * 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 static 
org.apache.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME;
+import static org.jooq.impl.DSL.name;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Map;
+import javax.sql.DataSource;
+import org.apache.hadoop.hdds.ComponentVersion;
+import org.apache.hadoop.ozone.upgrade.ComponentUpgradeActionProvider;
+import org.apache.hadoop.ozone.upgrade.ComponentVersionManager;
+import org.apache.hadoop.ozone.upgrade.UpgradeException;
+import org.jooq.DSLContext;
+import org.jooq.InsertSetMoreStep;
+import org.jooq.UpdateSetMoreStep;
+import org.jooq.impl.DSL;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Recon-specific version manager for SQL schema upgrades.
+ */
+@Singleton
+public class ReconVersionManager extends ComponentVersionManager {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ReconVersionManager.class);
+
+  private final DataSource dataSource;
+  private final Map<ComponentVersion, ReconUpgradeAction> upgradeActions;
+
+  @Inject
+  public ReconVersionManager(DataSource dataSource) throws SQLException {
+    this(dataSource, new ReconUpgradeActionProvider());
+  }
+
+  @VisibleForTesting
+  ReconVersionManager(DataSource dataSource,
+      ComponentUpgradeActionProvider<ReconUpgradeAction> 
upgradeActionProvider) throws SQLException {
+    super(loadApparentVersion(dataSource), ReconVersion.SOFTWARE_VERSION);
+    this.dataSource = dataSource;
+    this.upgradeActions = upgradeActionProvider.load();
+  }
+
+  private static ComponentVersion loadApparentVersion(DataSource dataSource) 
throws SQLException {
+    int persisted = readPersistedApparentVersion(dataSource);
+    ReconVersion apparent = ReconVersion.deserialize(persisted);
+    if (apparent == ReconVersion.UNKNOWN_VERSION) {
+      throw new SQLException("Initialization failed. Database contains unknown 
apparent version "
+          + persisted + " for software version " + 
ReconVersion.SOFTWARE_VERSION
+          + ". Make sure this component was not downgraded after 
finalization");
+    }
+    return apparent;
+  }
+
+  /**
+   * Returns the persisted apparent version from {@code RECON_SCHEMA_VERSION}.
+   * If the table has no row, returns {@link ReconVersion#INITIAL_VERSION}.
+   */
+  private static int readPersistedApparentVersion(DataSource dataSource) 
throws SQLException {
+    try (Connection conn = dataSource.getConnection()) {
+      DSLContext dsl = DSL.using(conn);
+      return dsl.select(DSL.field(name("version_number")))
+          .from(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+          .fetchOptional()
+          .map(record -> record.get(
+              DSL.field(name("version_number"), Integer.class)))
+          .orElse(ReconVersion.INITIAL_VERSION.serialize());
+    } catch (Exception e) {
+      LOG.error("Failed to fetch the persisted apparent version.", e);
+      throw new SQLException("Unable to read apparent version from the 
table.", e);
+    }
+  }
+
+  @VisibleForTesting
+  Map<ComponentVersion, ReconUpgradeAction> getUpgradeActionsForTesting() {
+    return upgradeActions;
+  }
+
+  @Override
+  protected void persistApparentVersion(ComponentVersion newVersion) throws 
IOException {
+    int serializedVersion = newVersion.serialize();
+    try (Connection conn = dataSource.getConnection()) {
+      DSLContext dsl = DSL.using(conn);
+      boolean recordExists = dsl.fetchExists(dsl.selectOne()
+          .from(DSL.table(SCHEMA_VERSION_TABLE_NAME)));
+
+      if (recordExists) {
+        try (UpdateSetMoreStep<?> update = 
dsl.update(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+            .set(DSL.field(name("version_number")), serializedVersion)
+            .set(DSL.field(name("applied_on")), DSL.currentTimestamp())) {
+          update.execute();
+        }
+        LOG.info("Updated apparent version to '{}'.", serializedVersion);
+      } else {
+        try (InsertSetMoreStep<?> insert = 
dsl.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+            .set(DSL.field(name("version_number")), serializedVersion)
+            .set(DSL.field(name("applied_on")), DSL.currentTimestamp())) {
+          insert.execute();
+        }
+        LOG.info("Inserted new apparent version '{}'.", serializedVersion);
+      }
+    } catch (SQLException e) {
+      throw new IOException("Failed to persist apparent version " + 
newVersion, e);
+    }
+  }
+
+  @Override
+  public int getPersistedApparentVersion() {
+    try {
+      return readPersistedApparentVersion(dataSource);
+    } catch (SQLException e) {
+      throw new IllegalStateException("Failed to read persisted apparent 
version", e);
+    }
+  }
+
+  @Override
+  protected void runUpgradeAction(ComponentVersion version) throws 
UpgradeException {
+    ReconUpgradeAction action = upgradeActions.get(version);
+    if (action == null) {
+      return;
+    }
+    try {
+      action.execute(dataSource);
+    } catch (Exception e) {
+      logAndThrow(e, "Recon upgrade action for version " + version + " 
failed.",
+          UpgradeException.ResultCodes.FINALIZE_UPGRADE_ACTION_FAILED);
+    }
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicatedSizeOfFilesUpgradeAction.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicatedSizeOfFilesUpgradeAction.java
index 9d5a37cf7db..b12e2e93eea 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicatedSizeOfFilesUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicatedSizeOfFilesUpgradeAction.java
@@ -30,7 +30,7 @@
  * The action triggers a full rebuild of the NSSummary ensuring that the new 
field: replicatedSizeOfFiles is correctly
  * populated for all objects.
  */
-@UpgradeActionRecon(feature = ReconLayoutFeature.REPLICATED_SIZE_OF_FILES)
+@UpgradeActionRecon(feature = ReconVersion.REPLICATED_SIZE_OF_FILES)
 public class ReplicatedSizeOfFilesUpgradeAction implements ReconUpgradeAction {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(ReplicatedSizeOfFilesUpgradeAction.class);
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java
index ebf8556f5c4..a5b313192cc 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java
@@ -17,11 +17,10 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
-import static 
org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature.UNHEALTHY_CONTAINER_REPLICA_MISMATCH;
+import static 
org.apache.hadoop.ozone.recon.upgrade.ReconVersion.UNHEALTHY_CONTAINER_REPLICA_MISMATCH;
 import static 
org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME;
 import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK;
 import static org.jooq.impl.DSL.field;
-import static org.jooq.impl.DSL.name;
 
 import java.sql.Connection;
 import java.sql.SQLException;
@@ -80,7 +79,7 @@ private void addUpdatedConstraint() {
 
     
dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME)
         
.add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + 
"ck1")
-            .check(field(name("container_state"))
+            .check(field(DSL.name("container_state"))
                 .in(enumStates)))
         .execute();
 
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainersStateContainerIdIndexUpgradeAction.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainersStateContainerIdIndexUpgradeAction.java
index 6bd91b78a7e..ebfe459a6fc 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainersStateContainerIdIndexUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainersStateContainerIdIndexUpgradeAction.java
@@ -19,7 +19,6 @@
 
 import static 
org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME;
 import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK;
-import static org.jooq.impl.DSL.name;
 
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -34,7 +33,7 @@
 /**
  * Upgrade action to ensure idx_state_container_id exists on 
UNHEALTHY_CONTAINERS.
  */
-@UpgradeActionRecon(feature = 
ReconLayoutFeature.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX)
+@UpgradeActionRecon(feature = 
ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX)
 public class UnhealthyContainersStateContainerIdIndexUpgradeAction
     implements ReconUpgradeAction {
 
@@ -60,8 +59,8 @@ public void execute(DataSource source) throws Exception {
           UNHEALTHY_CONTAINERS_TABLE_NAME);
       dslContext.createIndex(INDEX_NAME)
           .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME),
-              DSL.field(name("container_state")),
-              DSL.field(name("container_id")))
+              DSL.field(DSL.name("container_state")),
+              DSL.field(DSL.name("container_id")))
           .execute();
     } catch (SQLException e) {
       throw new SQLException("Failed to create " + INDEX_NAME
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
index d4b59a09799..010af79851f 100644
--- 
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
@@ -53,7 +53,7 @@
 public @interface UpgradeActionRecon {
 
   /**
-   * Defines the layout feature this upgrade action is associated with.
+   * Defines the component version this upgrade action is associated with.
    */
-  ReconLayoutFeature feature();
+  ReconVersion feature();
 }
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
index 3c01312c43b..5db305044cc 100644
--- 
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
@@ -24,7 +24,7 @@
 import static 
org.apache.ozone.recon.schema.StatsSchemaDefinition.GLOBAL_STATS_TABLE_NAME;
 import static org.jooq.impl.DSL.name;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.mock;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -34,12 +34,10 @@
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.List;
-import javax.sql.DataSource;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.hadoop.ozone.recon.ReconContext;
-import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager;
-import org.apache.hadoop.ozone.recon.upgrade.ReconLayoutVersionManager;
+import org.apache.hadoop.ozone.recon.upgrade.ReconVersion;
+import org.apache.hadoop.ozone.recon.upgrade.ReconVersionManager;
 import org.apache.ozone.recon.schema.SchemaVersionTableDefinition;
 import org.jooq.DSLContext;
 import org.jooq.Record1;
@@ -141,7 +139,8 @@ public void testSchemaVersionCRUDOperations() throws 
SQLException {
    *
    * Expected Outcome:
    * - The schema version table is created during initialization.
-   * - The MLV is set to the latest SLV (Software Layout Version), indicating 
the schema is up-to-date.
+   * - The persisted apparent version in the table is set to the current 
software version, indicating the schema is
+   * up-to-date.
    * - No upgrade actions are triggered as all tables are already at the 
latest version.
    */
   @Test
@@ -153,22 +152,17 @@ public void testFreshInstallScenario() throws Exception {
 
     // Initialize the schema
     SchemaVersionTableDefinition schemaVersionTable = new 
SchemaVersionTableDefinition(getDataSource());
-    schemaVersionTable.setLatestSLV(3); // Assuming the latest SLV = 3
+    schemaVersionTable.setSoftwareVersion(3);
     schemaVersionTable.initializeSchema();
 
     // Verify that the SchemaVersionTable is created
     boolean tableExists = TABLE_EXISTS_CHECK.test(connection, 
SCHEMA_VERSION_TABLE_NAME);
-    assertEquals(true, tableExists, "The Schema Version Table should be 
created.");
+    assertTrue(tableExists, "The Schema Version Table should be created.");
 
-    // Initialize ReconSchemaVersionTableManager and ReconLayoutVersionManager
-    ReconSchemaVersionTableManager schemaVersionTableManager = new 
ReconSchemaVersionTableManager(getDataSource());
-    DataSource mockDataSource = mock(DataSource.class);
-    ReconLayoutVersionManager layoutVersionManager =
-        new ReconLayoutVersionManager(schemaVersionTableManager, 
mock(ReconContext.class), mockDataSource);
-
-    // Fetch and verify the current MLV
-    int mlv = layoutVersionManager.getCurrentMLV();
-    assertEquals(3, mlv, "For a fresh install, MLV should be set to the latest 
SLV value.");
+    try (ReconVersionManager versionManager = new 
ReconVersionManager(getDataSource())) {
+      assertEquals(3, versionManager.getPersistedApparentVersion(),
+          "For a fresh install, apparent version should equal software 
version.");
+    }
   }
 
   /**
@@ -178,7 +172,7 @@ public void testFreshInstallScenario() throws Exception {
    *
    * Expected Outcome:
    * - The schema version table is created during initialization.
-   * - The MLV is set to -1, indicating the starting point of the schema 
version framework.
+   * - The apparent version is INITIAL_VERSION (empty version table).
    * - Ensures only necessary upgrades are executed, avoiding redundant 
updates.
    */
   @Test
@@ -198,29 +192,24 @@ public void testPreUpgradedClusterScenario() throws 
Exception {
 
     // Verify SchemaVersionTable is created
     boolean tableExists = TABLE_EXISTS_CHECK.test(connection, 
SCHEMA_VERSION_TABLE_NAME);
-    assertEquals(true, tableExists, "The Schema Version Table should be 
created.");
+    assertTrue(tableExists, "The Schema Version Table should be created.");
 
-    // Initialize ReconSchemaVersionTableManager and ReconLayoutVersionManager
-    ReconSchemaVersionTableManager schemaVersionTableManager = new 
ReconSchemaVersionTableManager(getDataSource());
-    DataSource mockDataSource = mock(DataSource.class);
-    ReconLayoutVersionManager layoutVersionManager =
-        new ReconLayoutVersionManager(schemaVersionTableManager, 
mock(ReconContext.class), mockDataSource);
-
-    // Fetch and verify the current MLV
-    int mlv = layoutVersionManager.getCurrentMLV();
-    assertEquals(-1, mlv, "For a pre-upgraded cluster, MLV should be set to 
-1.");
+    try (ReconVersionManager versionManager = new 
ReconVersionManager(getDataSource())) {
+      assertEquals(ReconVersion.INITIAL_VERSION.serialize(), 
versionManager.getPersistedApparentVersion(),
+          "For a pre-upgraded cluster, apparent version should be 
INITIAL_VERSION.");
+    }
   }
 
   /***
    * Scenario:
    * - This simulates a cluster where the schema version table already exists,
    *   indicating the schema version framework is in place.
-   * - The schema version table contains a previously finalized Metadata 
Layout Version (MLV).
+   * - The schema version table contains a previously finalized apparent 
version.
    *
    * Expected Outcome:
-   * - The MLV stored in the schema version table (2) is correctly read by the 
ReconLayoutVersionManager.
-   * - The MLV is retained and not overridden by the SLV value (3) during 
schema initialization.
-   * - This ensures no unnecessary upgrades are triggered and the existing MLV 
remains consistent.
+   * - The apparent version stored in the schema version table (2) is 
correctly read.
+   * - It is retained and not overridden by the software version (3) during 
schema initialization.
+   * - This ensures no unnecessary upgrades are triggered and the existing 
apparent version remains consistent.
    */
   @Test
   public void testUpgradedClusterScenario() throws Exception {
@@ -236,7 +225,7 @@ public void testUpgradedClusterScenario() throws Exception {
       createSchemaVersionTable(connection);
     }
 
-    // Insert a single existing MLV (e.g., version 2) into the Schema Version 
Table
+    // Insert apparent version 2 into the Schema Version Table
     DSLContext dslContext = DSL.using(connection);
     dslContext.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME))
         .columns(DSL.field(name("version_number")),
@@ -246,22 +235,14 @@ public void testUpgradedClusterScenario() throws 
Exception {
 
     // Initialize the schema
     SchemaVersionTableDefinition schemaVersionTable = new 
SchemaVersionTableDefinition(getDataSource());
-    schemaVersionTable.setLatestSLV(3); // Assuming the latest SLV = 3
+    schemaVersionTable.setSoftwareVersion(3);
     schemaVersionTable.initializeSchema();
 
-    // Initialize managers to interact with schema version framework
-    ReconSchemaVersionTableManager schemaVersionTableManager = new 
ReconSchemaVersionTableManager(getDataSource());
-    DataSource mockDataSource = mock(DataSource.class);
-    ReconLayoutVersionManager layoutVersionManager =
-        new ReconLayoutVersionManager(schemaVersionTableManager, 
mock(ReconContext.class), mockDataSource);
-
-    // Fetch and verify the current MLV stored in the database
-    int mlv = layoutVersionManager.getCurrentMLV();
-
-    // Assert that the MLV stored in the DB is retained and not overridden by 
the SLV value
-    // when running initializeSchema() before upgrade takes place
-    assertEquals(2, mlv, "For a cluster with an existing schema version 
framework, " +
-        "the MLV should match the value stored in the DB.");
+    try (ReconVersionManager versionManager = new 
ReconVersionManager(getDataSource())) {
+      assertEquals(2, versionManager.getPersistedApparentVersion(),
+          "For a cluster with an existing schema version framework, " +
+              "the apparent version should match the value stored in the DB.");
+    }
   }
 
   /**
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
deleted file mode 100644
index 5f2d451dd06..00000000000
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
- * 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 static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import javax.sql.DataSource;
-import org.apache.hadoop.ozone.recon.ReconContext;
-import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager;
-import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.InOrder;
-import org.mockito.MockedStatic;
-
-/**
- * Tests for ReconLayoutVersionManager.
- */
-public class TestReconLayoutVersionManager {
-
-  private ReconSchemaVersionTableManager schemaVersionTableManager;
-  private ReconLayoutVersionManager layoutVersionManager;
-  private MockedStatic<ReconLayoutFeature> mockedEnum;
-  private DataSource mockDataSource;
-  private Connection mockConnection;
-
-  @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);
-
-    ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class);
-    when(feature1.getVersion()).thenReturn(1);
-    ReconUpgradeAction action1 = mock(ReconUpgradeAction.class);
-    when(feature1.getAction())
-        .thenReturn(Optional.of(action1));
-
-    ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class);
-    when(feature2.getVersion()).thenReturn(2);
-    ReconUpgradeAction action2 = mock(ReconUpgradeAction.class);
-    when(feature2.getAction())
-        .thenReturn(Optional.of(action2));
-
-    // Common mocks for all tests
-    ReconStorageContainerManagerFacade scmFacadeMock = 
mock(ReconStorageContainerManagerFacade.class);
-    mockDataSource = mock(DataSource.class);
-    mockConnection = mock(Connection.class);
-
-    // Define the custom features to be returned
-    mockedEnum.when(ReconLayoutFeature::values).thenReturn(new 
ReconLayoutFeature[]{feature1, feature2});
-
-    layoutVersionManager = new 
ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class),
-        mockDataSource);
-
-    when(scmFacadeMock.getDataSource()).thenReturn(mockDataSource);
-    when(mockDataSource.getConnection()).thenReturn(mockConnection);
-
-    doNothing().when(mockConnection).setAutoCommit(false);
-    doNothing().when(mockConnection).commit();
-    doNothing().when(mockConnection).rollback();
-  }
-
-  @AfterEach
-  public void tearDown() {
-    // Close the static mock after each test to deregister it
-    mockedEnum.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 ensures that the 
updateSchemaVersion for
-   * the schemaVersionTable is triggered for each feature version.
-   */
-  @Test
-  public void testFinalizeLayoutFeaturesWithMockedValues() throws SQLException 
{
-    // Execute the method under test
-    layoutVersionManager.finalizeLayoutFeatures();
-
-    // Verify that schema versions are updated for our custom features
-    verify(schemaVersionTableManager, times(1))
-        .updateSchemaVersion(1, mockConnection);
-    verify(schemaVersionTableManager, times(1))
-        .updateSchemaVersion(2, mockConnection);
-  }
-
-  /**
-   * 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 {
-    // Ensure no layout features are present
-    mockedEnum.when(ReconLayoutFeature::values).thenReturn(new 
ReconLayoutFeature[]{});
-
-    // Execute the method under test
-    layoutVersionManager.finalizeLayoutFeatures();
-
-    // Verify that no schema version updates were attempted
-    verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt(), 
any(Connection.class));
-  }
-
-  /**
-   * 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(mockDataSource);
-    when(feature1.getAction())
-        .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) {
-      // Exception is expected, so it's fine to catch and ignore it here
-    }
-
-    // Verify that metadata layout version MLV was not updated as the 
transaction was rolled back
-    assertEquals(0, layoutVersionManager.getCurrentMLV());
-
-    // Verify that a rollback was triggered
-    verify(mockConnection, times(1)).rollback();
-  }
-
-  /**
-   * Tests the scenario where the schema version update fails. Ensures that if 
the schema
-   * version update fails, the transaction is rolled back and the metadata 
layout version
-   * is not updated.
-   */
-  @Test
-  public void testUpdateSchemaFailure() 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 schema version update
-    doThrow(new RuntimeException("Schema update 
failed")).when(schemaVersionTableManager).
-        updateSchemaVersion(1, mockConnection);
-    when(feature1.getAction())
-        .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) {
-      // Exception is expected, so it's fine to catch and ignore it here
-    }
-
-    // Verify that metadata layout version MLV was not updated as the 
transaction was rolled back
-    assertEquals(0, layoutVersionManager.getCurrentMLV());
-
-    // Verify that the upgrade action was not committed and a rollback was 
triggered
-    verify(mockConnection, times(1)).rollback();
-  }
-
-  /**
-   * 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())
-        .thenReturn(Optional.of(action1));
-
-    ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class);
-    when(feature2.getVersion()).thenReturn(2);
-    ReconUpgradeAction action2 = mock(ReconUpgradeAction.class);
-    when(feature2.getAction())
-        .thenReturn(Optional.of(action2));
-
-    ReconLayoutFeature feature3 = mock(ReconLayoutFeature.class);
-    when(feature3.getVersion()).thenReturn(3);
-    ReconUpgradeAction action3 = mock(ReconUpgradeAction.class);
-    when(feature3.getAction())
-        .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(mockDataSource); // Should be executed 
first
-    inOrder.verify(action2).execute(mockDataSource); // Should be executed 
second
-    inOrder.verify(action3).execute(mockDataSource); // 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 {
-    // Mock the current schema version to the maximum layout version
-    when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(0);
-
-    mockedEnum.when(ReconLayoutFeature::values).thenReturn(new 
ReconLayoutFeature[]{});
-
-    // Execute the method under test
-    layoutVersionManager.finalizeLayoutFeatures();
-
-    // Verify that no schema version updates were attempted
-    verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt(), 
eq(mockConnection));
-  }
-
-  /**
-   * 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: Mock the schema version manager
-    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())
-        .thenReturn(Optional.of(action1));
-
-    ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class);
-    when(feature2.getVersion()).thenReturn(2);
-    ReconUpgradeAction action2 = mock(ReconUpgradeAction.class);
-    when(feature2.getAction())
-        .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, 
mockConnection);
-    verify(schemaVersionTableManager, times(1)).updateSchemaVersion(2, 
mockConnection);
-
-    // 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())
-        .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, 
mockConnection);
-
-    // Verify that action1 and action2 were not executed again.
-    verify(action1, times(1)).execute(mockDataSource);
-    verify(action2, times(1)).execute(mockDataSource);
-
-    // Verify that the upgrade action for feature 3 was executed.
-    verify(action3, times(1)).execute(mockDataSource);
-  }
-
-}
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersion.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersion.java
new file mode 100644
index 00000000000..9bda982837d
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersion.java
@@ -0,0 +1,59 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hadoop.hdds.AbstractComponentVersionTest;
+import org.apache.hadoop.hdds.ComponentVersion;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Invariants for {@link ReconVersion}.
+ */
+public class TestReconVersion extends AbstractComponentVersionTest {
+
+  @Override
+  protected ComponentVersion[] getValues() {
+    return ReconVersion.values();
+  }
+
+  @Override
+  protected ComponentVersion getDefaultVersion() {
+    return ReconVersion.INITIAL_VERSION;
+  }
+
+  @Override
+  protected ComponentVersion getUnknownVersion() {
+    return ReconVersion.UNKNOWN_VERSION;
+  }
+
+  @Override
+  protected ComponentVersion deserialize(int value) {
+    return ReconVersion.deserialize(value);
+  }
+
+  @Test
+  public void testKnownVersionNumbersAreContiguous() {
+    ReconVersion[] values = ReconVersion.values();
+    int knownVersionCount = values.length - 1;
+    for (int i = 0; i < knownVersionCount - 1; i++) {
+      assertEquals(values[i].serialize() + 1, values[i + 1].serialize());
+    }
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
new file mode 100644
index 00000000000..24033063e31
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
@@ -0,0 +1,237 @@
+/*
+ * 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 static 
org.apache.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME;
+import static org.jooq.impl.DSL.name;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.sql.DataSource;
+import org.apache.hadoop.hdds.ComponentVersion;
+import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
+import org.apache.hadoop.ozone.recon.persistence.DataSourceConfiguration;
+import org.apache.hadoop.ozone.recon.persistence.JooqPersistenceModule;
+import org.apache.hadoop.ozone.upgrade.AbstractComponentVersionManagerTest;
+import org.apache.hadoop.ozone.upgrade.ComponentUpgradeActionProvider;
+import org.apache.hadoop.ozone.upgrade.ComponentVersionManager;
+import org.apache.hadoop.ozone.upgrade.UpgradeException;
+import org.jooq.Configuration;
+import org.jooq.CreateTableColumnStep;
+import org.jooq.DSLContext;
+import org.jooq.InsertSetMoreStep;
+import org.jooq.impl.DSL;
+import org.jooq.impl.SQLDataType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.provider.Arguments;
+
+/**
+ * Tests for {@link ReconVersionManager}.
+ */
+class TestReconVersionManager extends AbstractComponentVersionManagerTest {
+
+  @TempDir
+  private Path tempDir;
+
+  private static final List<ComponentVersion> ALL_VERSIONS =
+      Arrays.stream(ReconVersion.values())
+          .filter(v -> v != ReconVersion.UNKNOWN_VERSION)
+          .collect(Collectors.toList());
+
+  public static Stream<Arguments> preFinalizedVersionArgs() {
+    return ALL_VERSIONS.stream()
+        .limit(ALL_VERSIONS.size() - 1)
+        .map(Arguments::of);
+  }
+
+  @Override
+  protected ComponentVersionManager createManager(int 
serializedApparentVersion) throws IOException {
+    return createManager(serializedApparentVersion, HashMap::new);
+  }
+
+  private ReconVersionManager createManager(int serializedApparentVersion,
+      ComponentUpgradeActionProvider<ReconUpgradeAction> actions) throws 
IOException {
+    try {
+      Injector injector = createTestInjector("recon-version-");
+      DataSource dataSource = injector.getInstance(DataSource.class);
+      initializeVersionTable(injector.getInstance(Configuration.class), 
serializedApparentVersion);
+      return new ReconVersionManager(dataSource, actions);
+    } catch (SQLException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private Injector createTestInjector(String directoryPrefix) throws 
IOException {
+    Path dbDir = Files.createTempDirectory(tempDir, directoryPrefix);
+    File configDir = Files.createDirectory(dbDir.resolve("Config")).toFile();
+    Provider<DataSourceConfiguration> configProvider =
+        new 
AbstractReconSqlDBTest.DerbyDataSourceConfigurationProvider(configDir);
+    return Guice.createInjector(
+        new JooqPersistenceModule(configProvider),
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(DataSourceConfiguration.class).toProvider(configProvider);
+          }
+        });
+  }
+
+  private static void initializeVersionTable(Configuration configuration,
+      int serializedApparentVersion) throws SQLException {
+    try (DSLContext dsl = DSL.using(configuration)) {
+      try (CreateTableColumnStep createTable = 
dsl.createTableIfNotExists(SCHEMA_VERSION_TABLE_NAME)
+          .column("version_number", SQLDataType.INTEGER.nullable(false))
+          .column("applied_on", SQLDataType.TIMESTAMP)) {
+        createTable.execute();
+      }
+      dsl.deleteFrom(DSL.table(SCHEMA_VERSION_TABLE_NAME)).execute();
+      if (serializedApparentVersion != 
ReconVersion.INITIAL_VERSION.serialize()) {
+        try (InsertSetMoreStep<?> insert = 
dsl.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME))
+            .set(DSL.field(name("version_number")), serializedApparentVersion)
+            .set(DSL.field(name("applied_on")), DSL.currentTimestamp())) {
+          insert.execute();
+        }
+      }
+    }
+  }
+
+  @Override
+  protected List<ComponentVersion> allVersionsInOrder() {
+    return ALL_VERSIONS;
+  }
+
+  @Override
+  protected ComponentVersion expectedSoftwareVersion() {
+    return ReconVersion.SOFTWARE_VERSION;
+  }
+
+  @Override
+  @Test
+  public void testClasspathScanDiscoversUpgradeActions() throws Exception {
+    try (ReconVersionManager versionManager = createManager(
+        ReconVersion.INITIAL_VERSION.serialize(), new 
ReconUpgradeActionProvider())) {
+      assertTrue(versionManager.needsFinalization());
+      ReconUpgradeAction taskStatusAction = 
versionManager.getUpgradeActionsForTesting()
+          .get(ReconVersion.TASK_STATUS_STATISTICS);
+      assertInstanceOf(ReconTaskStatusTableUpgradeAction.class, 
taskStatusAction);
+    }
+
+    try (ReconVersionManager versionManager = createManager(
+        ReconVersion.SOFTWARE_VERSION.serialize(), new 
ReconUpgradeActionProvider())) {
+      assertFalse(versionManager.needsFinalization());
+      ReconUpgradeAction taskStatusAction = 
versionManager.getUpgradeActionsForTesting()
+          .get(ReconVersion.TASK_STATUS_STATISTICS);
+      assertInstanceOf(ReconTaskStatusTableUpgradeAction.class, 
taskStatusAction);
+    }
+  }
+
+  @Override
+  @Test
+  public void testFinalizeRunsSuppliedUpgradeAction() throws Exception {
+    ReconUpgradeAction mockReplicaMismatchAction = 
mock(ReconUpgradeAction.class);
+    ReconUpgradeAction mockContainerIdIndexAction = 
mock(ReconUpgradeAction.class);
+
+    ComponentUpgradeActionProvider<ReconUpgradeAction> provider = () -> {
+      Map<ComponentVersion, ReconUpgradeAction> m = new HashMap<>();
+      m.put(ReconVersion.UNHEALTHY_CONTAINER_REPLICA_MISMATCH, 
mockReplicaMismatchAction);
+      m.put(ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX, 
mockContainerIdIndexAction);
+      return m;
+    };
+
+    try (ReconVersionManager versionManager = createManager(
+        ReconVersion.UNHEALTHY_CONTAINER_REPLICA_MISMATCH.serialize(), 
provider)) {
+      versionManager.finalizeUpgrade();
+      assertEquals(ReconVersion.SOFTWARE_VERSION, 
versionManager.getApparentVersion());
+
+      // Apparent version is already UNHEALTHY_CONTAINER_REPLICA_MISMATCH; 
finalization runs actions for later
+      // versions only, not for UNHEALTHY_CONTAINER_REPLICA_MISMATCH itself.
+      verify(mockReplicaMismatchAction, never()).execute(any());
+      verify(mockContainerIdIndexAction, atLeastOnce()).execute(any());
+      assertEquals(ReconVersion.SOFTWARE_VERSION.serialize(),
+          versionManager.getPersistedApparentVersion());
+    }
+  }
+
+  @Override
+  @Test
+  public void testUpgradeActionFailureAbortsFinalize() throws Exception {
+    ComponentUpgradeActionProvider<ReconUpgradeAction> provider = () -> {
+      Map<ComponentVersion, ReconUpgradeAction> m = new HashMap<>();
+      m.put(ReconVersion.TASK_STATUS_STATISTICS, ds -> {
+        throw new IOException("expected test failure");
+      });
+      return m;
+    };
+
+    try (ReconVersionManager versionManager = createManager(
+        ReconVersion.INITIAL_VERSION.serialize(), provider)) {
+      UpgradeException thrown = assertThrows(UpgradeException.class, 
versionManager::finalizeUpgrade);
+      
assertEquals(UpgradeException.ResultCodes.FINALIZE_UPGRADE_ACTION_FAILED, 
thrown.getResult());
+      assertEquals(ReconVersion.INITIAL_VERSION, 
versionManager.getApparentVersion());
+      assertEquals(ReconVersion.INITIAL_VERSION.serialize(),
+          versionManager.getPersistedApparentVersion());
+    }
+  }
+
+  @Override
+  @Test
+  public void testPersistFailureRollsBack() throws Exception {
+    Injector injector = createTestInjector("recon-persist-fail-");
+    DataSource dataSource = injector.getInstance(DataSource.class);
+    initializeVersionTable(injector.getInstance(Configuration.class),
+        ReconVersion.INITIAL_VERSION.serialize());
+    ReconVersionManager versionManager = spy(new 
ReconVersionManager(dataSource, HashMap::new));
+    doThrow(new IOException("persist failed"))
+        
.when(versionManager).persistApparentVersion(ReconVersion.TASK_STATUS_STATISTICS);
+
+    try (ReconVersionManager manager = versionManager) {
+      assertEquals(ReconVersion.INITIAL_VERSION, manager.getApparentVersion());
+      UpgradeException thrown = assertThrows(UpgradeException.class, 
manager::finalizeUpgrade);
+      
assertEquals(UpgradeException.ResultCodes.APPARENT_VERSION_UPDATE_FAILED, 
thrown.getResult());
+      assertEquals(ReconVersion.INITIAL_VERSION, manager.getApparentVersion());
+      assertEquals(ReconVersion.INITIAL_VERSION.serialize(),
+          manager.getPersistedApparentVersion());
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to