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 353b7cc67cd HDDS-15488. Recon upgrade actions should be idempotent 
(#10442)
353b7cc67cd is described below

commit 353b7cc67cdbb8de63f778f9ee48adbf92a6fe54
Author: Ethan Rose <[email protected]>
AuthorDate: Tue Jun 16 14:52:50 2026 -0400

    HDDS-15488. Recon upgrade actions should be idempotent (#10442)
---
 .../org/apache/ozone/recon/schema/SqlDbUtils.java  |  98 ++++++++++++++++
 .../upgrade/InitialConstraintUpgradeAction.java    |  59 +---------
 .../upgrade/NSSummaryAggregatedTotalsUpgrade.java  |  22 +---
 .../upgrade/ReconTaskStatusTableUpgradeAction.java |  57 ++++-----
 .../ozone/recon/upgrade/ReconUpgradeAction.java    |  86 ++++++++++++++
 .../ReplicatedSizeOfFilesUpgradeAction.java        |  26 +----
 .../UnhealthyContainerReplicaMismatchAction.java   |  53 +--------
 ...ntainersStateContainerIdIndexUpgradeAction.java |  20 +---
 .../TestInitialConstraintUpgradeAction.java        |  17 ++-
 .../TestNSSummaryAggregatedTotalsUpgrade.java      |  24 ++++
 .../TestReconTaskStatusTableUpgradeAction.java     | 127 +++++++++++++++++++++
 .../TestReplicatedSizeOfFilesUpgradeAction.java    |  36 ++++--
 ...stUnhealthyContainerReplicaMismatchAction.java} |  55 +++------
 ...ntainersStateContainerIdIndexUpgradeAction.java |  25 ++--
 14 files changed, 432 insertions(+), 273 deletions(-)

diff --git 
a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SqlDbUtils.java
 
b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SqlDbUtils.java
index 22c00cb9685..5e75bbe9413 100644
--- 
a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SqlDbUtils.java
+++ 
b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/SqlDbUtils.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.sql.Connection;
+import java.sql.DatabaseMetaData;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -115,4 +116,101 @@ public static List<String> listAllTables(Connection 
connection) throws SQLExcept
     LOG.debug("Found {} user-defined tables in the database: {}", 
tableNames.size(), tableNames);
     return tableNames;
   }
+
+  /**
+   * Checks whether a column exists in a Derby table via {@code 
SYS.SYSCOLUMNS}.
+   *
+   * @param conn the database connection
+   * @param tableName the table name (case-insensitive)
+   * @param columnName the column name (case-insensitive)
+   * @return true if the column exists
+   */
+  public static boolean columnExists(Connection conn, String tableName, String 
columnName)
+      throws SQLException {
+    DatabaseMetaData metaData = conn.getMetaData();
+    try (ResultSet rs = metaData.getColumns(null, null, tableName, 
columnName)) {
+      if (rs.next()) {
+        return true;
+      }
+    }
+    try (ResultSet rs = metaData.getColumns(null, null, 
tableName.toUpperCase(), columnName.toUpperCase())) {
+      return rs.next();
+    }
+  }
+
+  /**
+   * Checks whether a Derby column allows NULL values.
+   *
+   * @param conn the database connection
+   * @param tableName the table name (case-insensitive)
+   * @param columnName the column name (case-insensitive)
+   * @return true if the column is nullable, false if NOT NULL or column does 
not exist
+   */
+  public static boolean isColumnNullable(Connection conn, String tableName, 
String columnName)
+      throws SQLException {
+    DatabaseMetaData metaData = conn.getMetaData();
+    try (ResultSet rs = metaData.getColumns(null, null, tableName, 
columnName)) {
+      if (rs.next()) {
+        return rs.getInt("NULLABLE") != DatabaseMetaData.columnNoNulls;
+      }
+    }
+    try (ResultSet rs = metaData.getColumns(null, null, 
tableName.toUpperCase(), columnName.toUpperCase())) {
+      if (rs.next()) {
+        return rs.getInt("NULLABLE") != DatabaseMetaData.columnNoNulls;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Checks whether a named constraint exists on a Derby table.
+   *
+   * @param conn the database connection
+   * @param tableName the table name (case-insensitive)
+   * @param constraintName the constraint name (case-insensitive)
+   * @return true if the constraint exists on the table
+   */
+  public static boolean constraintExists(Connection conn, String tableName, 
String constraintName)
+      throws SQLException {
+    String sql = "SELECT 1 FROM SYS.SYSCONSTRAINTS c "
+        + "INNER JOIN SYS.SYSTABLES t ON c.TABLEID = t.TABLEID "
+        + "WHERE t.TABLENAME = ? AND c.CONSTRAINTNAME = ?";
+    try (java.sql.PreparedStatement ps = conn.prepareStatement(sql)) {
+      ps.setString(1, tableName.toUpperCase());
+      ps.setString(2, constraintName);
+      try (ResultSet rs = ps.executeQuery()) {
+        if (rs.next()) {
+          return true;
+        }
+      }
+    }
+    try (java.sql.PreparedStatement ps = conn.prepareStatement(sql)) {
+      ps.setString(1, tableName.toUpperCase());
+      ps.setString(2, constraintName.toUpperCase());
+      try (ResultSet rs = ps.executeQuery()) {
+        return rs.next();
+      }
+    }
+  }
+
+  /**
+   * Checks whether a named index exists on a table.
+   *
+   * @param conn the database connection
+   * @param tableName the table name
+   * @param indexName the index name (case-insensitive)
+   * @return true if the index exists
+   */
+  public static boolean indexExists(Connection conn, String tableName, String 
indexName) throws SQLException {
+    DatabaseMetaData metaData = conn.getMetaData();
+    try (ResultSet rs = metaData.getIndexInfo(null, null, tableName, false, 
false)) {
+      while (rs.next()) {
+        String existing = rs.getString("INDEX_NAME");
+        if (existing != null && existing.equalsIgnoreCase(indexName)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
 }
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 6f632de180b..3adadafe222 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
@@ -18,18 +18,9 @@
 package org.apache.hadoop.ozone.recon.upgrade;
 
 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 com.google.common.annotations.VisibleForTesting;
-import java.sql.Connection;
 import java.sql.SQLException;
-import java.util.Arrays;
 import javax.sql.DataSource;
-import org.apache.ozone.recon.schema.ContainerSchemaDefinition;
-import org.jooq.DSLContext;
-import org.jooq.impl.DSL;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,58 +30,10 @@
  */
 @UpgradeActionRecon(feature = INITIAL_VERSION)
 public class InitialConstraintUpgradeAction implements ReconUpgradeAction {
-
   private static final Logger LOG = 
LoggerFactory.getLogger(InitialConstraintUpgradeAction.class);
-  private DSLContext dslContext;
 
   @Override
   public void execute(DataSource source) throws SQLException {
-    try (Connection conn = source.getConnection()) {
-      if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) {
-        return;
-      }
-      dslContext = DSL.using(conn);
-      // Drop the existing constraint
-      dropConstraint();
-      // Add the updated constraint with all enum states
-      addUpdatedConstraint();
-    } catch (SQLException e) {
-      throw new SQLException("Failed to execute 
InitialConstraintUpgradeAction", e);
-    }
-  }
-
-  /**
-   * Drops the existing constraint from the UNHEALTHY_CONTAINERS table.
-   */
-  private void dropConstraint() {
-    String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1";
-    dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME)
-        .dropConstraint(constraintName)
-        .execute();
-    LOG.debug("Dropped the existing constraint: {}", constraintName);
-  }
-
-  /**
-   * Adds the updated constraint directly within this class.
-   */
-  private void addUpdatedConstraint() {
-    String[] enumStates = Arrays
-        .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values())
-        .map(Enum::name)
-        .toArray(String[]::new);
-
-    
dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME)
-        
.add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + 
"ck1")
-        .check(field(DSL.name("container_state"))
-        .in(enumStates)))
-        .execute();
-
-    LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table 
for enum state values: {}",
-        Arrays.toString(enumStates));
-  }
-
-  @VisibleForTesting
-  public void setDslContext(DSLContext dslContext) {
-    this.dslContext = dslContext;
+    ReconUpgradeAction.updateUnhealthyContainerStatesConstraint(source, LOG);
   }
 }
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 f153c4a35ac..b806e29fd61 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
@@ -17,11 +17,7 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
-import com.google.inject.Injector;
 import javax.sql.DataSource;
-import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
-import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
-import org.apache.hadoop.ozone.recon.tasks.ReconTaskReInitializationEvent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -40,22 +36,6 @@ public class NSSummaryAggregatedTotalsUpgrade implements 
ReconUpgradeAction {
 
   @Override
   public void execute(DataSource source) throws Exception {
-    // Resolve required services from Guice
-    Injector injector = ReconGuiceServletContextListener.getGlobalInjector();
-    if (injector == null) {
-      throw new IllegalStateException(
-          "Guice injector not initialized. NSSummary rebuild cannot proceed 
during upgrade.");
-    }
-
-    ReconTaskController reconTaskController = 
injector.getInstance(ReconTaskController.class);
-    LOG.info("Triggering asynchronous NSSummary tree rebuild for materialized 
totals (upgrade action).");
-    ReconTaskController.ReInitializationResult result = 
reconTaskController.queueReInitializationEvent(
-        ReconTaskReInitializationEvent.ReInitializationReason.MANUAL_TRIGGER);
-    if (result != ReconTaskController.ReInitializationResult.SUCCESS) {
-      LOG.error(
-          "Failed to queue reinitialization event for manual trigger (result: 
{}), failing the reinitialization " +
-              "during NSSummaryAggregatedTotalsUpgrade action, will be retried 
as part of syncDataFromOM " +
-              "scheduler task.", result);
-    }
+    ReconUpgradeAction.queueNSSummaryRebuildIfNeeded(LOG);
   }
 }
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 49e86b0d55e..91509598c4f 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
@@ -19,6 +19,8 @@
 
 import static 
org.apache.ozone.recon.schema.ReconTaskSchemaDefinition.RECON_TASK_STATUS_TABLE_NAME;
 import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK;
+import static org.apache.ozone.recon.schema.SqlDbUtils.columnExists;
+import static org.apache.ozone.recon.schema.SqlDbUtils.isColumnNullable;
 
 import java.sql.Connection;
 import java.sql.SQLException;
@@ -40,31 +42,37 @@
 public class ReconTaskStatusTableUpgradeAction implements ReconUpgradeAction {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(ReconTaskStatusTableUpgradeAction.class);
+  private static final String LAST_TASK_RUN_STATUS = "last_task_run_status";
+  private static final String IS_CURRENT_TASK_RUNNING = 
"is_current_task_running";
 
-  /**
-   * Utility function to add provided column to RECON_TASK_STATUS table as 
INTEGER type.
-   * @param dslContext  Stores {@link DSLContext} to perform alter operations
-   * @param columnName  Name of the column to be inserted to the table
-   */
-  private void addColumnToTable(DSLContext dslContext, String columnName) {
-    //Column is set as nullable to avoid any errors.
+  private void addColumnIfMissing(Connection conn, DSLContext dslContext, 
String columnName)
+      throws SQLException {
+    if (columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, columnName)) {
+      LOG.info("Column '{}' already exists on {}, skipping add.", columnName, 
RECON_TASK_STATUS_TABLE_NAME);
+      return;
+    }
+    LOG.info("Adding '{}' column to task status table", columnName);
     dslContext.alterTable(RECON_TASK_STATUS_TABLE_NAME)
-        .addColumn(columnName, SQLDataType.INTEGER.nullable(true)).execute();
+        .addColumn(columnName, SQLDataType.INTEGER.nullable(true))
+        .execute();
   }
 
-  /**
-   *  Utility function to set the provided column as Non-Null to enforce 
constraints in RECON_TASK_STATUS table.
-   * @param dslContext Stores {@link DSLContext} to perform alter operations
-   * @param columnName Name of the column to set as non-null
-   */
-  private void setColumnAsNonNullable(DSLContext dslContext, String 
columnName) {
+  private void setColumnAsNonNullableIfNeeded(Connection conn, DSLContext 
dslContext, String columnName)
+      throws SQLException {
+    if (!columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, columnName)) {
+      return;
+    }
+    if (!isColumnNullable(conn, RECON_TASK_STATUS_TABLE_NAME, columnName)) {
+      LOG.info("Column '{}' is already NOT NULL on {}, skipping.", columnName, 
RECON_TASK_STATUS_TABLE_NAME);
+      return;
+    }
     dslContext.alterTable(RECON_TASK_STATUS_TABLE_NAME)
         .alterColumn(DSL.name(columnName)).setNotNull()
         .execute();
   }
 
   @Override
-  public void execute(DataSource dataSource) throws DataAccessException {
+  public void execute(DataSource dataSource) throws DataAccessException, 
SQLException {
     try (Connection conn = dataSource.getConnection()) {
       if (!TABLE_EXISTS_CHECK.test(conn, RECON_TASK_STATUS_TABLE_NAME)) {
         return;
@@ -73,23 +81,18 @@ public void execute(DataSource dataSource) throws 
DataAccessException {
       DSLContext dslContext = DSL.using(conn);
       // JOOQ doesn't support Derby DB officially, there is no way to run 'ADD 
COLUMN' command in single call
       // for multiple columns. Hence, we run it as two separate steps.
-      LOG.info("Adding 'last_task_run_status' column to task status table");
-      addColumnToTable(dslContext, "last_task_run_status");
-      LOG.info("Adding 'is_current_task_running' column to task status table");
-      addColumnToTable(dslContext, "is_current_task_running");
+      addColumnIfMissing(conn, dslContext, LAST_TASK_RUN_STATUS);
+      addColumnIfMissing(conn, dslContext, IS_CURRENT_TASK_RUNNING);
 
-      //Handle previous table values with new columns default values
+      // Handle previous table values with new columns default values
       int updatedRowCount = 
dslContext.update(DSL.table(RECON_TASK_STATUS_TABLE_NAME))
-          .set(DSL.field(DSL.name("last_task_run_status"), 
SQLDataType.INTEGER), 0)
-          .set(DSL.field(DSL.name("is_current_task_running"), 
SQLDataType.INTEGER), 0)
+          .set(DSL.field(DSL.name(LAST_TASK_RUN_STATUS), SQLDataType.INTEGER), 
0)
+          .set(DSL.field(DSL.name(IS_CURRENT_TASK_RUNNING), 
SQLDataType.INTEGER), 0)
           .execute();
       LOG.info("Updated {} rows with default value for new columns", 
updatedRowCount);
 
-      // Now we will set the column as not-null to enforce constraints
-      setColumnAsNonNullable(dslContext, "last_task_run_status");
-      setColumnAsNonNullable(dslContext, "is_current_task_running");
-    } catch (SQLException | DataAccessException ex) {
-      LOG.error("Error while upgrading RECON_TASK_STATUS table.", ex);
+      setColumnAsNonNullableIfNeeded(conn, dslContext, LAST_TASK_RUN_STATUS);
+      setColumnAsNonNullableIfNeeded(conn, dslContext, 
IS_CURRENT_TASK_RUNNING);
     }
   }
 }
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 3994223447f..e0336664b52 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
@@ -17,11 +17,97 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
+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 com.google.inject.Injector;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
 import javax.sql.DataSource;
+import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
+import org.apache.hadoop.ozone.recon.tasks.NSSummaryTask;
+import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
+import org.apache.hadoop.ozone.recon.tasks.ReconTaskReInitializationEvent;
 import org.apache.hadoop.ozone.upgrade.UpgradeAction;
+import org.apache.ozone.recon.schema.ContainerSchemaDefinition;
+import org.jooq.DSLContext;
+import org.jooq.exception.DataAccessException;
+import org.jooq.impl.DSL;
+import org.slf4j.Logger;
 
 /**
  * Recon upgrade action executed during finalization of a {@link ReconVersion}.
  */
 public interface ReconUpgradeAction extends UpgradeAction<DataSource> {
+  /**
+   * Execute the upgrade action during finalization.
+   */
+  @Override
+  void execute(DataSource source) throws Exception;
+
+  /**
+   * Helper method used by upgrade actions that need to add new unhealthy 
container states to the unhealthy containers
+   * table.
+   */
+  static void updateUnhealthyContainerStatesConstraint(DataSource dataSource, 
Logger log) throws SQLException {
+    try (Connection conn = dataSource.getConnection()) {
+      if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) {
+        return;
+      }
+      DSLContext dslContext = DSL.using(conn);
+      final String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1";
+
+      try {
+        dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME)
+            .dropConstraint(constraintName)
+            .execute();
+        log.debug("Dropped the existing constraint: {}", constraintName);
+      } catch (DataAccessException e) {
+        log.debug("Constraint {} was not present: {}", constraintName, 
e.getMessage());
+      }
+
+      String[] enumStates = Arrays
+          .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values())
+          .map(Enum::name)
+          .toArray(String[]::new);
+
+      dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME)
+          .add(DSL.constraint(constraintName)
+              .check(field(DSL.name("container_state")).in(enumStates)))
+          .execute();
+
+      log.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table 
for enum state values: {}",
+          Arrays.toString(enumStates));
+    }
+  }
+
+  /**
+   * Idempotently queues an NSSummary rebuild, skipping if a rebuild is 
already in progress.
+   * Logs and returns without throwing if the queue attempt does not succeed.
+   */
+  static void queueNSSummaryRebuildIfNeeded(Logger log) {
+    Injector injector = ReconGuiceServletContextListener.getGlobalInjector();
+    if (injector == null) {
+      throw new IllegalStateException(
+          "Guice injector not initialized. NSSummary rebuild cannot proceed 
during upgrade.");
+    }
+
+    ReconTaskController reconTaskController = 
injector.getInstance(ReconTaskController.class);
+
+
+    log.info("Triggering asynchronous NSSummary tree rebuild.");
+    if (NSSummaryTask.getRebuildState() == NSSummaryTask.RebuildState.RUNNING) 
{
+      log.info("NSSummary rebuild already in progress, skipping duplicate 
upgrade queue.");
+      return;
+    }
+    ReconTaskController.ReInitializationResult result = 
reconTaskController.queueReInitializationEvent(
+        ReconTaskReInitializationEvent.ReInitializationReason.MANUAL_TRIGGER);
+    if (result != ReconTaskController.ReInitializationResult.SUCCESS) {
+      log.error(
+          "Failed to queue reinitialization event for NSSummary rebuild 
(result: {}). "
+              + "Will be retried as part of syncDataFromOM scheduler task.", 
result);
+    }
+  }
 }
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 b12e2e93eea..d1e5889d599 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
@@ -17,11 +17,7 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
-import com.google.inject.Injector;
 import javax.sql.DataSource;
-import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
-import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
-import org.apache.hadoop.ozone.recon.tasks.ReconTaskReInitializationEvent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,25 +32,7 @@ public class ReplicatedSizeOfFilesUpgradeAction implements 
ReconUpgradeAction {
   private static final Logger LOG = 
LoggerFactory.getLogger(ReplicatedSizeOfFilesUpgradeAction.class);
 
   @Override
-  public void execute(DataSource dataSource) {
-    try {
-      Injector injector = ReconGuiceServletContextListener.getGlobalInjector();
-      if (injector == null) {
-        throw new IllegalStateException("Guice injector is not initialized. 
Cannot perform NSSummary rebuild.");
-      }
-      ReconTaskController reconTaskController = 
injector.getInstance(ReconTaskController.class);
-      LOG.info("Starting full rebuild of NSSummary for 
REPLICATED_SIZE_OF_FILES upgrade...");
-      ReconTaskController.ReInitializationResult result = 
reconTaskController.queueReInitializationEvent(
-          
ReconTaskReInitializationEvent.ReInitializationReason.MANUAL_TRIGGER);
-      if (result != ReconTaskController.ReInitializationResult.SUCCESS) {
-        throw new RuntimeException(
-            "Failed to queue reinitialization event (result: " + result + "). 
" +
-                "NSSummary rebuild required for REPLICATED_SIZE_OF_FILES 
upgrade.");
-      }
-    } catch (Exception e) {
-      LOG.error("Error during NSSummary rebuild for REPLICATED_SIZE_OF_FILES 
upgrade.", e);
-      throw new RuntimeException("Failed to rebuild NSSummary during upgrade", 
e);
-    }
-    LOG.info("Completed full rebuild of NSSummary for REPLICATED_SIZE_OF_FILES 
upgrade.");
+  public void execute(DataSource dataSource) throws Exception {
+    ReconUpgradeAction.queueNSSummaryRebuildIfNeeded(LOG);
   }
 }
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 a5b313192cc..3753ad244ab 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
@@ -18,17 +18,8 @@
 package org.apache.hadoop.ozone.recon.upgrade;
 
 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 java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Arrays;
 import javax.sql.DataSource;
-import org.apache.ozone.recon.schema.ContainerSchemaDefinition;
-import org.jooq.DSLContext;
-import org.jooq.impl.DSL;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,51 +30,9 @@
 @UpgradeActionRecon(feature = UNHEALTHY_CONTAINER_REPLICA_MISMATCH)
 public class UnhealthyContainerReplicaMismatchAction implements 
ReconUpgradeAction {
   private static final Logger LOG = 
LoggerFactory.getLogger(UnhealthyContainerReplicaMismatchAction.class);
-  private DSLContext dslContext;
 
   @Override
   public void execute(DataSource source) throws Exception {
-    try (Connection conn = source.getConnection()) {
-      if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) {
-        return;
-      }
-      dslContext = DSL.using(conn);
-      // Drop the existing constraint
-      dropConstraint();
-      // Add the updated constraint with all enum states
-      addUpdatedConstraint();
-    } catch (SQLException e) {
-      throw new SQLException("Failed to execute 
UnhealthyContainerReplicaMismatchAction", e);
-    }
-  }
-
-  /**
-   * Drops the existing constraint from the UNHEALTHY_CONTAINERS table.
-   */
-  private void dropConstraint() {
-    String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1";
-    dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME)
-        .dropConstraint(constraintName)
-        .execute();
-    LOG.debug("Dropped the existing constraint: {}", constraintName);
-  }
-
-  /**
-   * Adds the updated constraint directly within this class.
-   */
-  private void addUpdatedConstraint() {
-    String[] enumStates = Arrays
-        .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values())
-        .map(Enum::name)
-        .toArray(String[]::new);
-
-    
dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME)
-        
.add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + 
"ck1")
-            .check(field(DSL.name("container_state"))
-                .in(enumStates)))
-        .execute();
-
-    LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table 
for enum state values: {}",
-        Arrays.toString(enumStates));
+    ReconUpgradeAction.updateUnhealthyContainerStatesConstraint(source, LOG);
   }
 }
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 ebfe459a6fc..c64a6e33ca1 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,10 +19,9 @@
 
 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.apache.ozone.recon.schema.SqlDbUtils.indexExists;
 
 import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
 import java.sql.SQLException;
 import javax.sql.DataSource;
 import org.jooq.DSLContext;
@@ -48,7 +47,7 @@ public void execute(DataSource source) throws Exception {
         return;
       }
 
-      if (indexExists(conn, INDEX_NAME)) {
+      if (indexExists(conn, UNHEALTHY_CONTAINERS_TABLE_NAME, INDEX_NAME)) {
         LOG.info("Index {} already exists on {}", INDEX_NAME,
             UNHEALTHY_CONTAINERS_TABLE_NAME);
         return;
@@ -67,19 +66,4 @@ public void execute(DataSource source) throws Exception {
           + " on " + UNHEALTHY_CONTAINERS_TABLE_NAME, e);
     }
   }
-
-  private boolean indexExists(Connection conn, String indexName)
-      throws SQLException {
-    DatabaseMetaData metaData = conn.getMetaData();
-    try (ResultSet rs = metaData.getIndexInfo(
-        null, null, UNHEALTHY_CONTAINERS_TABLE_NAME, false, false)) {
-      while (rs.next()) {
-        String existing = rs.getString("INDEX_NAME");
-        if (existing != null && existing.equalsIgnoreCase(indexName)) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
 }
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java
index 7c4c8e55122..5b90e50a4ce 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java
@@ -18,10 +18,13 @@
 package org.apache.hadoop.ozone.recon.upgrade;
 
 import static 
org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME;
+import static org.apache.ozone.recon.schema.SqlDbUtils.constraintExists;
 import static org.jooq.impl.DSL.field;
 import static org.jooq.impl.DSL.name;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -60,9 +63,6 @@ public void setUp() throws SQLException {
     DataSource dataSource = getInjector().getInstance(DataSource.class);
     when(mockScmFacade.getDataSource()).thenReturn(dataSource);
 
-    // Set the DataSource and DSLContext directly
-    upgradeAction.setDslContext(dslContext);
-
     // Check if the table already exists
     try (Connection conn = dataSource.getConnection()) {
       DatabaseMetaData dbMetaData = conn.getMetaData();
@@ -81,6 +81,17 @@ public void setUp() throws SQLException {
     }
   }
 
+  @Test
+  public void testExecuteIsIdempotent() throws SQLException {
+    DataSource dataSource = getInjector().getInstance(DataSource.class);
+    upgradeAction.execute(dataSource);
+    try (Connection conn = dataSource.getConnection()) {
+      assertTrue(constraintExists(conn, UNHEALTHY_CONTAINERS_TABLE_NAME,
+          UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1"));
+    }
+    assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+  }
+
   @Test
   public void testUpgradeAppliesConstraintModificationForAllStates() throws 
SQLException {
     // Run the upgrade action
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestNSSummaryAggregatedTotalsUpgrade.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestNSSummaryAggregatedTotalsUpgrade.java
index 80b6d7cc576..99ac2a1a76d 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestNSSummaryAggregatedTotalsUpgrade.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestNSSummaryAggregatedTotalsUpgrade.java
@@ -19,13 +19,16 @@
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.inject.Injector;
 import javax.sql.DataSource;
 import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
+import org.apache.hadoop.ozone.recon.tasks.NSSummaryTask;
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskReInitializationEvent;
 import org.junit.jupiter.api.BeforeEach;
@@ -136,6 +139,27 @@ public void testExecuteWithNullInjector() {
     }
   }
 
+  @Test
+  public void testExecuteIsIdempotentWhenRebuildRunning() {
+    try (MockedStatic<ReconGuiceServletContextListener> mockedListener =
+             Mockito.mockStatic(ReconGuiceServletContextListener.class);
+         MockedStatic<NSSummaryTask> mockedNSSummary = 
Mockito.mockStatic(NSSummaryTask.class)) {
+
+      mockedListener.when(ReconGuiceServletContextListener::getGlobalInjector)
+          .thenReturn(mockInjector);
+
+      when(mockInjector.getInstance(ReconTaskController.class))
+          .thenReturn(mockReconTaskController);
+
+      mockedNSSummary.when(NSSummaryTask::getRebuildState)
+          .thenReturn(NSSummaryTask.RebuildState.RUNNING);
+
+      assertDoesNotThrow(() -> upgradeAction.execute(mockDataSource));
+
+      verify(mockReconTaskController, 
never()).queueReInitializationEvent(any());
+    }
+  }
+
   @Test
   public void testExecuteWithInjectorException() throws Exception {
     try (MockedStatic<ReconGuiceServletContextListener> mockedListener = 
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java
new file mode 100644
index 00000000000..2d98bba6ab1
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.recon.upgrade;
+
+import static 
org.apache.ozone.recon.schema.ReconTaskSchemaDefinition.RECON_TASK_STATUS_TABLE_NAME;
+import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK;
+import static org.apache.ozone.recon.schema.SqlDbUtils.columnExists;
+import static org.apache.ozone.recon.schema.SqlDbUtils.isColumnNullable;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
+import org.jooq.DSLContext;
+import org.jooq.impl.DSL;
+import org.jooq.impl.SQLDataType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for ReconTaskStatusTableUpgradeAction.
+ */
+public class TestReconTaskStatusTableUpgradeAction extends 
AbstractReconSqlDBTest {
+
+  private static final String LAST_TASK_RUN_STATUS = "last_task_run_status";
+  private static final String IS_CURRENT_TASK_RUNNING = 
"is_current_task_running";
+
+  private DSLContext dslContext;
+  private DataSource dataSource;
+  private ReconTaskStatusTableUpgradeAction upgradeAction;
+
+  @BeforeEach
+  public void setUp() {
+    dslContext = getDslContext();
+    dataSource = getInjector().getInstance(DataSource.class);
+    upgradeAction = new ReconTaskStatusTableUpgradeAction();
+  }
+
+  @Test
+  public void testExecuteAddsColumnsToLegacyTable() throws Exception {
+    createLegacyTaskStatusTable();
+    try (Connection conn = dataSource.getConnection()) {
+      assertFalse(columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, 
LAST_TASK_RUN_STATUS));
+      assertFalse(columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, 
IS_CURRENT_TASK_RUNNING));
+    }
+
+    upgradeAction.execute(dataSource);
+
+    try (Connection conn = dataSource.getConnection()) {
+      assertTrue(columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, 
LAST_TASK_RUN_STATUS));
+      assertTrue(columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, 
IS_CURRENT_TASK_RUNNING));
+      assertFalse(isColumnNullable(conn, RECON_TASK_STATUS_TABLE_NAME, 
LAST_TASK_RUN_STATUS));
+      assertFalse(isColumnNullable(conn, RECON_TASK_STATUS_TABLE_NAME, 
IS_CURRENT_TASK_RUNNING));
+    }
+  }
+
+  @Test
+  public void testExecuteIsIdempotentOnLegacyTable() throws Exception {
+    createLegacyTaskStatusTable();
+    upgradeAction.execute(dataSource);
+    assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+
+    try (Connection conn = dataSource.getConnection()) {
+      assertTrue(columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, 
LAST_TASK_RUN_STATUS));
+      assertTrue(columnExists(conn, RECON_TASK_STATUS_TABLE_NAME, 
IS_CURRENT_TASK_RUNNING));
+      assertFalse(isColumnNullable(conn, RECON_TASK_STATUS_TABLE_NAME, 
LAST_TASK_RUN_STATUS));
+      assertFalse(isColumnNullable(conn, RECON_TASK_STATUS_TABLE_NAME, 
IS_CURRENT_TASK_RUNNING));
+    }
+  }
+
+  @Test
+  public void testExecuteIsIdempotentOnCurrentSchema() {
+    assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+    assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+  }
+
+  @Test
+  public void testNoOpWhenTableMissing() throws SQLException {
+    dropTaskStatusTableIfPresent();
+    assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+  }
+
+  private void createLegacyTaskStatusTable() throws SQLException {
+    dropTaskStatusTableIfPresent();
+    try (Connection conn = dataSource.getConnection()) {
+      DSLContext legacyDsl = DSL.using(conn);
+      // Derby rejects explicit "null" in CREATE TABLE for BIGINT columns; add 
them via ALTER instead.
+      legacyDsl.createTable(RECON_TASK_STATUS_TABLE_NAME)
+          .column("task_name", SQLDataType.VARCHAR(766).nullable(false))
+          .constraint(DSL.constraint("pk_task_name")
+              .primaryKey("task_name"))
+          .execute();
+      legacyDsl.alterTable(RECON_TASK_STATUS_TABLE_NAME)
+          .addColumn("last_updated_timestamp", SQLDataType.BIGINT)
+          .execute();
+      legacyDsl.alterTable(RECON_TASK_STATUS_TABLE_NAME)
+          .addColumn("last_updated_seq_number", SQLDataType.BIGINT)
+          .execute();
+    }
+  }
+
+  private void dropTaskStatusTableIfPresent() throws SQLException {
+    try (Connection conn = dataSource.getConnection()) {
+      if (TABLE_EXISTS_CHECK.test(conn, RECON_TASK_STATUS_TABLE_NAME)) {
+        dslContext.dropTable(RECON_TASK_STATUS_TABLE_NAME).execute();
+      }
+    }
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReplicatedSizeOfFilesUpgradeAction.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReplicatedSizeOfFilesUpgradeAction.java
index 2597b8877c6..54227a40dd8 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReplicatedSizeOfFilesUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReplicatedSizeOfFilesUpgradeAction.java
@@ -17,11 +17,10 @@
 
 package org.apache.hadoop.ozone.recon.upgrade;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doThrow;
 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;
@@ -29,6 +28,7 @@
 import com.google.inject.Injector;
 import javax.sql.DataSource;
 import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
+import org.apache.hadoop.ozone.recon.tasks.NSSummaryTask;
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskReInitializationEvent;
 import org.junit.jupiter.api.BeforeEach;
@@ -58,7 +58,7 @@ public void setUp() {
   }
 
   @Test
-  public void testExecuteSuccessfullyRebuildsNSSummary() {
+  public void testExecuteSuccessfullyRebuildsNSSummary() throws Exception {
     try (MockedStatic<ReconGuiceServletContextListener> mockStaticContext =
              mockStatic(ReconGuiceServletContextListener.class)) {
       
mockStaticContext.when(ReconGuiceServletContextListener::getGlobalInjector).thenReturn(mockInjector);
@@ -74,18 +74,34 @@ public void testExecuteSuccessfullyRebuildsNSSummary() {
   }
 
   @Test
-  public void testExecuteThrowsRuntimeExceptionOnRebuildFailure() {
+  public void testExecuteIsIdempotentWhenRebuildRunning() {
+    try (MockedStatic<ReconGuiceServletContextListener> mockStaticContext =
+             mockStatic(ReconGuiceServletContextListener.class);
+         MockedStatic<NSSummaryTask> mockedNSSummary = 
mockStatic(NSSummaryTask.class)) {
+      
mockStaticContext.when(ReconGuiceServletContextListener::getGlobalInjector).thenReturn(mockInjector);
+      
when(mockInjector.getInstance(ReconTaskController.class)).thenReturn(mockReconTaskController);
+      mockedNSSummary.when(NSSummaryTask::getRebuildState)
+          .thenReturn(NSSummaryTask.RebuildState.RUNNING);
+
+      assertDoesNotThrow(() -> upgradeAction.execute(mockDataSource));
+
+      verify(mockReconTaskController, 
never()).queueReInitializationEvent(any());
+    }
+  }
+
+  @Test
+  public void testExecuteDoesNotThrowOnRebuildFailure() {
     try (MockedStatic<ReconGuiceServletContextListener> mockStaticContext =
              mockStatic(ReconGuiceServletContextListener.class)) {
       
mockStaticContext.when(ReconGuiceServletContextListener::getGlobalInjector).thenReturn(mockInjector);
       
when(mockInjector.getInstance(ReconTaskController.class)).thenReturn(mockReconTaskController);
+      when(mockReconTaskController.queueReInitializationEvent(
+          any(ReconTaskReInitializationEvent.ReInitializationReason.class)))
+          .thenReturn(ReconTaskController.ReInitializationResult.RETRY_LATER);
 
-      // Simulate a failure during the rebuild process
-      doThrow(new RuntimeException("Simulated rebuild 
error")).when(mockReconTaskController)
-          
.queueReInitializationEvent(any(ReconTaskReInitializationEvent.ReInitializationReason.class));
+      assertDoesNotThrow(() -> upgradeAction.execute(mockDataSource));
 
-      RuntimeException thrown = assertThrows(RuntimeException.class, () -> 
upgradeAction.execute(mockDataSource));
-      assertEquals("Failed to rebuild NSSummary during upgrade", 
thrown.getMessage());
+      verify(mockReconTaskController, 
times(1)).queueReInitializationEvent(any());
     }
   }
 }
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java
similarity index 64%
copy from 
hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
copy to 
hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java
index b4cde1bb673..3b58d884609 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java
@@ -19,14 +19,12 @@
 
 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.apache.ozone.recon.schema.SqlDbUtils.constraintExists;
 import static org.jooq.impl.DSL.name;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
 import java.sql.SQLException;
 import javax.sql.DataSource;
 import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
@@ -37,42 +35,31 @@
 import org.junit.jupiter.api.Test;
 
 /**
- * Tests for UnhealthyContainersStateContainerIdIndexUpgradeAction.
+ * Tests for UnhealthyContainerReplicaMismatchAction.
  */
-public class TestUnhealthyContainersStateContainerIdIndexUpgradeAction
-    extends AbstractReconSqlDBTest {
+public class TestUnhealthyContainerReplicaMismatchAction extends 
AbstractReconSqlDBTest {
 
-  private static final String INDEX_NAME = "idx_state_container_id";
+  private static final String CONSTRAINT_NAME = 
UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1";
 
   private DSLContext dslContext;
   private DataSource dataSource;
-  private UnhealthyContainersStateContainerIdIndexUpgradeAction upgradeAction;
+  private UnhealthyContainerReplicaMismatchAction upgradeAction;
 
   @BeforeEach
-  public void setUp() {
+  public void setUp() throws SQLException {
     dslContext = getDslContext();
     dataSource = getInjector().getInstance(DataSource.class);
-    upgradeAction = new 
UnhealthyContainersStateContainerIdIndexUpgradeAction();
+    upgradeAction = new UnhealthyContainerReplicaMismatchAction();
+    createTableWithoutCheckConstraint();
   }
 
   @Test
-  public void testCreatesIndexWhenMissing() throws Exception {
-    createTableWithoutIndex();
-    assertFalse(indexExists(INDEX_NAME));
-
+  public void testExecuteIsIdempotent() throws Exception {
     upgradeAction.execute(dataSource);
-
-    assertTrue(indexExists(INDEX_NAME));
-  }
-
-  @Test
-  public void testExecuteIsIdempotentWhenIndexAlreadyExists() throws Exception 
{
-    createTableWithoutIndex();
-    upgradeAction.execute(dataSource);
-    assertTrue(indexExists(INDEX_NAME));
-
+    try (Connection conn = dataSource.getConnection()) {
+      assertTrue(constraintExists(conn, UNHEALTHY_CONTAINERS_TABLE_NAME, 
CONSTRAINT_NAME));
+    }
     assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
-    assertTrue(indexExists(INDEX_NAME));
   }
 
   @Test
@@ -81,7 +68,7 @@ public void testNoOpWhenTableMissing() throws SQLException {
     assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
   }
 
-  private void createTableWithoutIndex() throws SQLException {
+  private void createTableWithoutCheckConstraint() throws SQLException {
     dropTableIfPresent();
     dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME)
         .column("container_id", SQLDataType.BIGINT.nullable(false))
@@ -98,20 +85,4 @@ private void dropTableIfPresent() throws SQLException {
       }
     }
   }
-
-  private boolean indexExists(String indexName) throws SQLException {
-    try (Connection conn = dataSource.getConnection()) {
-      DatabaseMetaData metaData = conn.getMetaData();
-      try (ResultSet rs = metaData.getIndexInfo(
-          null, null, UNHEALTHY_CONTAINERS_TABLE_NAME, false, false)) {
-        while (rs.next()) {
-          String existing = rs.getString("INDEX_NAME");
-          if (existing != null && existing.equalsIgnoreCase(indexName)) {
-            return true;
-          }
-        }
-      }
-    }
-    return false;
-  }
 }
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
index b4cde1bb673..98aab4aea92 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
@@ -19,14 +19,13 @@
 
 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.apache.ozone.recon.schema.SqlDbUtils.indexExists;
 import static org.jooq.impl.DSL.name;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
 import java.sql.SQLException;
 import javax.sql.DataSource;
 import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
@@ -58,21 +57,21 @@ public void setUp() {
   @Test
   public void testCreatesIndexWhenMissing() throws Exception {
     createTableWithoutIndex();
-    assertFalse(indexExists(INDEX_NAME));
+    assertFalse(tableHasIndex(INDEX_NAME));
 
     upgradeAction.execute(dataSource);
 
-    assertTrue(indexExists(INDEX_NAME));
+    assertTrue(tableHasIndex(INDEX_NAME));
   }
 
   @Test
   public void testExecuteIsIdempotentWhenIndexAlreadyExists() throws Exception 
{
     createTableWithoutIndex();
     upgradeAction.execute(dataSource);
-    assertTrue(indexExists(INDEX_NAME));
+    assertTrue(tableHasIndex(INDEX_NAME));
 
     assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
-    assertTrue(indexExists(INDEX_NAME));
+    assertTrue(tableHasIndex(INDEX_NAME));
   }
 
   @Test
@@ -99,19 +98,9 @@ private void dropTableIfPresent() throws SQLException {
     }
   }
 
-  private boolean indexExists(String indexName) throws SQLException {
+  private boolean tableHasIndex(String indexName) throws SQLException {
     try (Connection conn = dataSource.getConnection()) {
-      DatabaseMetaData metaData = conn.getMetaData();
-      try (ResultSet rs = metaData.getIndexInfo(
-          null, null, UNHEALTHY_CONTAINERS_TABLE_NAME, false, false)) {
-        while (rs.next()) {
-          String existing = rs.getString("INDEX_NAME");
-          if (existing != null && existing.equalsIgnoreCase(indexName)) {
-            return true;
-          }
-        }
-      }
+      return indexExists(conn, UNHEALTHY_CONTAINERS_TABLE_NAME, indexName);
     }
-    return false;
   }
 }


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

Reply via email to