records =
+ new ArrayList<>(BATCH_INSERT_CHUNK_SIZE);
+
+ for (int from = 0; from < recs.size(); from += BATCH_INSERT_CHUNK_SIZE) {
+ int to = Math.min(from + BATCH_INSERT_CHUNK_SIZE, recs.size());
+ records.clear();
+ for (int i = from; i < to; i++) {
+ records.add(toJooqRecord(dslContext, recs.get(i)));
+ }
+ dslContext.batchInsert(records).execute();
+ }
+ }
+
+ private UnhealthyContainersRecord toJooqRecord(DSLContext txContext,
+ UnhealthyContainerRecord rec) {
+ UnhealthyContainersRecord record = txContext.newRecord(UNHEALTHY_CONTAINERS);
+ record.setContainerId(rec.getContainerId());
+ record.setContainerState(rec.getContainerState());
+ record.setInStateSince(rec.getInStateSince());
+ record.setExpectedReplicaCount(rec.getExpectedReplicaCount());
+ record.setActualReplicaCount(rec.getActualReplicaCount());
+ record.setReplicaDelta(rec.getReplicaDelta());
+ record.setReason(rec.getReason());
+ return record;
+ }
+
+ /**
+ * Batch delete all health states for multiple containers.
+ * This deletes all states generated from SCM/Recon health scans:
+ * MISSING, EMPTY_MISSING, UNDER_REPLICATED, OVER_REPLICATED,
+ * MIS_REPLICATED, NEGATIVE_SIZE and REPLICA_MISMATCH for all containers
+ * in the list.
+ *
+ * REPLICA_MISMATCH is included here because it is re-evaluated on every
+ * scan cycle (just like the SCM-sourced states); omitting it would leave
+ * stale REPLICA_MISMATCH records in the table after a mismatch is resolved.
+ *
+ *
Derby bytecode limit: Derby translates each SQL statement into
+ * a Java class whose methods must each stay under the JVM 64 KB bytecode
+ * limit. A single {@code IN} predicate with more than ~2,000 values (when
+ * combined with the 7-state container_state filter) overflows this limit
+ * and causes {@code ERROR XBCM4}. This method automatically partitions
+ * {@code containerIds} into chunks of at most {@value #MAX_DELETE_CHUNK_SIZE}
+ * IDs so callers never need to worry about the limit, regardless of how
+ * many containers a scan cycle processes.
+ *
+ * @param containerIds List of container IDs to delete states for
+ */
+ public void batchDeleteSCMStatesForContainers(List containerIds) {
+ if (containerIds == null || containerIds.isEmpty()) {
+ return;
+ }
+
+ DSLContext dslContext = containerSchemaDefinition.getDSLContext();
+ int totalDeleted = deleteScmStatesForContainers(dslContext, containerIds);
+ LOG.debug("Batch deleted {} health state records for {} containers",
+ totalDeleted, containerIds.size());
+ }
+
+ /**
+ * Atomically replaces unhealthy rows for a given set of containers.
+ * Delete and insert happen in the same DB transaction.
+ */
+ public void replaceUnhealthyContainerRecordsAtomically(
+ List containerIdsToDelete,
+ List recordsToInsert) {
+ if ((containerIdsToDelete == null || containerIdsToDelete.isEmpty())
+ && (recordsToInsert == null || recordsToInsert.isEmpty())) {
+ return;
+ }
+
+ DSLContext dslContext = containerSchemaDefinition.getDSLContext();
+ dslContext.transaction(configuration -> {
+ DSLContext txContext = configuration.dsl();
+ if (containerIdsToDelete != null && !containerIdsToDelete.isEmpty()) {
+ deleteScmStatesForContainers(txContext, containerIdsToDelete);
+ }
+ if (recordsToInsert != null && !recordsToInsert.isEmpty()) {
+ batchInsertInChunks(txContext, recordsToInsert);
+ }
+ });
+ }
+
+ private int deleteScmStatesForContainers(DSLContext dslContext,
+ List containerIds) {
+ int totalDeleted = 0;
+
+ for (int from = 0; from < containerIds.size(); from += MAX_DELETE_CHUNK_SIZE) {
+ int to = Math.min(from + MAX_DELETE_CHUNK_SIZE, containerIds.size());
+ List chunk = containerIds.subList(from, to);
+
+ int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS)
+ .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.in(chunk))
+ .and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.in(
+ UnHealthyContainerStates.MISSING.toString(),
+ UnHealthyContainerStates.EMPTY_MISSING.toString(),
+ UnHealthyContainerStates.UNDER_REPLICATED.toString(),
+ UnHealthyContainerStates.OVER_REPLICATED.toString(),
+ UnHealthyContainerStates.MIS_REPLICATED.toString(),
+ UnHealthyContainerStates.NEGATIVE_SIZE.toString(),
+ UnHealthyContainerStates.REPLICA_MISMATCH.toString()))
+ .execute();
+ totalDeleted += deleted;
+ }
+ return totalDeleted;
+ }
+
+ /**
+ * Returns previous in-state-since timestamps for tracked unhealthy states.
+ * The key is a stable containerId + state tuple.
+ */
+ public Map getExistingInStateSinceByContainerIds(
+ List containerIds) {
+ if (containerIds == null || containerIds.isEmpty()) {
+ return new HashMap<>();
+ }
+
+ DSLContext dslContext = containerSchemaDefinition.getDSLContext();
+ Map existing = new HashMap<>();
+ try {
+ dslContext.select(
+ UNHEALTHY_CONTAINERS.CONTAINER_ID,
+ UNHEALTHY_CONTAINERS.CONTAINER_STATE,
+ UNHEALTHY_CONTAINERS.IN_STATE_SINCE)
+ .from(UNHEALTHY_CONTAINERS)
+ .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.in(containerIds))
+ .and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.in(
+ UnHealthyContainerStates.MISSING.toString(),
+ UnHealthyContainerStates.EMPTY_MISSING.toString(),
+ UnHealthyContainerStates.UNDER_REPLICATED.toString(),
+ UnHealthyContainerStates.OVER_REPLICATED.toString(),
+ UnHealthyContainerStates.MIS_REPLICATED.toString(),
+ UnHealthyContainerStates.NEGATIVE_SIZE.toString(),
+ UnHealthyContainerStates.REPLICA_MISMATCH.toString()))
+ .forEach(record -> existing.put(
+ new ContainerStateKey(record.get(UNHEALTHY_CONTAINERS.CONTAINER_ID),
+ record.get(UNHEALTHY_CONTAINERS.CONTAINER_STATE)),
+ record.get(UNHEALTHY_CONTAINERS.IN_STATE_SINCE)));
+ } catch (Exception e) {
+ LOG.warn("Failed to load existing inStateSince records. Falling back to current scan time.", e);
+ }
+ return existing;
+ }
+
+ /**
+ * Preserve existing inStateSince values for records that remain in the
+ * same unhealthy state across scan cycles.
+ */
+ public List applyExistingInStateSince(
+ List records,
+ List containerIds) {
+ if (records == null || records.isEmpty()
+ || containerIds == null || containerIds.isEmpty()) {
+ return records;
+ }
+
+ Map existingByContainerAndState =
+ getExistingInStateSinceByContainerIds(containerIds);
+ if (existingByContainerAndState.isEmpty()) {
+ return records;
+ }
+
+ List withPreservedInStateSince =
+ new ArrayList<>(records.size());
+ for (UnhealthyContainerRecord record : records) {
+ Long existingInStateSince = existingByContainerAndState.get(
+ new ContainerStateKey(record.getContainerId(),
+ record.getContainerState()));
+ if (existingInStateSince == null) {
+ withPreservedInStateSince.add(record);
+ } else {
+ withPreservedInStateSince.add(new UnhealthyContainerRecord(
+ record.getContainerId(),
+ record.getContainerState(),
+ existingInStateSince,
+ record.getExpectedReplicaCount(),
+ record.getActualReplicaCount(),
+ record.getReplicaDelta(),
+ record.getReason()));
+ }
+ }
+ return withPreservedInStateSince;
+ }
+
+ /**
+ * Get summary of unhealthy containers grouped by state from UNHEALTHY_CONTAINERS table.
+ */
+ public List getUnhealthyContainersSummary() {
+ DSLContext dslContext = containerSchemaDefinition.getDSLContext();
+ List result = new ArrayList<>();
+
+ try {
+ return dslContext
+ .select(UNHEALTHY_CONTAINERS.CONTAINER_STATE.as("containerState"),
+ count().as("cnt"))
+ .from(UNHEALTHY_CONTAINERS)
+ .groupBy(UNHEALTHY_CONTAINERS.CONTAINER_STATE)
+ .fetchInto(UnhealthyContainersSummary.class);
+ } catch (Exception e) {
+ LOG.error("Failed to get summary from UNHEALTHY_CONTAINERS table", e);
+ return result;
+ }
+ }
+
+ /**
+ * Get unhealthy containers from UNHEALTHY_CONTAINERS table.
*/
- public List getUnhealthyContainers(
- UnHealthyContainerStates state, Long minContainerId, Optional maxContainerId, int limit) {
+ public List getUnhealthyContainers(
+ UnHealthyContainerStates state, long minContainerId, long maxContainerId, int limit) {
DSLContext dslContext = containerSchemaDefinition.getDSLContext();
+
SelectQuery query = dslContext.selectQuery();
query.addFrom(UNHEALTHY_CONTAINERS);
+
Condition containerCondition;
OrderField[] orderField;
- if (maxContainerId.isPresent() && maxContainerId.get() > 0) {
- containerCondition = UNHEALTHY_CONTAINERS.CONTAINER_ID.lessThan(maxContainerId.get());
- orderField = new OrderField[]{UNHEALTHY_CONTAINERS.CONTAINER_ID.desc(),
- UNHEALTHY_CONTAINERS.CONTAINER_STATE.asc()};
+
+ if (maxContainerId > 0) {
+ containerCondition = UNHEALTHY_CONTAINERS.CONTAINER_ID.lessThan(maxContainerId);
+ orderField = new OrderField[]{
+ UNHEALTHY_CONTAINERS.CONTAINER_ID.desc(),
+ UNHEALTHY_CONTAINERS.CONTAINER_STATE.asc()
+ };
} else {
containerCondition = UNHEALTHY_CONTAINERS.CONTAINER_ID.greaterThan(minContainerId);
- orderField = new OrderField[]{UNHEALTHY_CONTAINERS.CONTAINER_ID.asc(),
- UNHEALTHY_CONTAINERS.CONTAINER_STATE.asc()};
+ orderField = new OrderField[]{
+ UNHEALTHY_CONTAINERS.CONTAINER_ID.asc(),
+ UNHEALTHY_CONTAINERS.CONTAINER_STATE.asc()
+ };
}
+
if (state != null) {
- if (state.equals(ALL_REPLICAS_BAD)) {
- query.addConditions(containerCondition.and(UNHEALTHY_CONTAINERS.CONTAINER_STATE
- .eq(UNDER_REPLICATED.toString())));
- query.addConditions(UNHEALTHY_CONTAINERS.ACTUAL_REPLICA_COUNT.eq(0));
- } else {
- query.addConditions(containerCondition.and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString())));
- }
+ query.addConditions(containerCondition.and(
+ UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString())));
} else {
- // CRITICAL FIX: Apply pagination condition even when state is null
- // This ensures proper pagination for the "get all unhealthy containers" use case
query.addConditions(containerCondition);
}
query.addOrderBy(orderField);
query.addLimit(limit);
- return query.fetchInto(UnhealthyContainers.class).stream()
- .sorted(Comparator.comparingLong(UnhealthyContainers::getContainerId))
- .collect(Collectors.toList());
+ // Pre-buffer `limit` rows per JDBC round-trip instead of Derby's default of 1 row.
+ query.fetchSize(limit);
+
+ try {
+ Stream stream =
+ query.fetchInto(UnhealthyContainersRecord.class).stream();
+
+ if (maxContainerId > 0) {
+ // Reverse-pagination path: SQL orders DESC (to get the last `limit` rows before
+ // maxContainerId); re-sort to ASC so callers always see ascending container IDs.
+ stream = stream.sorted(Comparator.comparingLong(UnhealthyContainersRecord::getContainerId));
+ }
+ // Forward-pagination path: SQL already orders ASC — no Java re-sort needed.
+
+ return stream.map(record -> new UnhealthyContainerRecord(
+ record.getContainerId(),
+ record.getContainerState(),
+ record.getInStateSince(),
+ record.getExpectedReplicaCount(),
+ record.getActualReplicaCount(),
+ record.getReplicaDelta(),
+ record.getReason()))
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ LOG.error("Failed to query UNHEALTHY_CONTAINERS table", e);
+ return new ArrayList<>();
+ }
}
/**
- * Obtain a count of all containers in each state. If there are no unhealthy
- * containers an empty list will be returned. If there are unhealthy
- * containers for a certain state, no entry will be returned for it.
- * @return Count of unhealthy containers in each state
+ * Clear all records from UNHEALTHY_CONTAINERS table (for testing).
*/
- public List getUnhealthyContainersSummary() {
+ @VisibleForTesting
+ public void clearAllUnhealthyContainerRecords() {
DSLContext dslContext = containerSchemaDefinition.getDSLContext();
- return dslContext
- .select(UNHEALTHY_CONTAINERS.CONTAINER_STATE.as("containerState"),
- count().as("cnt"))
- .from(UNHEALTHY_CONTAINERS)
- .groupBy(UNHEALTHY_CONTAINERS.CONTAINER_STATE)
- .fetchInto(UnhealthyContainersSummary.class);
+ try {
+ dslContext.deleteFrom(UNHEALTHY_CONTAINERS).execute();
+ LOG.info("Cleared all UNHEALTHY_CONTAINERS table's unhealthy container records");
+ } catch (Exception e) {
+ LOG.error("Failed to clear UNHEALTHY_CONTAINERS table's unhealthy container records", e);
+ }
}
- public Cursor getAllUnhealthyRecordsCursor() {
- DSLContext dslContext = containerSchemaDefinition.getDSLContext();
- return dslContext
- .selectFrom(UNHEALTHY_CONTAINERS)
- .orderBy(UNHEALTHY_CONTAINERS.CONTAINER_ID.asc())
- .fetchLazy();
+ /**
+ * POJO representing a record in UNHEALTHY_CONTAINERS table.
+ */
+ public static class UnhealthyContainerRecord {
+ private final long containerId;
+ private final String containerState;
+ private final long inStateSince;
+ private final int expectedReplicaCount;
+ private final int actualReplicaCount;
+ private final int replicaDelta;
+ private final String reason;
+
+ public UnhealthyContainerRecord(long containerId, String containerState,
+ long inStateSince, int expectedReplicaCount, int actualReplicaCount,
+ int replicaDelta, String reason) {
+ this.containerId = containerId;
+ this.containerState = containerState;
+ this.inStateSince = inStateSince;
+ this.expectedReplicaCount = expectedReplicaCount;
+ this.actualReplicaCount = actualReplicaCount;
+ this.replicaDelta = replicaDelta;
+ this.reason = reason;
+ }
+
+ public long getContainerId() {
+ return containerId;
+ }
+
+ public String getContainerState() {
+ return containerState;
+ }
+
+ public long getInStateSince() {
+ return inStateSince;
+ }
+
+ public int getExpectedReplicaCount() {
+ return expectedReplicaCount;
+ }
+
+ public int getActualReplicaCount() {
+ return actualReplicaCount;
+ }
+
+ public int getReplicaDelta() {
+ return replicaDelta;
+ }
+
+ public String getReason() {
+ return reason;
+ }
}
- public void insertUnhealthyContainerRecords(List recs) {
- if (LOG.isDebugEnabled()) {
- recs.forEach(rec -> LOG.debug("rec.getContainerId() : {}, rec.getContainerState(): {}",
- rec.getContainerId(), rec.getContainerState()));
+ /**
+ * Key type for (containerId, state).
+ */
+ public static final class ContainerStateKey {
+ private final long containerId;
+ private final String state;
+
+ public ContainerStateKey(long containerId, String state) {
+ this.containerId = containerId;
+ this.state = state;
}
- try (Connection connection = containerSchemaDefinition.getDataSource().getConnection()) {
- connection.setAutoCommit(false); // Turn off auto-commit for transactional control
- try {
- for (UnhealthyContainers rec : recs) {
- try {
- unhealthyContainersDao.insert(rec);
- } catch (DataAccessException dataAccessException) {
- // Log the error and update the existing record if ConstraintViolationException occurs
- unhealthyContainersDao.update(rec);
- LOG.debug("Error while inserting unhealthy container record: {}", rec, dataAccessException);
- }
- }
- connection.commit(); // Commit all inserted/updated records
- } catch (Exception innerException) {
- connection.rollback(); // Rollback transaction if an error occurs inside processing
- LOG.error("Transaction rolled back due to error", innerException);
- throw innerException;
- } finally {
- connection.setAutoCommit(true); // Reset auto-commit before the connection is auto-closed
+ public long getContainerId() {
+ return containerId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
}
- } catch (Exception e) {
- LOG.error("Failed to insert records into {} ", UNHEALTHY_CONTAINERS_TABLE_NAME, e);
- throw new RuntimeException("Recon failed to insert " + recs.size() + " unhealthy container records.", e);
+ if (!(other instanceof ContainerStateKey)) {
+ return false;
+ }
+ ContainerStateKey that = (ContainerStateKey) other;
+ return containerId == that.containerId && state.equals(that.state);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(containerId, state);
}
}
/**
- * Clear all unhealthy container records. This is primarily used for testing
- * to ensure clean state between tests.
+ * POJO representing a summary record for unhealthy containers.
*/
- @VisibleForTesting
- public void clearAllUnhealthyContainerRecords() {
- DSLContext dslContext = containerSchemaDefinition.getDSLContext();
- try {
- dslContext.deleteFrom(UNHEALTHY_CONTAINERS).execute();
- LOG.info("Cleared all unhealthy container records");
- } catch (Exception e) {
- LOG.info("Failed to clear unhealthy container records", e);
+ public static class UnhealthyContainersSummary {
+ private final String containerState;
+ private final int count;
+
+ public UnhealthyContainersSummary(String containerState, int count) {
+ this.containerState = containerState;
+ this.count = count;
}
- }
+ public String getContainerState() {
+ return containerState;
+ }
+
+ public int getCount() {
+ return count;
+ }
+ }
}
diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconDeadNodeHandler.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconDeadNodeHandler.java
index 56441a37c5ba..63a99ca1c83f 100644
--- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconDeadNodeHandler.java
+++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconDeadNodeHandler.java
@@ -26,7 +26,6 @@
import org.apache.hadoop.hdds.scm.node.NodeManager;
import org.apache.hadoop.hdds.scm.pipeline.PipelineManager;
import org.apache.hadoop.hdds.server.events.EventPublisher;
-import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,14 +39,14 @@ public class ReconDeadNodeHandler extends DeadNodeHandler {
LoggerFactory.getLogger(ReconDeadNodeHandler.class);
private StorageContainerServiceProvider scmClient;
- private ContainerHealthTask containerHealthTask;
+ private ReconScmTask containerHealthTask;
private PipelineSyncTask pipelineSyncTask;
public ReconDeadNodeHandler(NodeManager nodeManager,
PipelineManager pipelineManager,
ContainerManager containerManager,
StorageContainerServiceProvider scmClient,
- ContainerHealthTask containerHealthTask,
+ ReconScmTask containerHealthTask,
PipelineSyncTask pipelineSyncTask) {
super(nodeManager, pipelineManager, containerManager);
this.scmClient = scmClient;
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 57067c421344..bc6d4943ecdf 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
@@ -115,6 +115,7 @@
import org.apache.hadoop.ozone.recon.ReconServerConfigKeys;
import org.apache.hadoop.ozone.recon.ReconUtils;
import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask;
+import org.apache.hadoop.ozone.recon.fsck.ReconReplicationManager;
import org.apache.hadoop.ozone.recon.fsck.ReconSafeModeMgrTask;
import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
@@ -154,8 +155,9 @@ public class ReconStorageContainerManagerFacade
private final SCMNodeDetails reconNodeDetails;
private final SCMHAManager scmhaManager;
private final SequenceIdGenerator sequenceIdGen;
- private final ContainerHealthTask containerHealthTask;
+ private final ReconScmTask containerHealthTask;
private final DataSource dataSource;
+ private final ContainerHealthSchemaManager containerHealthSchemaManager;
private DBStore dbStore;
private ReconNodeManager nodeManager;
@@ -167,6 +169,7 @@ public class ReconStorageContainerManagerFacade
private ReconSafeModeMgrTask reconSafeModeMgrTask;
private ContainerSizeCountTask containerSizeCountTask;
private ContainerCountBySizeDao containerCountBySizeDao;
+ private ReconReplicationManager reconReplicationManager;
private AtomicBoolean isSyncDataFromSCMRunning;
private final String threadNamePrefix;
@@ -178,13 +181,13 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf,
StorageContainerServiceProvider scmServiceProvider,
ContainerCountBySizeDao containerCountBySizeDao,
UtilizationSchemaDefinition utilizationSchemaDefinition,
- ContainerHealthSchemaManager containerHealthSchemaManager,
ReconContainerMetadataManager reconContainerMetadataManager,
ReconUtils reconUtils,
ReconSafeModeManager safeModeManager,
ReconContext reconContext,
DataSource dataSource,
- ReconTaskStatusUpdaterManager taskStatusUpdaterManager)
+ ReconTaskStatusUpdaterManager taskStatusUpdaterManager,
+ ContainerHealthSchemaManager containerHealthSchemaManager)
throws IOException {
reconNodeDetails = reconUtils.getReconNodeDetails(conf);
this.threadNamePrefix = reconNodeDetails.threadNamePrefix();
@@ -246,7 +249,8 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf,
dbStore,
ReconSCMDBDefinition.CONTAINERS.getTable(dbStore),
pipelineManager, scmServiceProvider,
- containerHealthSchemaManager, reconContainerMetadataManager,
+ containerHealthSchemaManager,
+ reconContainerMetadataManager,
scmhaManager, sequenceIdGen, pendingOps);
this.scmServiceProvider = scmServiceProvider;
this.isSyncDataFromSCMRunning = new AtomicBoolean();
@@ -266,15 +270,44 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf,
PipelineSyncTask pipelineSyncTask = new PipelineSyncTask(pipelineManager, nodeManager,
scmServiceProvider, reconTaskConfig, taskStatusUpdaterManager);
- containerHealthTask = new ContainerHealthTask(containerManager, scmServiceProvider,
- containerHealthSchemaManager, containerPlacementPolicy,
- reconTaskConfig, reconContainerMetadataManager, conf, taskStatusUpdaterManager);
+ // Create ContainerHealthTask (always runs, writes to UNHEALTHY_CONTAINERS)
+ LOG.info("Creating ContainerHealthTask");
+ containerHealthTask = new ContainerHealthTask(
+ reconTaskConfig,
+ taskStatusUpdaterManager,
+ this // ReconStorageContainerManagerFacade - provides access to ReconReplicationManager
+ );
this.containerSizeCountTask = new ContainerSizeCountTask(containerManager,
reconTaskConfig, containerCountBySizeDao, utilizationSchemaDefinition, taskStatusUpdaterManager);
+ this.containerHealthSchemaManager = containerHealthSchemaManager;
this.dataSource = dataSource;
+ // Initialize Recon's ReplicationManager for local health checks
+ try {
+ LOG.info("Creating ReconReplicationManager");
+ this.reconReplicationManager = new ReconReplicationManager(
+ ReconReplicationManager.InitContext.newBuilder()
+ .setRmConf(conf.getObject(ReplicationManager.ReplicationManagerConfiguration.class))
+ .setConf(conf)
+ .setContainerManager(containerManager)
+ // Use same placement policy for both Ratis and EC in Recon.
+ .setRatisContainerPlacement(containerPlacementPolicy)
+ .setEcContainerPlacement(containerPlacementPolicy)
+ .setEventPublisher(eventQueue)
+ .setScmContext(scmContext)
+ .setNodeManager(nodeManager)
+ .setClock(Clock.system(ZoneId.systemDefault()))
+ .build(),
+ containerHealthSchemaManager
+ );
+ LOG.info("Successfully created ReconReplicationManager");
+ } catch (IOException e) {
+ LOG.error("Failed to create ReconReplicationManager", e);
+ throw e;
+ }
+
StaleNodeHandler staleNodeHandler =
new ReconStaleNodeHandler(nodeManager, pipelineManager, pipelineSyncTask);
DeadNodeHandler deadNodeHandler = new ReconDeadNodeHandler(nodeManager,
@@ -671,7 +704,7 @@ public ContainerManager getContainerManager() {
@Override
public ReplicationManager getReplicationManager() {
- return null;
+ return reconReplicationManager;
}
@Override
@@ -727,7 +760,7 @@ public ContainerSizeCountTask getContainerSizeCountTask() {
}
@VisibleForTesting
- public ContainerHealthTask getContainerHealthTask() {
+ public ReconScmTask getContainerHealthTask() {
return containerHealthTask;
}
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
index 76d5bdcb9a91..a1e8abf8d0c0 100644
--- 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
@@ -34,7 +34,9 @@ public enum ReconLayoutFeature {
// 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");
+ 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;
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
new file mode 100644
index 000000000000..6bd91b78a7e6
--- /dev/null
+++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainersStateContainerIdIndexUpgradeAction.java
@@ -0,0 +1,86 @@
+/*
+ * 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.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;
+import java.sql.ResultSet;
+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;
+
+/**
+ * Upgrade action to ensure idx_state_container_id exists on UNHEALTHY_CONTAINERS.
+ */
+@UpgradeActionRecon(feature = ReconLayoutFeature.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX)
+public class UnhealthyContainersStateContainerIdIndexUpgradeAction
+ implements ReconUpgradeAction {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(UnhealthyContainersStateContainerIdIndexUpgradeAction.class);
+ private static final String INDEX_NAME = "idx_state_container_id";
+
+ @Override
+ public void execute(DataSource source) throws Exception {
+ try (Connection conn = source.getConnection()) {
+ if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) {
+ return;
+ }
+
+ if (indexExists(conn, INDEX_NAME)) {
+ LOG.info("Index {} already exists on {}", INDEX_NAME,
+ UNHEALTHY_CONTAINERS_TABLE_NAME);
+ return;
+ }
+
+ DSLContext dslContext = DSL.using(conn);
+ LOG.info("Creating index {} on {}", INDEX_NAME,
+ 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")))
+ .execute();
+ } catch (SQLException e) {
+ throw new SQLException("Failed to create " + INDEX_NAME
+ + " 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/api/TestClusterStateEndpoint.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestClusterStateEndpoint.java
index 6e187095c92a..fd497fd4b965 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestClusterStateEndpoint.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestClusterStateEndpoint.java
@@ -195,7 +195,7 @@ public void testStorageReportIsClusterStorageReport() {
when(mockContainerManager.getContainerStateCount(HddsProtos.LifeCycleState.DELETED))
.thenReturn(0);
when(mockContainerHealthSchemaManager.getUnhealthyContainers(
- any(), anyLong(), any(), anyInt())).thenReturn(Collections.emptyList());
+ any(), anyLong(), anyLong(), anyInt())).thenReturn(Collections.emptyList());
SCMNodeStat scmNodeStat = new SCMNodeStat(
1000L, 400L, 600L, 300L, 50L, 20L);
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
index 1d4c546181fd..cc2a02d0da36 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
@@ -114,7 +114,6 @@
import org.apache.hadoop.ozone.recon.tasks.NSSummaryTaskWithFSO;
import org.apache.hadoop.ozone.recon.tasks.ReconOmTask;
import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates;
-import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers;
import org.apache.ozone.test.tag.Flaky;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -951,7 +950,7 @@ public void testUnhealthyContainersFilteredResponse()
throws IOException, TimeoutException {
String missing = UnHealthyContainerStates.MISSING.toString();
String emptyMissing = UnHealthyContainerStates.EMPTY_MISSING.toString();
- String negativeSize = UnHealthyContainerStates.NEGATIVE_SIZE.toString(); // For NEGATIVE_SIZE state
+ String negativeSize = UnHealthyContainerStates.NEGATIVE_SIZE.toString();
// Initial empty response verification
Response response = containerEndpoint
@@ -973,8 +972,6 @@ public void testUnhealthyContainersFilteredResponse()
uuid3 = newDatanode("host3", "127.0.0.3");
uuid4 = newDatanode("host4", "127.0.0.4");
createUnhealthyRecords(5, 4, 3, 2, 1);
- createEmptyMissingUnhealthyRecords(2); // For EMPTY_MISSING state
- createNegativeSizeUnhealthyRecords(2); // For NEGATIVE_SIZE state
// Check for unhealthy containers
response = containerEndpoint.getUnhealthyContainers(missing, 1000, 0, 0);
@@ -1000,19 +997,19 @@ public void testUnhealthyContainersFilteredResponse()
assertEquals(missing, r.getContainerState());
}
- // Check for empty missing containers, should return zero
+ // Compatibility: legacy states should be valid filters and return empty.
Response filteredEmptyMissingResponse = containerEndpoint
.getUnhealthyContainers(emptyMissing, 1000, 0, 0);
responseObject = (UnhealthyContainersResponse) filteredEmptyMissingResponse.getEntity();
records = responseObject.getContainers();
assertEquals(0, records.size());
- // Check for negative size containers, should return zero
Response filteredNegativeSizeResponse = containerEndpoint
.getUnhealthyContainers(negativeSize, 1000, 0, 0);
responseObject = (UnhealthyContainersResponse) filteredNegativeSizeResponse.getEntity();
records = responseObject.getContainers();
assertEquals(0, records.size());
+
}
@Test
@@ -1128,22 +1125,6 @@ UUID newDatanode(String hostName, String ipAddress) throws IOException {
return uuid;
}
- private void createEmptyMissingUnhealthyRecords(int emptyMissing) {
- int cid = 0;
- for (int i = 0; i < emptyMissing; i++) {
- createUnhealthyRecord(++cid, UnHealthyContainerStates.EMPTY_MISSING.toString(),
- 3, 3, 0, null, false);
- }
- }
-
- private void createNegativeSizeUnhealthyRecords(int negativeSize) {
- int cid = 0;
- for (int i = 0; i < negativeSize; i++) {
- createUnhealthyRecord(++cid, UnHealthyContainerStates.NEGATIVE_SIZE.toString(),
- 3, 3, 0, null, false); // Added for NEGATIVE_SIZE state
- }
- }
-
private void createUnhealthyRecords(int missing, int overRep, int underRep,
int misRep, int dataChecksum) {
int cid = 0;
@@ -1176,18 +1157,11 @@ private void createUnhealthyRecords(int missing, int overRep, int underRep,
private void createUnhealthyRecord(int id, String state, int expected,
int actual, int delta, String reason, boolean dataChecksumMismatch) {
long cID = Integer.toUnsignedLong(id);
- UnhealthyContainers missing = new UnhealthyContainers();
- missing.setContainerId(cID);
- missing.setContainerState(state);
- missing.setInStateSince(12345L);
- missing.setActualReplicaCount(actual);
- missing.setExpectedReplicaCount(expected);
- missing.setReplicaDelta(delta);
- missing.setReason(reason);
-
- ArrayList missingList = new ArrayList<>();
- missingList.add(missing);
- containerHealthSchemaManager.insertUnhealthyContainerRecords(missingList);
+ ArrayList records =
+ new ArrayList<>();
+ records.add(new ContainerHealthSchemaManager.UnhealthyContainerRecord(
+ cID, state, 12345L, expected, actual, delta, reason));
+ containerHealthSchemaManager.insertUnhealthyContainerRecords(records);
long differentChecksum = dataChecksumMismatch ? 2345L : 1234L;
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestDeletedKeysSearchEndpoint.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestDeletedKeysSearchEndpoint.java
index 39ab3ec18a81..e4393cafba5f 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestDeletedKeysSearchEndpoint.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestDeletedKeysSearchEndpoint.java
@@ -44,7 +44,6 @@
import org.apache.hadoop.ozone.recon.ReconTestInjector;
import org.apache.hadoop.ozone.recon.api.types.KeyInsightInfoResponse;
import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
@@ -100,7 +99,6 @@ public void setUp() throws Exception {
.addBinding(StorageContainerServiceProvider.class,
mock(StorageContainerServiceProviderImpl.class))
.addBinding(OMDBInsightEndpoint.class)
- .addBinding(ContainerHealthSchemaManager.class)
.build();
omdbInsightEndpoint = reconTestInjector.getInstance(OMDBInsightEndpoint.class);
populateOMDB();
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOmDBInsightEndPoint.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOmDBInsightEndPoint.java
index a0f53f3172d8..c4f419b6e634 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOmDBInsightEndPoint.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOmDBInsightEndPoint.java
@@ -77,7 +77,6 @@
import org.apache.hadoop.ozone.recon.api.types.ReconBasicOmKeyInfo;
import org.apache.hadoop.ozone.recon.api.types.ResponseStatus;
import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.scm.ReconPipelineManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
@@ -310,7 +309,6 @@ public void setUp() throws Exception {
.addBinding(StorageContainerServiceProvider.class,
mock(StorageContainerServiceProviderImpl.class))
.addBinding(OMDBInsightEndpoint.class)
- .addBinding(ContainerHealthSchemaManager.class)
.build();
reconContainerMetadataManager =
reconTestInjector.getInstance(ReconContainerMetadataManager.class);
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenContainerCount.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenContainerCount.java
index 639d72944a9e..be2fd9bd8247 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenContainerCount.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenContainerCount.java
@@ -72,7 +72,6 @@
import org.apache.hadoop.ozone.recon.api.types.DatanodeMetadata;
import org.apache.hadoop.ozone.recon.api.types.DatanodesResponse;
import org.apache.hadoop.ozone.recon.common.ReconTestUtils;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.scm.ReconPipelineManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
@@ -205,7 +204,6 @@ private void initializeInjector() throws Exception {
.withContainerDB()
.addBinding(NodeEndpoint.class)
.addBinding(MetricsServiceProviderFactory.class)
- .addBinding(ContainerHealthSchemaManager.class)
.addBinding(ReconUtils.class, reconUtilsMock)
.addBinding(StorageContainerLocationProtocol.class,
mockScmClient)
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenKeysSearchEndpoint.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenKeysSearchEndpoint.java
index f8dfd43a6701..510367870ea4 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenKeysSearchEndpoint.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestOpenKeysSearchEndpoint.java
@@ -44,7 +44,6 @@
import org.apache.hadoop.ozone.recon.ReconTestInjector;
import org.apache.hadoop.ozone.recon.api.types.KeyInsightInfoResponse;
import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager;
@@ -104,7 +103,6 @@ public void setUp() throws Exception {
.addBinding(StorageContainerServiceProvider.class,
mock(StorageContainerServiceProviderImpl.class))
.addBinding(OMDBInsightEndpoint.class)
- .addBinding(ContainerHealthSchemaManager.class)
.build();
ReconNamespaceSummaryManager reconNamespaceSummaryManager =
reconTestInjector.getInstance(ReconNamespaceSummaryManager.class);
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
index baee4efe4535..da7edc620f32 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
@@ -50,7 +50,6 @@
import org.apache.hadoop.ozone.recon.ReconTestInjector;
import org.apache.hadoop.ozone.recon.ReconUtils;
import org.apache.hadoop.ozone.recon.common.ReconTestUtils;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
@@ -138,7 +137,6 @@ reconUtilsMock, ozoneManagerProtocol, new ReconContext(configuration, reconUtils
.withContainerDB()
.addBinding(NodeEndpoint.class)
.addBinding(MetricsServiceProviderFactory.class)
- .addBinding(ContainerHealthSchemaManager.class)
.addBinding(ReconUtils.class, reconUtilsMock)
.addBinding(StorageContainerLocationProtocol.class,
mock(StorageContainerLocationProtocol.class))
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java
index 4210756d1cd5..367c77f3f504 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java
@@ -17,788 +17,77 @@
package org.apache.hadoop.ozone.recon.fsck;
-import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.THREE;
-import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates.ALL_REPLICAS_BAD;
-import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import java.io.IOException;
-import java.lang.reflect.Field;
import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import org.apache.hadoop.hdds.client.RatisReplicationConfig;
-import org.apache.hadoop.hdds.client.ReplicatedReplicationConfig;
-import org.apache.hadoop.hdds.conf.OzoneConfiguration;
-import org.apache.hadoop.hdds.protocol.DatanodeDetails;
-import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
-import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
-import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto.State;
-import org.apache.hadoop.hdds.scm.ContainerPlacementStatus;
-import org.apache.hadoop.hdds.scm.PlacementPolicy;
-import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
-import org.apache.hadoop.hdds.scm.container.ContainerID;
-import org.apache.hadoop.hdds.scm.container.ContainerInfo;
-import org.apache.hadoop.hdds.scm.container.ContainerManager;
-import org.apache.hadoop.hdds.scm.container.ContainerReplica;
-import org.apache.hadoop.hdds.scm.container.TestContainerInfo;
-import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline;
-import org.apache.hadoop.hdds.scm.container.placement.algorithms.ContainerPlacementStatusDefault;
-import org.apache.hadoop.ozone.recon.metrics.ContainerHealthMetrics;
-import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
-import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
-import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig;
import org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdater;
import org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdaterManager;
-import org.apache.ozone.recon.schema.ContainerSchemaDefinition;
-import org.apache.ozone.recon.schema.generated.tables.daos.ReconTaskStatusDao;
-import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersDao;
-import org.apache.ozone.recon.schema.generated.tables.pojos.ReconTaskStatus;
-import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers;
-import org.apache.ozone.test.LambdaTestUtils;
import org.junit.jupiter.api.Test;
/**
- * Class to test a single run of the Container Health Task.
+ * Unit tests for ContainerHealthTask execution flow.
*/
-public class TestContainerHealthTask extends AbstractReconSqlDBTest {
+public class TestContainerHealthTask {
- public TestContainerHealthTask() {
- super();
- }
-
- @SuppressWarnings("checkstyle:methodlength")
@Test
- public void testRun() throws Exception {
- UnhealthyContainersDao unHealthyContainersTableHandle =
- getDao(UnhealthyContainersDao.class);
-
- ContainerHealthSchemaManager containerHealthSchemaManager =
- new ContainerHealthSchemaManager(
- getSchemaDefinition(ContainerSchemaDefinition.class),
- unHealthyContainersTableHandle);
- ReconStorageContainerManagerFacade scmMock =
+ public void testRunTaskInvokesReconReplicationManagerProcessAll()
+ throws Exception {
+ ReconReplicationManager reconReplicationManager =
+ mock(ReconReplicationManager.class);
+ ReconStorageContainerManagerFacade reconScm =
mock(ReconStorageContainerManagerFacade.class);
- ReconContainerMetadataManager reconContainerMetadataManager =
- mock(ReconContainerMetadataManager.class);
- MockPlacementPolicy placementMock = new MockPlacementPolicy();
- ContainerManager containerManagerMock = mock(ContainerManager.class);
- StorageContainerServiceProvider scmClientMock =
- mock(StorageContainerServiceProvider.class);
- ContainerReplica unhealthyReplicaMock = mock(ContainerReplica.class);
- when(unhealthyReplicaMock.getState()).thenReturn(State.UNHEALTHY);
- ContainerReplica healthyReplicaMock = mock(ContainerReplica.class);
- when(healthyReplicaMock.getState()).thenReturn(State.CLOSED);
-
- // Create 7 containers. The first 5 will have various unhealthy states
- // defined below. The container with ID=6 will be healthy and
- // container with ID=7 will be EMPTY_MISSING (but not inserted into DB)
- List mockContainers = getMockContainers(8);
- when(scmMock.getScmServiceProvider()).thenReturn(scmClientMock);
- when(scmMock.getContainerManager()).thenReturn(containerManagerMock);
- when(containerManagerMock.getContainers(any(ContainerID.class),
- anyInt())).thenReturn(mockContainers);
- for (ContainerInfo c : mockContainers) {
- when(containerManagerMock.getContainer(c.containerID())).thenReturn(c);
- when(scmClientMock.getContainerWithPipeline(c.getContainerID()))
- .thenReturn(new ContainerWithPipeline(c, null));
- }
-
- ReplicatedReplicationConfig replicationConfig = RatisReplicationConfig.getInstance(THREE);
- // Under replicated
- ContainerInfo containerInfo1 =
- TestContainerInfo.newBuilderForTest().setContainerID(1).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(1L))).thenReturn(containerInfo1);
- when(containerManagerMock.getContainerReplicas(containerInfo1.containerID()))
- .thenReturn(getMockReplicas(1L, State.CLOSED, State.UNHEALTHY));
-
- // return all UNHEALTHY replicas for container ID 2 -> UNDER_REPLICATED
- ContainerInfo containerInfo2 =
- TestContainerInfo.newBuilderForTest().setContainerID(2).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(2L))).thenReturn(containerInfo2);
- when(containerManagerMock.getContainerReplicas(containerInfo2.containerID()))
- .thenReturn(getMockReplicas(2L, State.UNHEALTHY));
-
- // return 0 replicas for container ID 3 -> EMPTY_MISSING (will not be inserted into DB)
- ContainerInfo containerInfo3 =
- TestContainerInfo.newBuilderForTest().setContainerID(3).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(3L))).thenReturn(containerInfo3);
- when(containerManagerMock.getContainerReplicas(containerInfo3.containerID()))
- .thenReturn(Collections.emptySet());
-
- // Return 5 Healthy Replicas -> Over-replicated
- ContainerInfo containerInfo4 =
- TestContainerInfo.newBuilderForTest().setContainerID(4).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(4L))).thenReturn(containerInfo4);
- when(containerManagerMock.getContainerReplicas(containerInfo4.containerID()))
- .thenReturn(getMockReplicas(4L, State.CLOSED, State.CLOSED,
- State.CLOSED, State.CLOSED, State.CLOSED));
-
- // Mis-replicated
- ContainerInfo containerInfo5 =
- TestContainerInfo.newBuilderForTest().setContainerID(5).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(5L))).thenReturn(containerInfo5);
- Set misReplicas = getMockReplicas(5L,
- State.CLOSED, State.CLOSED, State.CLOSED);
- placementMock.setMisRepWhenDnPresent(
- misReplicas.iterator().next().getDatanodeDetails().getUuid());
- when(containerManagerMock.getContainerReplicas(containerInfo5.containerID()))
- .thenReturn(misReplicas);
-
- // Return 3 Healthy Replicas -> Healthy container
- ContainerInfo containerInfo6 =
- TestContainerInfo.newBuilderForTest().setContainerID(6).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(6L))).thenReturn(containerInfo6);
- when(containerManagerMock.getContainerReplicas(containerInfo6.containerID()))
- .thenReturn(getMockReplicas(6L,
- State.CLOSED, State.CLOSED, State.CLOSED));
-
- // return 0 replicas for container ID 7 -> MISSING (will later transition to EMPTY_MISSING but not inserted into DB)
- ContainerInfo containerInfo7 =
- TestContainerInfo.newBuilderForTest().setContainerID(7).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(7L))).thenReturn(containerInfo7);
- when(containerManagerMock.getContainerReplicas(containerInfo7.containerID()))
- .thenReturn(Collections.emptySet());
- when(reconContainerMetadataManager.getKeyCountForContainer(
- 7L)).thenReturn(5L); // Indicates non-empty container 7 for now
-
- // container ID 8 - REPLICA_MISMATCH
- ContainerInfo containerInfo8 =
- TestContainerInfo.newBuilderForTest().setContainerID(8).setReplicationConfig(replicationConfig).build();
- when(containerManagerMock.getContainer(ContainerID.valueOf(8L))).thenReturn(containerInfo8);
- Set mismatchReplicas = getMockReplicasChecksumMismatch(8L,
- State.CLOSED, State.CLOSED, State.CLOSED);
- when(containerManagerMock.getContainerReplicas(containerInfo8.containerID()))
- .thenReturn(mismatchReplicas);
-
- List all = unHealthyContainersTableHandle.findAll();
- assertThat(all).isEmpty();
-
- long currentTime = System.currentTimeMillis();
- ReconTaskStatusDao reconTaskStatusDao = getDao(ReconTaskStatusDao.class);
- ReconTaskConfig reconTaskConfig = new ReconTaskConfig();
- reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10));
-
- // Start container health task
- ContainerHealthTask containerHealthTask =
- new ContainerHealthTask(scmMock.getContainerManager(),
- scmMock.getScmServiceProvider(), containerHealthSchemaManager,
- placementMock, reconTaskConfig, reconContainerMetadataManager,
- new OzoneConfiguration(), getMockTaskStatusUpdaterManager());
- containerHealthTask.start();
-
- // Ensure unhealthy container count in DB matches expected
- LambdaTestUtils.await(60000, 1000, () ->
- (unHealthyContainersTableHandle.count() == 6));
-
- // Check for UNDER_REPLICATED container states
- UnhealthyContainers rec =
- unHealthyContainersTableHandle.fetchByContainerId(1L).get(0);
- assertEquals("UNDER_REPLICATED", rec.getContainerState());
- assertEquals(2, rec.getReplicaDelta().intValue());
-
- rec = unHealthyContainersTableHandle.fetchByContainerId(2L).get(0);
- assertEquals("UNDER_REPLICATED", rec.getContainerState());
- assertEquals(3, rec.getReplicaDelta().intValue());
-
- // Assert that EMPTY_MISSING state containers were never added to DB.
- assertEquals(0,
- unHealthyContainersTableHandle.fetchByContainerId(3L).size());
-
- List unhealthyContainers =
- containerHealthSchemaManager.getUnhealthyContainers(
- ALL_REPLICAS_BAD, 0L, Optional.empty(), Integer.MAX_VALUE);
- assertEquals(1, unhealthyContainers.size());
- assertEquals(2L,
- unhealthyContainers.get(0).getContainerId().longValue());
- assertEquals(0,
- unhealthyContainers.get(0).getActualReplicaCount().intValue());
-
- // Check for MISSING state in container ID 7
- rec = unHealthyContainersTableHandle.fetchByContainerId(7L).get(0);
- assertEquals("MISSING", rec.getContainerState());
- assertEquals(3, rec.getReplicaDelta().intValue());
-
- Field field = ContainerHealthTask.class.getDeclaredField("containerHealthMetrics");
- field.setAccessible(true);
-
- // Read private field value
- ContainerHealthMetrics containerHealthMetrics = (ContainerHealthMetrics) field.get(containerHealthTask);
-
- // Only Container ID: 7 is MISSING, so count of missing container count metrics should be equal to 1
- assertEquals(1, containerHealthMetrics.getMissingContainerCount());
- // Container ID: 1 and Container ID: 2, both are UNDER_REPLICATED, so UNDER_REPLICATED
- // container count metric should be 2
- assertEquals(2, containerHealthMetrics.getUnderReplicatedContainerCount());
-
- rec = unHealthyContainersTableHandle.fetchByContainerId(4L).get(0);
- assertEquals("OVER_REPLICATED", rec.getContainerState());
- assertEquals(-2, rec.getReplicaDelta().intValue());
-
- rec = unHealthyContainersTableHandle.fetchByContainerId(5L).get(0);
- assertEquals("MIS_REPLICATED", rec.getContainerState());
- assertEquals(1, rec.getReplicaDelta().intValue());
- assertEquals(2, rec.getExpectedReplicaCount().intValue());
- assertEquals(1, rec.getActualReplicaCount().intValue());
- assertNotNull(rec.getReason());
-
- rec = unHealthyContainersTableHandle.fetchByContainerId(8L).get(0);
- assertEquals("REPLICA_MISMATCH", rec.getContainerState());
- assertEquals(0, rec.getReplicaDelta().intValue());
- assertEquals(3, rec.getExpectedReplicaCount().intValue());
- assertEquals(3, rec.getActualReplicaCount().intValue());
-
- ReconTaskStatus taskStatus =
- reconTaskStatusDao.findById(containerHealthTask.getTaskName());
- assertThat(taskStatus.getLastUpdatedTimestamp())
- .isGreaterThan(currentTime);
-
- // Adjust the mock results and rerun to check for updates or removal of records
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(1L)))
- .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED));
-
- // ID 2 was UNDER_REPLICATED - make it healthy now and after this step, UNDER_REPLICATED
- // container count metric will be 1.
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(2L)))
- .thenReturn(getMockReplicas(2L,
- State.CLOSED, State.CLOSED, State.CLOSED));
-
- // Container 3 remains EMPTY_MISSING, but no DB insertion
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(3L)))
- .thenReturn(Collections.emptySet());
-
- // Return 4 Healthy -> Delta changes from -2 to -1
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(4L)))
- .thenReturn(getMockReplicas(4L, State.CLOSED, State.CLOSED,
- State.CLOSED, State.CLOSED));
-
- // Convert container 7 which was MISSING to EMPTY_MISSING (not inserted into DB)
- when(reconContainerMetadataManager.getKeyCountForContainer(
- 7L)).thenReturn(0L);
-
- placementMock.setMisRepWhenDnPresent(null);
-
- // Ensure count is reduced after EMPTY_MISSING containers are not inserted
- LambdaTestUtils.await(60000, 1000, () ->
- (unHealthyContainersTableHandle.count() == 3));
-
- rec = unHealthyContainersTableHandle.fetchByContainerId(1L).get(0);
- assertEquals("UNDER_REPLICATED", rec.getContainerState());
- assertEquals(1, rec.getReplicaDelta().intValue());
-
- // This container is now healthy, it should not be in the table any more
- assertEquals(0,
- unHealthyContainersTableHandle.fetchByContainerId(2L).size());
-
- // Now since container ID: 2 is gone back to HEALTHY state in above step, so UNDER-REPLICATED
- // container count should be just 1 (denoting only for container ID : 1)
- assertEquals(1, containerHealthMetrics.getUnderReplicatedContainerCount());
-
- // Assert that for container 7 no records exist in DB because it's now EMPTY_MISSING
- assertEquals(0,
- unHealthyContainersTableHandle.fetchByContainerId(7L).size());
-
- // Since Container ID: 7 is now EMPTY_MISSING, so MISSING container count metric
- // will now be 0 as there is no missing container now.
- assertEquals(0, containerHealthMetrics.getMissingContainerCount());
-
- rec = unHealthyContainersTableHandle.fetchByContainerId(4L).get(0);
- assertEquals("OVER_REPLICATED", rec.getContainerState());
- assertEquals(-1, rec.getReplicaDelta().intValue());
-
- // Ensure container 5 is now healthy and not in the table
- assertEquals(0,
- unHealthyContainersTableHandle.fetchByContainerId(5L).size());
-
- // Just check once again that count remains consistent
- LambdaTestUtils.await(60000, 1000, () ->
- (unHealthyContainersTableHandle.count() == 3));
-
- // Since other container states have been changing, but no change in UNDER_REPLICATED
- // container count, UNDER_REPLICATED count metric should not be affected from previous
- // assertion count.
- assertEquals(1, containerHealthMetrics.getUnderReplicatedContainerCount());
- assertEquals(0, containerHealthMetrics.getMissingContainerCount());
-
- containerHealthTask.stop();
- }
-
- @Test
- public void testDeletedContainer() throws Exception {
- UnhealthyContainersDao unHealthyContainersTableHandle =
- getDao(UnhealthyContainersDao.class);
-
- ContainerHealthSchemaManager containerHealthSchemaManager =
- new ContainerHealthSchemaManager(
- getSchemaDefinition(ContainerSchemaDefinition.class),
- unHealthyContainersTableHandle);
- ReconStorageContainerManagerFacade scmMock =
- mock(ReconStorageContainerManagerFacade.class);
- MockPlacementPolicy placementMock = new MockPlacementPolicy();
- ContainerManager containerManagerMock = mock(ContainerManager.class);
- StorageContainerServiceProvider scmClientMock =
- mock(StorageContainerServiceProvider.class);
- ReconContainerMetadataManager reconContainerMetadataManager =
- mock(ReconContainerMetadataManager.class);
-
- // Create 2 containers. The first is OPEN will no replicas, the second is
- // CLOSED with no replicas.
- List mockContainers = getMockContainers(3);
- when(scmMock.getScmServiceProvider()).thenReturn(scmClientMock);
- when(scmMock.getContainerManager()).thenReturn(containerManagerMock);
- when(containerManagerMock.getContainers(any(ContainerID.class),
- anyInt())).thenReturn(mockContainers);
- for (ContainerInfo c : mockContainers) {
- when(containerManagerMock.getContainer(c.containerID())).thenReturn(c);
- when(scmClientMock.getContainerWithPipeline(c.getContainerID()))
- .thenReturn(new ContainerWithPipeline(c, null));
- }
- // Empty Container with OPEN State and no replicas
- when(containerManagerMock.getContainer(ContainerID.valueOf(1L)).getState())
- .thenReturn(HddsProtos.LifeCycleState.OPEN);
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(1L)))
- .thenReturn(Collections.emptySet());
- when(scmClientMock.getContainerWithPipeline(1))
- .thenReturn(new ContainerWithPipeline(mockContainers.get(0), null));
-
- // Container State CLOSED with no replicas
- when(containerManagerMock.getContainer(ContainerID.valueOf(2L)).getState())
- .thenReturn(HddsProtos.LifeCycleState.CLOSED);
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(2L)))
- .thenReturn(Collections.emptySet());
- ContainerInfo mockDeletedContainer = getMockDeletedContainer(2);
- when(scmClientMock.getContainerWithPipeline(2))
- .thenReturn(new ContainerWithPipeline(mockDeletedContainer, null));
-
- // Container with OPEN State and no replicas
- when(containerManagerMock.getContainer(ContainerID.valueOf(3L)).getState())
- .thenReturn(HddsProtos.LifeCycleState.OPEN);
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(3L)))
- .thenReturn(Collections.emptySet());
- when(scmClientMock.getContainerWithPipeline(3))
- .thenReturn(new ContainerWithPipeline(mockContainers.get(0), null));
-
- List all = unHealthyContainersTableHandle.findAll();
- assertThat(all).isEmpty();
-
- long currentTime = System.currentTimeMillis();
- ReconTaskStatusDao reconTaskStatusDao = getDao(ReconTaskStatusDao.class);
- ReconTaskConfig reconTaskConfig = new ReconTaskConfig();
- reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2));
- when(reconContainerMetadataManager.getKeyCountForContainer(
- 1L)).thenReturn(5L);
- ContainerHealthTask containerHealthTask =
- new ContainerHealthTask(scmMock.getContainerManager(),
- scmMock.getScmServiceProvider(), containerHealthSchemaManager,
- placementMock, reconTaskConfig, reconContainerMetadataManager,
- new OzoneConfiguration(), getMockTaskStatusUpdaterManager());
- containerHealthTask.start();
- LambdaTestUtils.await(6000, 1000, () ->
- (unHealthyContainersTableHandle.count() == 1));
- UnhealthyContainers rec =
- unHealthyContainersTableHandle.fetchByContainerId(1L).get(0);
- assertEquals("MISSING", rec.getContainerState());
- assertEquals(3, rec.getReplicaDelta().intValue());
+ when(reconScm.getReplicationManager()).thenReturn(reconReplicationManager);
- ReconTaskStatus taskStatus =
- reconTaskStatusDao.findById(containerHealthTask.getTaskName());
- assertThat(taskStatus.getLastUpdatedTimestamp())
- .isGreaterThan(currentTime);
- }
-
- @Test
- public void testAllContainerStateInsertions() {
- UnhealthyContainersDao unHealthyContainersTableHandle =
- getDao(UnhealthyContainersDao.class);
-
- ContainerHealthSchemaManager containerHealthSchemaManager =
- new ContainerHealthSchemaManager(
- getSchemaDefinition(ContainerSchemaDefinition.class),
- unHealthyContainersTableHandle);
-
- // Iterate through each state in the UnHealthyContainerStates enum
- for (ContainerSchemaDefinition.UnHealthyContainerStates state :
- ContainerSchemaDefinition.UnHealthyContainerStates.values()) {
-
- // Create a dummy UnhealthyContainer record with the current state
- UnhealthyContainers unhealthyContainer = new UnhealthyContainers();
- unhealthyContainer.setContainerId(state.ordinal() + 1L);
-
- // Set replica counts based on the state
- switch (state) {
- case MISSING:
- case EMPTY_MISSING:
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(0);
- unhealthyContainer.setReplicaDelta(3);
- break;
-
- case UNDER_REPLICATED:
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(1);
- unhealthyContainer.setReplicaDelta(2);
- break;
-
- case OVER_REPLICATED:
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(4);
- unhealthyContainer.setReplicaDelta(-1);
- break;
-
- case MIS_REPLICATED:
- case NEGATIVE_SIZE:
- case REPLICA_MISMATCH:
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(3);
- unhealthyContainer.setReplicaDelta(0);
- break;
-
- case ALL_REPLICAS_BAD:
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(0);
- unhealthyContainer.setReplicaDelta(3);
- break;
-
- default:
- fail("Unhandled state: " + state.name() + ". Please add this state to the switch case.");
- }
-
- unhealthyContainer.setContainerState(state.name());
- unhealthyContainer.setInStateSince(System.currentTimeMillis());
-
- // Try inserting the record and catch any exception that occurs
- Exception exception = null;
- try {
- containerHealthSchemaManager.insertUnhealthyContainerRecords(
- Collections.singletonList(unhealthyContainer));
- } catch (Exception e) {
- exception = e;
- }
+ ContainerHealthTask task =
+ new ContainerHealthTask(
+ createTaskConfig(),
+ createTaskStatusUpdaterManagerMock(),
+ reconScm);
- // Assert no exception should be thrown for each state
- assertNull(exception,
- "Exception was thrown during insertion for state " + state.name() +
- ": " + exception);
-
- // Optionally, verify the record was inserted correctly
- List insertedRecords =
- unHealthyContainersTableHandle.fetchByContainerId(
- state.ordinal() + 1L);
- assertFalse(insertedRecords.isEmpty(),
- "Record was not inserted for state " + state.name() + ".");
- assertEquals(insertedRecords.get(0).getContainerState(), state.name(),
- "The inserted container state does not match for state " +
- state.name() + ".");
- }
- }
-
- @Test
- public void testInsertFailureAndUpdateBehavior() {
- UnhealthyContainersDao unHealthyContainersTableHandle =
- getDao(UnhealthyContainersDao.class);
-
- ContainerHealthSchemaManager containerHealthSchemaManager =
- new ContainerHealthSchemaManager(
- getSchemaDefinition(ContainerSchemaDefinition.class),
- unHealthyContainersTableHandle);
-
- ContainerSchemaDefinition.UnHealthyContainerStates state =
- ContainerSchemaDefinition.UnHealthyContainerStates.MISSING;
-
- long insertedTime = System.currentTimeMillis();
- // Create a dummy UnhealthyContainer record with the current state
- UnhealthyContainers unhealthyContainer = new UnhealthyContainers();
- unhealthyContainer.setContainerId(state.ordinal() + 1L);
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(0);
- unhealthyContainer.setReplicaDelta(3);
- unhealthyContainer.setContainerState(state.name());
- unhealthyContainer.setInStateSince(insertedTime);
-
- // Try inserting the record and catch any exception that occurs
- Exception exception = null;
- try {
- containerHealthSchemaManager.insertUnhealthyContainerRecords(
- Collections.singletonList(unhealthyContainer));
- } catch (Exception e) {
- exception = e;
- }
-
- // Assert no exception should be thrown for each state
- assertNull(exception,
- "Exception was thrown during insertion for state " + state.name() +
- ": " + exception);
-
- long updatedTime = System.currentTimeMillis();
- unhealthyContainer.setExpectedReplicaCount(3);
- unhealthyContainer.setActualReplicaCount(0);
- unhealthyContainer.setReplicaDelta(3);
- unhealthyContainer.setContainerState(state.name());
- unhealthyContainer.setInStateSince(updatedTime);
-
- try {
- containerHealthSchemaManager.insertUnhealthyContainerRecords(
- Collections.singletonList(unhealthyContainer));
- } catch (Exception e) {
- exception = e;
- }
-
- // Optionally, verify the record was updated correctly
- List updatedRecords =
- unHealthyContainersTableHandle.fetchByContainerId(
- state.ordinal() + 1L);
- assertFalse(updatedRecords.isEmpty(),
- "Record was not updated for state " + state.name() + ".");
- assertEquals(updatedRecords.get(0).getContainerState(), state.name(),
- "The inserted container state does not match for state " +
- state.name() + ".");
- assertEquals(updatedRecords.get(0).getInStateSince(), updatedTime);
+ task.runTask();
+ verify(reconReplicationManager, times(1)).processAll();
}
@Test
- public void testMissingAndEmptyMissingContainerDeletion() throws Exception {
- // Setup mock DAOs and managers
- UnhealthyContainersDao unHealthyContainersTableHandle =
- getDao(UnhealthyContainersDao.class);
- ContainerHealthSchemaManager containerHealthSchemaManager =
- new ContainerHealthSchemaManager(
- getSchemaDefinition(ContainerSchemaDefinition.class),
- unHealthyContainersTableHandle);
- ReconStorageContainerManagerFacade scmMock =
+ public void testRunTaskPropagatesProcessAllFailure() throws Exception {
+ ReconReplicationManager reconReplicationManager =
+ mock(ReconReplicationManager.class);
+ ReconStorageContainerManagerFacade reconScm =
mock(ReconStorageContainerManagerFacade.class);
- MockPlacementPolicy placementMock = new MockPlacementPolicy();
- ContainerManager containerManagerMock = mock(ContainerManager.class);
- StorageContainerServiceProvider scmClientMock =
- mock(StorageContainerServiceProvider.class);
- ReconContainerMetadataManager reconContainerMetadataManager =
- mock(ReconContainerMetadataManager.class);
- mock(ReconContainerMetadataManager.class);
-
- // Create 2 containers. They start in CLOSED state in Recon.
- List mockContainers = getMockContainers(2);
- when(scmMock.getScmServiceProvider()).thenReturn(scmClientMock);
- when(scmMock.getContainerManager()).thenReturn(containerManagerMock);
- when(containerManagerMock.getContainers(any(ContainerID.class),
- anyInt())).thenReturn(mockContainers);
-
- // Mark both containers as initially CLOSED in Recon
- for (ContainerInfo c : mockContainers) {
- when(containerManagerMock.getContainer(c.containerID())).thenReturn(c);
- }
-
- // Simulate SCM reporting the containers as DELETED
- ContainerInfo deletedContainer1 = getMockDeletedContainer(1);
- ContainerInfo deletedContainer2 = getMockDeletedContainer(2);
-
- when(scmClientMock.getContainerWithPipeline(1))
- .thenReturn(new ContainerWithPipeline(deletedContainer1, null));
- when(scmClientMock.getContainerWithPipeline(2))
- .thenReturn(new ContainerWithPipeline(deletedContainer2, null));
-
- // Both containers start as CLOSED in Recon (MISSING or EMPTY_MISSING)
- when(containerManagerMock.getContainer(ContainerID.valueOf(1L)).getState())
- .thenReturn(HddsProtos.LifeCycleState.CLOSED);
- when(containerManagerMock.getContainer(ContainerID.valueOf(2L)).getState())
- .thenReturn(HddsProtos.LifeCycleState.CLOSED);
-
- // Replicas are empty, so both containers should be considered for deletion
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(1L)))
- .thenReturn(Collections.emptySet());
- when(containerManagerMock.getContainerReplicas(ContainerID.valueOf(2L)))
- .thenReturn(Collections.emptySet());
-
- // Initialize UnhealthyContainers in DB (MISSING and EMPTY_MISSING)
- // Create and set up the first UnhealthyContainer for a MISSING container
- UnhealthyContainers container1 = new UnhealthyContainers();
- container1.setContainerId(1L);
- container1.setContainerState("MISSING");
- container1.setExpectedReplicaCount(3);
- container1.setActualReplicaCount(0);
- container1.setReplicaDelta(3);
- container1.setInStateSince(System.currentTimeMillis());
-
- // Create and set up the second UnhealthyContainer for an EMPTY_MISSING container
- UnhealthyContainers container2 = new UnhealthyContainers();
- container2.setContainerId(2L);
- container2.setContainerState("MISSING");
- container2.setExpectedReplicaCount(3);
- container2.setActualReplicaCount(0);
- container2.setReplicaDelta(3);
- container2.setInStateSince(System.currentTimeMillis());
-
- unHealthyContainersTableHandle.insert(container1);
- unHealthyContainersTableHandle.insert(container2);
-
- when(reconContainerMetadataManager.getKeyCountForContainer(1L)).thenReturn(5L);
- when(reconContainerMetadataManager.getKeyCountForContainer(2L)).thenReturn(0L);
-
- // Start the container health task
- ReconTaskConfig reconTaskConfig = new ReconTaskConfig();
- reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2));
- ContainerHealthTask containerHealthTask =
- new ContainerHealthTask(scmMock.getContainerManager(),
- scmMock.getScmServiceProvider(), containerHealthSchemaManager,
- placementMock, reconTaskConfig, reconContainerMetadataManager,
- new OzoneConfiguration(), getMockTaskStatusUpdaterManager());
-
- containerHealthTask.start();
-
- // Wait for the task to complete and ensure that updateContainerState is invoked for
- // container IDs 1 and 2 to mark the containers as DELETED, since they are DELETED in SCM.
- LambdaTestUtils.await(60000, 1000, () -> {
- verify(containerManagerMock, times(1))
- .updateContainerState(ContainerID.valueOf(1L), HddsProtos.LifeCycleEvent.DELETE);
- verify(containerManagerMock, times(1))
- .updateContainerState(ContainerID.valueOf(2L), HddsProtos.LifeCycleEvent.DELETE);
- return true;
- });
+ when(reconScm.getReplicationManager()).thenReturn(reconReplicationManager);
+ RuntimeException expected = new RuntimeException("processAll failed");
+ org.mockito.Mockito.doThrow(expected).when(reconReplicationManager)
+ .processAll();
+
+ ContainerHealthTask task =
+ new ContainerHealthTask(
+ createTaskConfig(),
+ createTaskStatusUpdaterManagerMock(),
+ reconScm);
+
+ RuntimeException thrown =
+ assertThrows(RuntimeException.class, task::runTask);
+ assertEquals("processAll failed", thrown.getMessage());
}
- private ReconTaskStatusUpdaterManager getMockTaskStatusUpdaterManager() {
- ReconTaskStatusUpdaterManager reconTaskStatusUpdaterManager = mock(ReconTaskStatusUpdaterManager.class);
- when(reconTaskStatusUpdaterManager.getTaskStatusUpdater(anyString())).thenAnswer(inv -> {
- String taskName = inv.getArgument(0);
- return new ReconTaskStatusUpdater(getDao(ReconTaskStatusDao.class), taskName);
- });
- return reconTaskStatusUpdaterManager;
+ private ReconTaskConfig createTaskConfig() {
+ ReconTaskConfig taskConfig = new ReconTaskConfig();
+ taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2));
+ return taskConfig;
}
- private Set getMockReplicas(
- long containerId, State...states) {
- Set replicas = new HashSet<>();
- for (State s : states) {
- replicas.add(ContainerReplica.newBuilder()
- .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails())
- .setContainerState(s)
- .setContainerID(ContainerID.valueOf(containerId))
- .setSequenceId(1)
- .setChecksums(ContainerChecksums.of(1234L, 0L))
- .build());
- }
- return replicas;
+ private ReconTaskStatusUpdaterManager createTaskStatusUpdaterManagerMock() {
+ ReconTaskStatusUpdaterManager manager =
+ mock(ReconTaskStatusUpdaterManager.class);
+ ReconTaskStatusUpdater updater = mock(ReconTaskStatusUpdater.class);
+ when(manager.getTaskStatusUpdater("ContainerHealthTask")).thenReturn(updater);
+ return manager;
}
-
- private Set getMockReplicasChecksumMismatch(
- long containerId, State...states) {
- Set replicas = new HashSet<>();
- long checksum = 1234L;
- for (State s : states) {
- replicas.add(ContainerReplica.newBuilder()
- .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails())
- .setContainerState(s)
- .setContainerID(ContainerID.valueOf(containerId))
- .setSequenceId(1)
- .setChecksums(ContainerChecksums.of(checksum, 0L))
- .build());
- checksum++;
- }
- return replicas;
- }
-
- private List getMockContainers(int num) {
- List containers = new ArrayList<>();
- for (int i = 1; i <= num; i++) {
- ContainerInfo c = mock(ContainerInfo.class);
- when(c.getContainerID()).thenReturn((long)i);
- when(c.getReplicationConfig())
- .thenReturn(RatisReplicationConfig.getInstance(
- THREE));
- when(c.getReplicationFactor())
- .thenReturn(THREE);
- when(c.getState()).thenReturn(HddsProtos.LifeCycleState.CLOSED);
- when(c.containerID()).thenReturn(ContainerID.valueOf(i));
- containers.add(c);
- }
- return containers;
- }
-
- private ContainerInfo getMockDeletedContainer(int containerID) {
- ContainerInfo c = mock(ContainerInfo.class);
- when(c.getContainerID()).thenReturn((long)containerID);
- when(c.getReplicationConfig())
- .thenReturn(RatisReplicationConfig
- .getInstance(THREE));
- when(c.containerID()).thenReturn(ContainerID.valueOf(containerID));
- when(c.getState()).thenReturn(HddsProtos.LifeCycleState.DELETED);
- return c;
- }
-
- /**
- * This is a simple implementation of PlacementPolicy, so that when
- * validateContainerPlacement() is called, by default it will return a value
- * placement object. To get an invalid placement object, simply pass a UUID
- * of a datanode via setMisRepWhenDnPresent. If a DN with that UUID is passed
- * to validateContainerPlacement, then it will return an invalid placement.
- */
- private static class MockPlacementPolicy implements
- PlacementPolicy {
-
- private UUID misRepWhenDnPresent = null;
-
- public void setMisRepWhenDnPresent(UUID dn) {
- misRepWhenDnPresent = dn;
- }
-
- @Override
- public List chooseDatanodes(
- List usedNodes, List excludedNodes,
- List favoredNodes,
- int nodesRequired, long metadataSizeRequired, long dataSizeRequired)
- throws IOException {
- return null;
- }
-
- @Override
- public ContainerPlacementStatus validateContainerPlacement(
- List dns, int replicas) {
- if (misRepWhenDnPresent != null && isDnPresent(dns)) {
- return new ContainerPlacementStatusDefault(1, 2, 3);
- } else {
- return new ContainerPlacementStatusDefault(1, 1, 1);
- }
- }
-
- @Override
- public Set replicasToCopyToFixMisreplication(
- Map replicas) {
- return Collections.emptySet();
- }
-
- @Override
- public Set replicasToRemoveToFixOverreplication(
- Set replicas, int expectedCountPerUniqueReplica) {
- return null;
- }
-
- private boolean isDnPresent(List dns) {
- for (DatanodeDetails dn : dns) {
- if (misRepWhenDnPresent != null
- && dn.getUuid().equals(misRepWhenDnPresent)) {
- return true;
- }
- }
- return false;
- }
- }
-
}
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskRecordGenerator.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskRecordGenerator.java
deleted file mode 100644
index 9e8b3905a58a..000000000000
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskRecordGenerator.java
+++ /dev/null
@@ -1,710 +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.fsck;
-
-import static org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
-import static org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto.State.CLOSED;
-import static org.apache.hadoop.ozone.recon.ReconConstants.CONTAINER_COUNT;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyList;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.apache.hadoop.hdds.client.RatisReplicationConfig;
-import org.apache.hadoop.hdds.conf.OzoneConfiguration;
-import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
-import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
-import org.apache.hadoop.hdds.scm.PlacementPolicy;
-import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
-import org.apache.hadoop.hdds.scm.container.ContainerID;
-import org.apache.hadoop.hdds.scm.container.ContainerInfo;
-import org.apache.hadoop.hdds.scm.container.ContainerReplica;
-import org.apache.hadoop.hdds.scm.container.placement.algorithms.ContainerPlacementStatusDefault;
-import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
-import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates;
-import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers;
-import org.apache.ozone.recon.schema.generated.tables.records.UnhealthyContainersRecord;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Test to validate the ContainerHealthTask Record Generator creates the correct
- * records to store in the database.
- */
-public class TestContainerHealthTaskRecordGenerator {
- private static final Logger LOG =
- LoggerFactory.getLogger(TestContainerHealthTaskRecordGenerator.class);
- private PlacementPolicy placementPolicy;
- private ContainerInfo container;
- private ContainerInfo emptyContainer;
- private ReconContainerMetadataManager reconContainerMetadataManager;
- private static final OzoneConfiguration CONF = new OzoneConfiguration();
-
- @BeforeEach
- public void setup() throws IOException {
- placementPolicy = mock(PlacementPolicy.class);
- container = mock(ContainerInfo.class);
- emptyContainer = mock(ContainerInfo.class);
- reconContainerMetadataManager = mock(ReconContainerMetadataManager.class);
- when(container.getReplicationFactor())
- .thenReturn(HddsProtos.ReplicationFactor.THREE);
- when(container.getReplicationConfig())
- .thenReturn(
- RatisReplicationConfig
- .getInstance(HddsProtos.ReplicationFactor.THREE));
- when(container.getState()).thenReturn(HddsProtos.LifeCycleState.CLOSED);
- when(container.containerID()).thenReturn(ContainerID.valueOf(123456));
- when(container.getContainerID()).thenReturn((long)123456);
- when(reconContainerMetadataManager.getKeyCountForContainer(
- (long) 123456)).thenReturn(5L);
- when(emptyContainer.getReplicationFactor())
- .thenReturn(HddsProtos.ReplicationFactor.THREE);
- when(emptyContainer.getReplicationConfig())
- .thenReturn(
- RatisReplicationConfig
- .getInstance(HddsProtos.ReplicationFactor.THREE));
- when(emptyContainer.containerID()).thenReturn(ContainerID.valueOf(345678));
- when(emptyContainer.getContainerID()).thenReturn((long) 345678);
- when(placementPolicy.validateContainerPlacement(
- anyList(), anyInt()))
- .thenReturn(new ContainerPlacementStatusDefault(1, 1, 1));
- }
-
- @Test
- public void testMissingRecordRetained() {
- Set replicas = new HashSet<>();
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- // Missing record should be retained
- assertTrue(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, missingRecord()
- ));
- // Under / Over / Mis replicated should not be retained as if a container is
- // missing then it is not in any other category.
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, underReplicatedRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, overReplicatedRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, misReplicatedRecord()
- ));
-
- replicas = generateReplicas(container, CLOSED, CLOSED, CLOSED);
- status = new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, missingRecord()
- ));
- }
-
- @Test
- public void testEmptyMissingRecordNotInsertedButLogged() {
- // Create a container that is in EMPTY_MISSING state
- Set replicas = new HashSet<>();
- ContainerHealthStatus status = new ContainerHealthStatus(emptyContainer, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
-
- // Initialize stats map
- Map> unhealthyContainerStateStatsMap = new HashMap<>();
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Generate records for EMPTY_MISSING container
- List records = ContainerHealthTask.ContainerHealthRecords.generateUnhealthyRecords(
- status, (long) 345678, unhealthyContainerStateStatsMap);
-
- // Assert that no records are created for EMPTY_MISSING state
- assertEquals(0, records.size());
-
- // Assert that the EMPTY_MISSING state is logged
- assertEquals(1, unhealthyContainerStateStatsMap.get(UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- }
-
- @Test
- public void testNegativeSizeRecordNotInsertedButLogged() {
- // Simulate a container with NEGATIVE_SIZE state
- when(container.getUsedBytes()).thenReturn(-10L); // Negative size
- Set replicas = generateReplicas(container, CLOSED, CLOSED);
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy, reconContainerMetadataManager, CONF);
-
- // Initialize stats map
- Map>
- unhealthyContainerStateStatsMap = new HashMap<>();
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Generate records for NEGATIVE_SIZE container
- List records =
- ContainerHealthTask.ContainerHealthRecords.generateUnhealthyRecords(
- status, (long) 123456, unhealthyContainerStateStatsMap);
-
- // Assert that none of the records are for negative.
- records.forEach(record -> assertNotEquals(
- UnHealthyContainerStates.NEGATIVE_SIZE.toString(), record.getContainerState()));
-
-
- // Assert that the NEGATIVE_SIZE state is logged
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.NEGATIVE_SIZE).getOrDefault(CONTAINER_COUNT, 0L));
- }
-
- @Test
- public void testUnderReplicatedRecordRetainedAndUpdated() {
- // under replicated container
- Set replicas =
- generateReplicas(container, CLOSED, CLOSED);
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
-
- UnhealthyContainersRecord rec = underReplicatedRecord();
- assertTrue(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, rec));
- // The record actual count should be updated from 1 -> 2
- assertEquals(2, rec.getActualReplicaCount().intValue());
- assertEquals(1, rec.getReplicaDelta().intValue());
-
- // Missing / Over / Mis replicated should not be retained
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, missingRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, overReplicatedRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, misReplicatedRecord()
- ));
-
- // Container is now replicated OK - should be removed.
- replicas = generateReplicas(container, CLOSED, CLOSED, CLOSED);
- status = new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, rec));
- }
-
- @Test
- public void testOverReplicatedRecordRetainedAndUpdated() {
- // under replicated container
- Set replicas =
- generateReplicas(container, CLOSED, CLOSED, CLOSED, CLOSED);
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
-
- UnhealthyContainersRecord rec = overReplicatedRecord();
- assertTrue(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, rec));
- // The record actual count should be updated from 5 -> 4
- assertEquals(4, rec.getActualReplicaCount().intValue());
- assertEquals(-1, rec.getReplicaDelta().intValue());
-
- // Missing / Over / Mis replicated should not be retained
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, missingRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, underReplicatedRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, misReplicatedRecord()
- ));
-
- // Container is now replicated OK - should be removed.
- replicas = generateReplicas(container, CLOSED, CLOSED, CLOSED);
- status = new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, rec));
- }
-
- @Test
- public void testMisReplicatedRecordRetainedAndUpdated() {
- // under replicated container
- Set replicas =
- generateReplicas(container, CLOSED, CLOSED, CLOSED);
- when(placementPolicy.validateContainerPlacement(
- anyList(), anyInt()))
- .thenReturn(new ContainerPlacementStatusDefault(2, 3, 5));
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
-
- UnhealthyContainersRecord rec = misReplicatedRecord();
- assertTrue(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, rec));
- // The record actual count should be updated from 1 -> 2
- assertEquals(2, rec.getActualReplicaCount().intValue());
- assertEquals(1, rec.getReplicaDelta().intValue());
- assertNotNull(rec.getReason());
-
- // Missing / Over / Mis replicated should not be retained
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, missingRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, underReplicatedRecord()
- ));
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, overReplicatedRecord()
- ));
-
- // Container is now placed OK - should be removed.
- when(placementPolicy.validateContainerPlacement(
- anyList(), anyInt()))
- .thenReturn(new ContainerPlacementStatusDefault(3, 3, 5));
- status = new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- assertFalse(ContainerHealthTask.ContainerHealthRecords
- .retainOrUpdateRecord(status, rec));
- }
-
- @Test
- @SuppressWarnings("checkstyle:methodlength")
- public void testCorrectRecordsGenerated() {
- Set replicas =
- generateReplicas(container, CLOSED, CLOSED, CLOSED);
- Map>
- unhealthyContainerStateStatsMap =
- new HashMap<>();
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
- // HEALTHY container - no records generated.
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- List records =
- ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(0, records.size());
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Over-replicated - expect 1 over replicated record
- replicas =
- generateReplicas(container, CLOSED, CLOSED, CLOSED, CLOSED, CLOSED);
- status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(1, records.size());
- UnhealthyContainers rec = records.get(0);
- assertEquals(UnHealthyContainerStates.OVER_REPLICATED.toString(),
- rec.getContainerState());
- assertEquals(3, rec.getExpectedReplicaCount().intValue());
- assertEquals(5, rec.getActualReplicaCount().intValue());
- assertEquals(-2, rec.getReplicaDelta().intValue());
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Replica mismatch
- replicas = generateMismatchedReplicas(container, CLOSED, CLOSED, CLOSED);
- status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(1, records.size());
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.REPLICA_MISMATCH)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Same data checksum replicas
- replicas = generateReplicas(container, CLOSED, CLOSED, CLOSED);
- status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(0, records.size());
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.REPLICA_MISMATCH)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Under and Mis Replicated - expect 2 records - mis and under replicated
- replicas =
- generateReplicas(container, CLOSED, CLOSED);
- when(placementPolicy.validateContainerPlacement(
- anyList(), anyInt()))
- .thenReturn(new ContainerPlacementStatusDefault(1, 2, 5));
- status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(2, records.size());
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- rec = findRecordForState(records, UnHealthyContainerStates.MIS_REPLICATED);
- assertEquals(UnHealthyContainerStates.MIS_REPLICATED.toString(),
- rec.getContainerState());
- assertEquals(2, rec.getExpectedReplicaCount().intValue());
- assertEquals(1, rec.getActualReplicaCount().intValue());
- assertEquals(1, rec.getReplicaDelta().intValue());
- assertNotNull(rec.getReason());
-
- rec = findRecordForState(records,
- UnHealthyContainerStates.UNDER_REPLICATED);
- assertEquals(UnHealthyContainerStates.UNDER_REPLICATED.toString(),
- rec.getContainerState());
- assertEquals(3, rec.getExpectedReplicaCount().intValue());
- assertEquals(2, rec.getActualReplicaCount().intValue());
- assertEquals(1, rec.getReplicaDelta().intValue());
-
- // Missing Record - expect just a single missing record even though
- // it is mis-replicated too
- replicas.clear();
- when(placementPolicy.validateContainerPlacement(
- anyList(), anyInt()))
- .thenReturn(new ContainerPlacementStatusDefault(1, 2, 5));
- status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(1, records.size());
- rec = records.get(0);
- assertEquals(UnHealthyContainerStates.MISSING.toString(),
- rec.getContainerState());
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- status =
- new ContainerHealthStatus(emptyContainer, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, (long) 345678,
- unhealthyContainerStateStatsMap);
-
- assertEquals(3, rec.getExpectedReplicaCount().intValue());
- assertEquals(0, rec.getActualReplicaCount().intValue());
- assertEquals(3, rec.getReplicaDelta().intValue());
-
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- unhealthyContainerStateStatsMap.clear();
- }
-
- @Test
- public void testRecordNotGeneratedIfAlreadyExists() {
- Map>
- unhealthyContainerStateStatsMap =
- new HashMap<>();
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
- Set existingRec = new HashSet<>();
-
- // Over-replicated
- Set replicas = generateReplicas(
- container, CLOSED, CLOSED, CLOSED, CLOSED, CLOSED);
- ContainerHealthStatus status =
- new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- List records =
- ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, existingRec, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(1, records.size());
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Missing
- replicas.clear();
- status = new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, existingRec, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(1, records.size());
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- initializeUnhealthyContainerStateStatsMap(unhealthyContainerStateStatsMap);
-
- // Under and Mis-Replicated
- replicas = generateReplicas(container, CLOSED, CLOSED);
- when(placementPolicy.validateContainerPlacement(
- anyList(), anyInt()))
- .thenReturn(new ContainerPlacementStatusDefault(1, 2, 5));
- status = new ContainerHealthStatus(container, replicas, placementPolicy,
- reconContainerMetadataManager, CONF);
- records = ContainerHealthTask.ContainerHealthRecords
- .generateUnhealthyRecords(status, existingRec, (long) 1234567,
- unhealthyContainerStateStatsMap);
- assertEquals(2, records.size());
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.EMPTY_MISSING)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(0, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.OVER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.UNDER_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
- assertEquals(1, unhealthyContainerStateStatsMap.get(
- UnHealthyContainerStates.MIS_REPLICATED)
- .getOrDefault(CONTAINER_COUNT, 0L));
-
- logUnhealthyContainerStats(unhealthyContainerStateStatsMap);
- unhealthyContainerStateStatsMap.clear();
- }
-
- private UnhealthyContainers findRecordForState(
- List recs, UnHealthyContainerStates state) {
- for (UnhealthyContainers r : recs) {
- if (r.getContainerState().equals(state.toString())) {
- return r;
- }
- }
- return null;
- }
-
- private UnhealthyContainersRecord missingRecord() {
- return new UnhealthyContainersRecord(container.containerID().getId(),
- UnHealthyContainerStates.MISSING.toString(), 10L,
- 3, 0, 3, null);
- }
-
- private UnhealthyContainersRecord underReplicatedRecord() {
- return new UnhealthyContainersRecord(container.containerID().getId(),
- UnHealthyContainerStates.UNDER_REPLICATED.toString(),
- 10L, 3, 1, 2, null);
- }
-
- private UnhealthyContainersRecord overReplicatedRecord() {
- return new UnhealthyContainersRecord(container.containerID().getId(),
- UnHealthyContainerStates.OVER_REPLICATED.toString(), 10L,
- 3, 5, -2, null);
- }
-
- private UnhealthyContainersRecord misReplicatedRecord() {
- return new UnhealthyContainersRecord(container.containerID().getId(),
- UnHealthyContainerStates.MIS_REPLICATED.toString(), 10L,
- 3, 1, 2, "should be on 1 more rack");
- }
-
- private Set generateReplicas(ContainerInfo cont,
- ContainerReplicaProto.State...states) {
- Set replicas = new HashSet<>();
- for (ContainerReplicaProto.State s : states) {
- replicas.add(new ContainerReplica.ContainerReplicaBuilder()
- .setContainerID(cont.containerID())
- .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails())
- .setChecksums(ContainerChecksums.of(1234L, 0L))
- .setContainerState(s)
- .build());
- }
- return replicas;
- }
-
- private Set generateMismatchedReplicas(ContainerInfo cont,
- ContainerReplicaProto.State...states) {
- Set replicas = new HashSet<>();
- long checksum = 1234L;
- for (ContainerReplicaProto.State s : states) {
- replicas.add(new ContainerReplica.ContainerReplicaBuilder()
- .setContainerID(cont.containerID())
- .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails())
- .setContainerState(s)
- .setChecksums(ContainerChecksums.of(checksum, 0L))
- .build());
- checksum++;
- }
- return replicas;
- }
-
- private void initializeUnhealthyContainerStateStatsMap(
- Map>
- unhealthyContainerStateStatsMap) {
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.MISSING, new HashMap<>());
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.EMPTY_MISSING, new HashMap<>());
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.UNDER_REPLICATED, new HashMap<>());
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.OVER_REPLICATED, new HashMap<>());
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.MIS_REPLICATED, new HashMap<>());
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.NEGATIVE_SIZE, new HashMap<>());
- unhealthyContainerStateStatsMap.put(
- UnHealthyContainerStates.REPLICA_MISMATCH, new HashMap<>());
- }
-
- private void logUnhealthyContainerStats(
- Map>
- unhealthyContainerStateStatsMap) {
- // If any EMPTY_MISSING containers, then it is possible that such
- // containers got stuck in the closing state which never got
- // any replicas created on the datanodes. In this case, we log it as
- // EMPTY_MISSING containers, but dont add it to the unhealthy container table.
- unhealthyContainerStateStatsMap.entrySet().forEach(stateEntry -> {
- UnHealthyContainerStates unhealthyContainerState = stateEntry.getKey();
- Map containerStateStatsMap = stateEntry.getValue();
- StringBuilder logMsgBuilder =
- new StringBuilder(unhealthyContainerState.toString());
- logMsgBuilder.append(" Container State Stats: \n\t");
- containerStateStatsMap.entrySet().forEach(statsEntry -> {
- logMsgBuilder.append(statsEntry.getKey());
- logMsgBuilder.append(" -> ");
- logMsgBuilder.append(statsEntry.getValue());
- logMsgBuilder.append(" , ");
- });
- LOG.info(logMsgBuilder.toString());
- });
- }
-}
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestReconReplicationManager.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestReconReplicationManager.java
new file mode 100644
index 000000000000..64867279169c
--- /dev/null
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestReconReplicationManager.java
@@ -0,0 +1,515 @@
+/*
+ * 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.fsck;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Clock;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.PlacementPolicy;
+import org.apache.hadoop.hdds.scm.container.ContainerHealthState;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerManager;
+import org.apache.hadoop.hdds.scm.container.ContainerReplica;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.hdds.scm.container.replication.ContainerReplicaOp;
+import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager;
+import org.apache.hadoop.hdds.scm.container.replication.ReplicationQueue;
+import org.apache.hadoop.hdds.scm.ha.SCMContext;
+import org.apache.hadoop.hdds.scm.node.NodeManager;
+import org.apache.hadoop.hdds.server.events.EventQueue;
+import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
+import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
+import org.apache.ozone.recon.schema.ContainerSchemaDefinition;
+import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates;
+import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersDao;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Smoke tests for ReconReplicationManager Local ReplicationManager.
+ *
+ * These tests verify that:
+ * 1. ReconReplicationManager can be instantiated properly
+ * 2. processAll() runs without errors
+ * 3. Database operations work correctly
+ * 4. It doesn't rely on RPC calls to SCM
+ *
+ * Note: Detailed health state testing requires integration tests with real
+ * ContainerManager, PlacementPolicy, and NodeManager implementations, as the
+ * health check logic in SCM's ReplicationManager is complex and depends on
+ * many factors beyond simple mocking.
+ */
+public class TestReconReplicationManager extends AbstractReconSqlDBTest {
+
+ private ContainerHealthSchemaManager schemaManagerV2;
+ private UnhealthyContainersDao dao;
+ private ContainerManager containerManager;
+ private ReconReplicationManager reconRM;
+
+ public TestReconReplicationManager() {
+ super();
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ dao = getDao(UnhealthyContainersDao.class);
+ schemaManagerV2 = new ContainerHealthSchemaManager(
+ getSchemaDefinition(ContainerSchemaDefinition.class));
+
+ containerManager = mock(ContainerManager.class);
+ PlacementPolicy placementPolicy = mock(PlacementPolicy.class);
+ SCMContext scmContext = mock(SCMContext.class);
+ NodeManager nodeManager = mock(NodeManager.class);
+
+ // Mock SCM context to allow processing
+ when(scmContext.isLeader()).thenReturn(true);
+ when(scmContext.isInSafeMode()).thenReturn(false);
+
+ // Create ReconReplicationManager
+ ReconReplicationManager.InitContext initContext =
+ ReconReplicationManager.InitContext.newBuilder()
+ .setRmConf(new ReplicationManager.ReplicationManagerConfiguration())
+ .setConf(new OzoneConfiguration())
+ .setContainerManager(containerManager)
+ .setRatisContainerPlacement(placementPolicy)
+ .setEcContainerPlacement(placementPolicy)
+ .setEventPublisher(new EventQueue())
+ .setScmContext(scmContext)
+ .setNodeManager(nodeManager)
+ .setClock(Clock.system(ZoneId.systemDefault()))
+ .build();
+
+ reconRM = new ReconReplicationManager(initContext, schemaManagerV2);
+ }
+
+ @Test
+ public void testProcessAllStoresEmptyMissingAndNegativeSizeRecords()
+ throws Exception {
+ final long emptyMissingContainerId = 101L;
+ final long negativeSizeContainerId = 202L;
+
+ ContainerInfo emptyMissingContainer = mockContainerInfo(
+ emptyMissingContainerId, 0, 1024L, 3);
+ ContainerInfo negativeSizeContainer = mockContainerInfo(
+ negativeSizeContainerId, 7, -1L, 3);
+ List containers = new ArrayList<>();
+ containers.add(emptyMissingContainer);
+ containers.add(negativeSizeContainer);
+
+ Set emptyReplicas = Collections.emptySet();
+ Set underReplicatedReplicas = new HashSet<>();
+ underReplicatedReplicas.add(mock(ContainerReplica.class));
+ underReplicatedReplicas.add(mock(ContainerReplica.class));
+
+ when(containerManager.getContainers()).thenReturn(containers);
+ when(containerManager.getContainer(ContainerID.valueOf(emptyMissingContainerId)))
+ .thenReturn(emptyMissingContainer);
+ when(containerManager.getContainer(ContainerID.valueOf(negativeSizeContainerId)))
+ .thenReturn(negativeSizeContainer);
+ when(containerManager.getContainerReplicas(ContainerID.valueOf(emptyMissingContainerId)))
+ .thenReturn(emptyReplicas);
+ when(containerManager.getContainerReplicas(ContainerID.valueOf(negativeSizeContainerId)))
+ .thenReturn(underReplicatedReplicas);
+
+ // Deterministically inject health states for this test to verify DB writes.
+ reconRM = createStateInjectingReconRM(
+ emptyMissingContainerId, negativeSizeContainerId);
+ reconRM.processAll();
+
+ List emptyMissing =
+ schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.EMPTY_MISSING, 0, 0, 100);
+ assertEquals(1, emptyMissing.size());
+ assertEquals(emptyMissingContainerId, emptyMissing.get(0).getContainerId());
+
+ List negativeSize =
+ schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.NEGATIVE_SIZE, 0, 0, 100);
+ assertEquals(1, negativeSize.size());
+ assertEquals(negativeSizeContainerId, negativeSize.get(0).getContainerId());
+ }
+
+ @Test
+ public void testProcessAllStoresAllPrimaryV2States() throws Exception {
+ final long missingContainerId = 301L;
+ final long underReplicatedContainerId = 302L;
+ final long overReplicatedContainerId = 303L;
+ final long misReplicatedContainerId = 304L;
+ final long mismatchContainerId = 305L;
+
+ List containers = Arrays.asList(
+ mockContainerInfo(missingContainerId, 10, 1024L, 3),
+ mockContainerInfo(underReplicatedContainerId, 5, 1024L, 3),
+ mockContainerInfo(overReplicatedContainerId, 5, 1024L, 3),
+ mockContainerInfo(misReplicatedContainerId, 5, 1024L, 3),
+ mockContainerInfo(mismatchContainerId, 5, 1024L, 3));
+ when(containerManager.getContainers()).thenReturn(containers);
+
+ Map> replicasByContainer = new HashMap<>();
+ replicasByContainer.put(missingContainerId, Collections.emptySet());
+ replicasByContainer.put(underReplicatedContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L));
+ replicasByContainer.put(overReplicatedContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L, 1000L, 1000L));
+ replicasByContainer.put(misReplicatedContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L, 1000L));
+ replicasByContainer.put(mismatchContainerId,
+ setOfMockReplicasWithChecksums(1000L, 2000L, 3000L));
+
+ Map stateByContainer = new HashMap<>();
+ stateByContainer.put(missingContainerId, ContainerHealthState.MISSING);
+ stateByContainer.put(underReplicatedContainerId,
+ ContainerHealthState.UNDER_REPLICATED);
+ stateByContainer.put(overReplicatedContainerId,
+ ContainerHealthState.OVER_REPLICATED);
+ stateByContainer.put(misReplicatedContainerId,
+ ContainerHealthState.MIS_REPLICATED);
+
+ for (ContainerInfo container : containers) {
+ long containerId = container.getContainerID();
+ when(containerManager.getContainer(ContainerID.valueOf(containerId)))
+ .thenReturn(container);
+ when(containerManager.getContainerReplicas(ContainerID.valueOf(containerId)))
+ .thenReturn(replicasByContainer.get(containerId));
+ }
+
+ reconRM = createStateInjectingReconRM(stateByContainer);
+ reconRM.processAll();
+
+ assertEquals(1, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.MISSING, 0, 0, 10).size());
+ assertEquals(1, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.UNDER_REPLICATED, 0, 0, 10).size());
+ assertEquals(1, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.OVER_REPLICATED, 0, 0, 10).size());
+ assertEquals(1, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.MIS_REPLICATED, 0, 0, 10).size());
+ assertEquals(1, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.REPLICA_MISMATCH, 0, 0, 10).size());
+ }
+
+ @Test
+ public void testProcessAllMapsCompositeScmStatesToBaseStates() throws Exception {
+ final long unhealthyUnderContainerId = 401L;
+ final long unhealthyOverContainerId = 402L;
+ final long qcStuckMissingContainerId = 403L;
+ final long qcStuckUnderContainerId = 404L;
+ final long qcStuckOverContainerId = 405L;
+ final long missingUnderContainerId = 406L;
+
+ List containers = Arrays.asList(
+ mockContainerInfo(unhealthyUnderContainerId, 5, 1024L, 3),
+ mockContainerInfo(unhealthyOverContainerId, 5, 1024L, 3),
+ mockContainerInfo(qcStuckMissingContainerId, 5, 1024L, 3),
+ mockContainerInfo(qcStuckUnderContainerId, 5, 1024L, 3),
+ mockContainerInfo(qcStuckOverContainerId, 5, 1024L, 3),
+ mockContainerInfo(missingUnderContainerId, 5, 1024L, 3));
+ when(containerManager.getContainers()).thenReturn(containers);
+
+ Map> replicasByContainer = new HashMap<>();
+ replicasByContainer.put(unhealthyUnderContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L));
+ replicasByContainer.put(unhealthyOverContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L, 1000L, 1000L));
+ replicasByContainer.put(qcStuckMissingContainerId, Collections.emptySet());
+ replicasByContainer.put(qcStuckUnderContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L));
+ replicasByContainer.put(qcStuckOverContainerId,
+ setOfMockReplicasWithChecksums(1000L, 1000L, 1000L, 1000L));
+ replicasByContainer.put(missingUnderContainerId, Collections.emptySet());
+
+ Map stateByContainer = new HashMap<>();
+ stateByContainer.put(unhealthyUnderContainerId,
+ ContainerHealthState.UNHEALTHY_UNDER_REPLICATED);
+ stateByContainer.put(unhealthyOverContainerId,
+ ContainerHealthState.UNHEALTHY_OVER_REPLICATED);
+ stateByContainer.put(qcStuckMissingContainerId,
+ ContainerHealthState.QUASI_CLOSED_STUCK_MISSING);
+ stateByContainer.put(qcStuckUnderContainerId,
+ ContainerHealthState.QUASI_CLOSED_STUCK_UNDER_REPLICATED);
+ stateByContainer.put(qcStuckOverContainerId,
+ ContainerHealthState.QUASI_CLOSED_STUCK_OVER_REPLICATED);
+ stateByContainer.put(missingUnderContainerId,
+ ContainerHealthState.MISSING_UNDER_REPLICATED);
+
+ for (ContainerInfo container : containers) {
+ long containerId = container.getContainerID();
+ when(containerManager.getContainer(ContainerID.valueOf(containerId)))
+ .thenReturn(container);
+ when(containerManager.getContainerReplicas(ContainerID.valueOf(containerId)))
+ .thenReturn(replicasByContainer.get(containerId));
+ }
+
+ reconRM = createStateInjectingReconRM(stateByContainer);
+ reconRM.processAll();
+
+ List missing =
+ schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.MISSING, 0, 0, 20);
+ List underReplicated =
+ schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.UNDER_REPLICATED, 0, 0, 20);
+ List overReplicated =
+ schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.OVER_REPLICATED, 0, 0, 20);
+
+ assertEquals(2, missing.size());
+ assertEquals(3, underReplicated.size());
+ assertEquals(2, overReplicated.size());
+ assertEquals(0, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.EMPTY_MISSING, 0, 0, 20).size());
+
+ assertTrue(containsContainerId(missing, missingUnderContainerId));
+ assertTrue(containsContainerId(underReplicated, missingUnderContainerId));
+ }
+
+ @Test
+ public void testProcessAllSkipsUnsupportedScmStateWithoutDbViolation()
+ throws Exception {
+ final long unsupportedStateContainerId = 407L;
+
+ ContainerInfo container = mockContainerInfo(
+ unsupportedStateContainerId, 5, 1024L, 3);
+ when(containerManager.getContainers()).thenReturn(Collections.singletonList(container));
+ when(containerManager.getContainer(ContainerID.valueOf(unsupportedStateContainerId)))
+ .thenReturn(container);
+ Set replicas =
+ setOfMockReplicasWithChecksums(1000L, 1000L, 1000L);
+ when(containerManager.getContainerReplicas(ContainerID.valueOf(unsupportedStateContainerId)))
+ .thenReturn(replicas);
+
+ Map stateByContainer = new HashMap<>();
+ // This SCM state has no matching value in Recon's allowed DB enum.
+ stateByContainer.put(unsupportedStateContainerId, ContainerHealthState.UNHEALTHY);
+
+ reconRM = createStateInjectingReconRM(stateByContainer);
+ reconRM.processAll();
+
+ assertEquals(0, dao.count());
+ assertEquals(0, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.MISSING, 0, 0, 10).size());
+ assertEquals(0, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.UNDER_REPLICATED, 0, 0, 10).size());
+ assertEquals(0, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.OVER_REPLICATED, 0, 0, 10).size());
+ assertEquals(0, schemaManagerV2.getUnhealthyContainers(
+ UnHealthyContainerStates.MIS_REPLICATED, 0, 0, 10).size());
+ }
+
+ @Test
+ public void testReconReplicationManagerCreation() {
+ // Verify ReconReplicationManager was created successfully
+ assertNotNull(reconRM);
+ }
+
+ @Test
+ public void testProcessAllWithNoContainers() throws Exception {
+ // Setup: No containers
+ when(containerManager.getContainers()).thenReturn(new ArrayList<>());
+
+ // Execute - should not throw any exceptions
+ reconRM.processAll();
+
+ // Verify: Method completed without errors
+ // No records should be in database since there are no containers
+ assertEquals(0, dao.count());
+ }
+
+ @Test
+ public void testProcessAllRunsMultipleTimes() throws Exception {
+ // Setup: No containers
+ when(containerManager.getContainers()).thenReturn(new ArrayList<>());
+
+ // Execute multiple times - verify it's idempotent
+ reconRM.processAll();
+ assertEquals(0, dao.count());
+
+ reconRM.processAll();
+ assertEquals(0, dao.count());
+
+ reconRM.processAll();
+ assertEquals(0, dao.count());
+ }
+
+ @Test
+ public void testDatabaseOperationsWork() throws Exception {
+ // This test verifies that the database schema and operations work
+ // Setup: No containers
+ when(containerManager.getContainers()).thenReturn(new ArrayList<>());
+
+ // Insert a test record directly
+ org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers record =
+ new org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers();
+ record.setContainerId(999L);
+ record.setContainerState("UNDER_REPLICATED");
+ record.setInStateSince(System.currentTimeMillis());
+ record.setExpectedReplicaCount(3);
+ record.setActualReplicaCount(2);
+ record.setReplicaDelta(1);
+ record.setReason("Test record");
+ dao.insert(record);
+
+ assertEquals(1, dao.count());
+
+ // Run processAll - the old record should persist because container 999
+ // is not in containerManager (only records for containers being processed are cleaned up)
+ reconRM.processAll();
+
+ // Verify the old record persists (correct behavior - containers not in
+ // containerManager should keep their records as they might indicate missing containers)
+ assertEquals(1, dao.count());
+ }
+
+ @Test
+ public void testSchemaManagerIntegration() {
+ // Verify the schema manager is properly integrated
+ assertNotNull(schemaManagerV2);
+
+ // Verify we can perform batch operations
+ // (This is a smoke test to ensure the wiring is correct)
+ schemaManagerV2.batchDeleteSCMStatesForContainers(new ArrayList<>());
+ schemaManagerV2.insertUnhealthyContainerRecords(new ArrayList<>());
+
+ // No assertion needed - just verify no exceptions thrown
+ }
+
+ private ContainerInfo mockContainerInfo(long containerId, long numberOfKeys,
+ long usedBytes, int requiredNodes) {
+ ContainerInfo containerInfo = mock(ContainerInfo.class);
+ ReplicationConfig replicationConfig = mock(ReplicationConfig.class);
+ AtomicReference healthStateRef =
+ new AtomicReference<>(ContainerHealthState.HEALTHY);
+
+ when(containerInfo.getContainerID()).thenReturn(containerId);
+ when(containerInfo.containerID()).thenReturn(ContainerID.valueOf(containerId));
+ when(containerInfo.getNumberOfKeys()).thenReturn(numberOfKeys);
+ when(containerInfo.getUsedBytes()).thenReturn(usedBytes);
+ when(containerInfo.getReplicationConfig()).thenReturn(replicationConfig);
+ when(containerInfo.getState()).thenReturn(HddsProtos.LifeCycleState.CLOSED);
+ when(containerInfo.getHealthState()).thenAnswer(invocation -> healthStateRef.get());
+ doAnswer(invocation -> {
+ healthStateRef.set(invocation.getArgument(0));
+ return null;
+ }).when(containerInfo).setHealthState(
+ org.mockito.ArgumentMatchers.any(ContainerHealthState.class));
+ when(replicationConfig.getRequiredNodes()).thenReturn(requiredNodes);
+ return containerInfo;
+ }
+
+ private ReconReplicationManager createStateInjectingReconRM(
+ long emptyMissingContainerId, long negativeSizeContainerId)
+ throws Exception {
+ Map stateByContainer = new HashMap<>();
+ stateByContainer.put(emptyMissingContainerId, ContainerHealthState.MISSING);
+ stateByContainer.put(negativeSizeContainerId,
+ ContainerHealthState.UNDER_REPLICATED);
+ return createStateInjectingReconRM(stateByContainer);
+ }
+
+ private ReconReplicationManager createStateInjectingReconRM(
+ Map stateByContainer) throws Exception {
+ PlacementPolicy placementPolicy = mock(PlacementPolicy.class);
+ SCMContext scmContext = mock(SCMContext.class);
+ NodeManager nodeManager = mock(NodeManager.class);
+ when(scmContext.isLeader()).thenReturn(true);
+ when(scmContext.isInSafeMode()).thenReturn(false);
+
+ ReconReplicationManager.InitContext initContext =
+ ReconReplicationManager.InitContext.newBuilder()
+ .setRmConf(new ReplicationManager.ReplicationManagerConfiguration())
+ .setConf(new OzoneConfiguration())
+ .setContainerManager(containerManager)
+ .setRatisContainerPlacement(placementPolicy)
+ .setEcContainerPlacement(placementPolicy)
+ .setEventPublisher(new EventQueue())
+ .setScmContext(scmContext)
+ .setNodeManager(nodeManager)
+ .setClock(Clock.system(ZoneId.systemDefault()))
+ .build();
+
+ return new ReconReplicationManager(initContext, schemaManagerV2) {
+ @Override
+ protected boolean processContainer(ContainerInfo containerInfo,
+ Set replicas, List pendingOps,
+ ReplicationQueue repQueue, ReplicationManagerReport report,
+ boolean readOnly) {
+ ReconReplicationManagerReport reconReport =
+ (ReconReplicationManagerReport) report;
+ ContainerHealthState state =
+ stateByContainer.get(containerInfo.getContainerID());
+ if (state != null) {
+ reconReport.incrementAndSample(state, containerInfo);
+ containerInfo.setHealthState(state);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean processContainer(ContainerInfo containerInfo,
+ ReplicationQueue repQueue, ReplicationManagerReport report,
+ boolean readOnly) {
+ ReconReplicationManagerReport reconReport =
+ (ReconReplicationManagerReport) report;
+ ContainerHealthState state =
+ stateByContainer.get(containerInfo.getContainerID());
+ if (state != null) {
+ reconReport.incrementAndSample(state, containerInfo);
+ containerInfo.setHealthState(state);
+ return true;
+ }
+ return false;
+ }
+ };
+ }
+
+ private Set setOfMockReplicasWithChecksums(Long... checksums) {
+ Set replicas = new HashSet<>();
+ for (Long checksum : checksums) {
+ ContainerReplica replica = mock(ContainerReplica.class);
+ when(replica.getDataChecksum()).thenReturn(checksum);
+ replicas.add(replica);
+ }
+ return replicas;
+ }
+
+ private boolean containsContainerId(
+ List records,
+ long containerId) {
+ return records.stream().anyMatch(r -> r.getContainerId() == containerId);
+ }
+}
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/heatmap/TestHeatMapInfo.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/heatmap/TestHeatMapInfo.java
index 284fab39100e..d1904f6f8fab 100644
--- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/heatmap/TestHeatMapInfo.java
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/heatmap/TestHeatMapInfo.java
@@ -37,7 +37,6 @@
import org.apache.hadoop.ozone.recon.ReconTestInjector;
import org.apache.hadoop.ozone.recon.api.types.EntityMetaData;
import org.apache.hadoop.ozone.recon.api.types.EntityReadAccessHeatMapResponse;
-import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
@@ -79,7 +78,6 @@ private void initializeInjector() throws Exception {
.withContainerDB()
.addBinding(StorageContainerServiceProvider.class,
mock(StorageContainerServiceProviderImpl.class))
- .addBinding(ContainerHealthSchemaManager.class)
.build();
heatMapUtil = reconTestInjector.getInstance(HeatMapUtil.class);
auditRespStr = "{\n" +
diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestUnhealthyContainersDerbyPerformance.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestUnhealthyContainersDerbyPerformance.java
new file mode 100644
index 000000000000..5cc90e88409f
--- /dev/null
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestUnhealthyContainersDerbyPerformance.java
@@ -0,0 +1,764 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.recon.persistence;
+
+import static org.apache.ozone.recon.schema.generated.tables.UnhealthyContainersTable.UNHEALTHY_CONTAINERS;
+import static org.jooq.impl.DSL.count;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.ozone.recon.ReconControllerModule.ReconDaoBindingModule;
+import org.apache.hadoop.ozone.recon.ReconSchemaManager;
+import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest.DerbyDataSourceConfigurationProvider;
+import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager.UnhealthyContainerRecord;
+import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager.UnhealthyContainersSummary;
+import org.apache.ozone.recon.schema.ContainerSchemaDefinition;
+import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates;
+import org.apache.ozone.recon.schema.ReconSchemaGenerationModule;
+import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersDao;
+import org.jooq.DSLContext;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Performance benchmark for the UNHEALTHY_CONTAINERS Derby table at 1 million
+ * records scale.
+ *
+ * Data layout
+ *
+ * Container IDs : 1 – 200,000 (CONTAINER_ID_RANGE)
+ * States per ID : 5 (UNDER_REPLICATED, MISSING, OVER_REPLICATED,
+ * MIS_REPLICATED, EMPTY_MISSING)
+ * Total records : 200,000 × 5 = 1,000,000
+ * Primary key : (container_id, container_state) — unique per pair
+ * Index : idx_state_container_id on (container_state, container_id)
+ * composite index supports both aggregates (COUNT/GROUP BY
+ * on state prefix) and O(1)-per-page cursor pagination
+ *
+ *
+ * Performance settings applied in this test
+ *
+ * - Page cache: {@code derby.storage.pageCacheSize = 20000}
+ * (~80 MB of 4-KB pages) keeps hot B-tree nodes in memory, reducing
+ * filesystem reads even with the file-based Derby driver.
+ * - JDBC fetch size: set to {@value #READ_PAGE_SIZE} on each query
+ * so Derby pre-buffers a full page of rows per JDBC round-trip instead
+ * of the default 1-row-at-a-time fetch.
+ * - Large page size: {@value #READ_PAGE_SIZE} rows per SQL fetch
+ * reduces the number of SQL round-trips from 200 (@ 1 K rows) to 40
+ * (@ 5 K rows) per 200 K-row state scan.
+ * - Large delete chunks: {@value #DELETE_CHUNK_SIZE} IDs per
+ * DELETE statement reduces Derby plan-compilation overhead from 100
+ * statements to 20 for a 100 K-ID batch delete.
+ *
+ *
+ * What is measured
+ *
+ * - Bulk INSERT throughput – 1 M records via JOOQ batchInsert in
+ * chunks of 1,000 inside a single Derby transaction.
+ * - COUNT(*) by state – index-covered aggregate, one per state.
+ * - GROUP BY summary – single pass over the idx_container_state
+ * index to aggregate all states.
+ * - Paginated SELECT by state – cursor-style walk using
+ * minContainerId / maxContainerId to fetch the full 200 K rows of one
+ * state in pages of {@value #READ_PAGE_SIZE}, without loading all rows
+ * into the JVM heap at once.
+ * - Batch DELETE throughput – removes records for half the
+ * container IDs list covering all rows
+ * (200 K × 5 states = 1 M rows) via a single
+ * IN-clause DELETE.
+ *
+ *
+ * Design notes
+ *
+ * - Derby is an embedded, single-file Java database — not designed for
+ * production-scale workloads. Performance numbers here document its
+ * baseline behaviour and will flag regressions, but should not be
+ * compared with PostgreSQL / MySQL numbers.
+ * - Timing thresholds are deliberately generous (≈ 10× expected) to be
+ * stable on slow CI machines. Actual durations are always logged.
+ * - Uses {@code @TestInstance(PER_CLASS)} so database/schema setup is
+ * done once in {@code @BeforeAll}; test methods then exercise
+ * insert/replace/delete flows explicitly.
+ *
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class TestUnhealthyContainersDerbyPerformance {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(TestUnhealthyContainersDerbyPerformance.class);
+
+ // -----------------------------------------------------------------------
+ // Dataset constants
+ // -----------------------------------------------------------------------
+
+ /** Number of unique container IDs. Each ID appears in every TESTED_STATES. */
+ private static final int CONTAINER_ID_RANGE = 200_000;
+
+ /** States distributed across all container IDs. */
+ private static final List TESTED_STATES = Arrays.asList(
+ UnHealthyContainerStates.UNDER_REPLICATED,
+ UnHealthyContainerStates.MISSING,
+ UnHealthyContainerStates.OVER_REPLICATED,
+ UnHealthyContainerStates.MIS_REPLICATED,
+ UnHealthyContainerStates.EMPTY_MISSING);
+
+ /** Number of tested states (equals TESTED_STATES.size()). */
+ private static final int STATE_COUNT = 5;
+
+ /** Total records = CONTAINER_ID_RANGE × STATE_COUNT. */
+ private static final int TOTAL_RECORDS = CONTAINER_ID_RANGE * STATE_COUNT;
+
+ /**
+ * Number of containers inserted per transaction.
+ *
+ * Derby's WAL (Write-Ahead Log) must hold all uncommitted rows before
+ * a transaction commits. Inserting all 1 M rows in one transaction causes
+ * Derby to exhaust its log buffer and hang indefinitely. Committing in
+ * chunks of {@value} containers ({@value} × 5 states = 10,000 rows/tx)
+ * lets Derby flush the log after each commit, keeping each transaction
+ * fast and bounded in memory usage.
+ */
+ private static final int CONTAINERS_PER_TX = 2_000; // 2 000 × 5 = 10 000 rows/tx
+
+ /**
+ * Number of container IDs to pass per
+ * {@link ContainerHealthSchemaManager#batchDeleteSCMStatesForContainers}
+ * call in the delete test.
+ *
+ * {@code batchDeleteSCMStatesForContainers} now handles internal
+ * chunking at 1,000 IDs per SQL statement to stay within Derby's
+ * 64 KB generated-bytecode limit
+ * (ERROR XBCM4). This test-level constant controls how many IDs are
+ * accumulated before each call and should match that limit so the test
+ * exercises exactly one SQL DELETE per call.
+ */
+ private static final int DELETE_CHUNK_SIZE = 1_000;
+
+ /**
+ * Number of records returned per page in the paginated-read tests.
+ *
+ * 5,000 rows per page means only 40 SQL round-trips to scan 200,000
+ * records for a single state, compared to 200 trips at the old 1,000-row
+ * page size. Combined with {@code query.fetchSize(READ_PAGE_SIZE)} this
+ * cuts round-trip overhead by 80% while keeping per-page heap usage well
+ * below 1 MB.
+ */
+ private static final int READ_PAGE_SIZE = 5_000;
+
+ // -----------------------------------------------------------------------
+ // Performance thresholds (CI-safe; expected run times are 5–10× faster
+ // than the original file-based Derby baseline after the optimisations)
+ // -----------------------------------------------------------------------
+
+ /** Maximum acceptable time to insert all TOTAL_RECORDS into Derby. */
+ private static final long MAX_INSERT_SECONDS = 300;
+
+ /** Maximum acceptable time for a single COUNT(*)-by-state query. */
+ private static final long MAX_COUNT_BY_STATE_SECONDS = 30;
+
+ /** Maximum acceptable time for the GROUP-BY summary query. */
+ private static final long MAX_SUMMARY_SECONDS = 30;
+
+ /**
+ * Maximum acceptable time to page through all CONTAINER_ID_RANGE records
+ * of a single state using {@link #READ_PAGE_SIZE}-row pages.
+ */
+ private static final long MAX_PAGINATED_READ_SECONDS = 60;
+
+ /** Maximum acceptable time to batch-delete 1 M rows. */
+ private static final long MAX_DELETE_SECONDS = 180;
+ /** Maximum acceptable time for one atomic delete+insert replace cycle. */
+ private static final long MAX_ATOMIC_REPLACE_SECONDS = 300;
+
+ // -----------------------------------------------------------------------
+ // Infrastructure (shared for the life of this test class)
+ // -----------------------------------------------------------------------
+
+ private ContainerHealthSchemaManager schemaManager;
+ private UnhealthyContainersDao dao;
+ private ContainerSchemaDefinition schemaDefinition;
+
+ // -----------------------------------------------------------------------
+ // One-time setup: create Derby schema + insert 1 M records
+ // -----------------------------------------------------------------------
+
+ /**
+ * Initialises the embedded Derby database and creates the Recon schema.
+ * Data population is done in dedicated test methods.
+ *
+ * The {@code @TempDir} is injected as a method parameter rather
+ * than a class field. With {@code @TestInstance(PER_CLASS)}, a field-level
+ * {@code @TempDir} is populated by JUnit's {@code TempDirExtension} in its
+ * own {@code beforeAll} callback, which may run after the user's
+ * {@code @BeforeAll} — leaving it null when needed here. A method
+ * parameter is resolved by JUnit before the method body executes.
+ *
+ * Performance settings applied here
+ *
+ * - Page cache ({@code derby.storage.pageCacheSize = 20000}):
+ * ~80 MB of 4-KB B-tree pages resident in heap — covers the hot path
+ * for index scans on a 1-M-row table even with the file-based
+ * driver.
+ *
+ */
+ @BeforeAll
+ public void setUpDatabase(@TempDir Path tempDir) throws Exception {
+ LOG.info("=== Derby Performance Benchmark — Setup ===");
+ LOG.info("Dataset: {} states × {} container IDs = {} total records",
+ TESTED_STATES.size(), CONTAINER_ID_RANGE, TOTAL_RECORDS);
+
+ // Derby engine property — must be set before the first connection.
+ //
+ // pageCacheSize: number of 4-KB pages Derby keeps in its buffer pool.
+ // Default = 1,000 pages (4 MB) — far too small for a 1-M-row table.
+ // 20,000 pages = ~80 MB, enough to hold the full B-tree for both the
+ // primary-key index and the composite (state, container_id) index.
+ System.setProperty("derby.storage.pageCacheSize", "20000");
+
+ // ----- Guice wiring (mirrors AbstractReconSqlDBTest) -----
+ File configDir = Files.createDirectory(tempDir.resolve("Config")).toFile();
+ Provider configProvider =
+ new DerbyDataSourceConfigurationProvider(configDir);
+
+ Injector injector = Guice.createInjector(
+ new JooqPersistenceModule(configProvider),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(DataSourceConfiguration.class).toProvider(configProvider);
+ bind(ReconSchemaManager.class);
+ }
+ },
+ new ReconSchemaGenerationModule(),
+ new ReconDaoBindingModule());
+
+ injector.getInstance(ReconSchemaManager.class).createReconSchema();
+
+ dao = injector.getInstance(UnhealthyContainersDao.class);
+ schemaDefinition = injector.getInstance(ContainerSchemaDefinition.class);
+ schemaManager = new ContainerHealthSchemaManager(schemaDefinition);
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 1 — Batch INSERT performance for 1M records
+ // -----------------------------------------------------------------------
+
+ /**
+ * Inserts 1M records via batch operations and logs total time taken.
+ */
+ @Test
+ @Order(1)
+ public void testBatchInsertOneMillionRecords() {
+ int txCount = (int) Math.ceil((double) CONTAINER_ID_RANGE / CONTAINERS_PER_TX);
+ LOG.info("--- Test 1: Batch INSERT {} records ({} containers/tx, {} transactions) ---",
+ TOTAL_RECORDS, CONTAINERS_PER_TX, txCount);
+
+ long now = System.currentTimeMillis();
+ long start = System.nanoTime();
+
+ for (int startId = 1; startId <= CONTAINER_ID_RANGE; startId += CONTAINERS_PER_TX) {
+ int endId = Math.min(startId + CONTAINERS_PER_TX - 1, CONTAINER_ID_RANGE);
+ List chunk = generateRecordsForRange(startId, endId, now);
+ schemaManager.insertUnhealthyContainerRecords(chunk);
+ }
+
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ double throughput = (double) TOTAL_RECORDS / (elapsedMs / 1000.0);
+ LOG.info("Batch INSERT complete: {} records in {} ms ({} rec/sec, {} tx)",
+ TOTAL_RECORDS, elapsedMs, String.format("%.0f", throughput), txCount);
+
+ assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_INSERT_SECONDS),
+ String.format("INSERT took %d ms, exceeded %d s threshold",
+ elapsedMs, MAX_INSERT_SECONDS));
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 2 — Verify the inserted row count
+ // -----------------------------------------------------------------------
+
+ @Test
+ @Order(2)
+ public void testTotalInsertedRecordCountIsOneMillion() {
+ LOG.info("--- Test 2: Verify total row count = {} ---", TOTAL_RECORDS);
+
+ long countStart = System.nanoTime();
+ long totalCount = dao.count();
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - countStart);
+
+ LOG.info("COUNT(*) = {} rows in {} ms", totalCount, elapsedMs);
+
+ assertEquals(TOTAL_RECORDS, totalCount,
+ "Total row count must equal the number of inserted records");
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 2 — COUNT(*) by each state (exercises idx_container_state)
+ // -----------------------------------------------------------------------
+
+ /**
+ * Runs one {@code COUNT(*) WHERE container_state = ?} query per tested
+ * state. Because {@code container_state} is indexed these should be fast
+ * index-covered aggregates.
+ *
+ * Each state must have exactly {@value #CONTAINER_ID_RANGE} records.
+ */
+ @Test
+ @Order(3)
+ public void testCountByStatePerformanceUsesIndex() {
+ LOG.info("--- Test 3: COUNT(*) by state (index-covered, {} records each) ---",
+ CONTAINER_ID_RANGE);
+
+ DSLContext dsl = schemaDefinition.getDSLContext();
+
+ for (UnHealthyContainerStates state : TESTED_STATES) {
+ long start = System.nanoTime();
+ int stateCount = dsl
+ .select(count())
+ .from(UNHEALTHY_CONTAINERS)
+ .where(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString()))
+ .fetchOne(0, int.class);
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+ LOG.info(" COUNT({}) = {} rows in {} ms", state, stateCount, elapsedMs);
+
+ assertEquals(CONTAINER_ID_RANGE, stateCount,
+ "Expected " + CONTAINER_ID_RANGE + " records for state " + state);
+
+ assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_COUNT_BY_STATE_SECONDS),
+ String.format("COUNT for state %s took %d ms, exceeded %d s threshold",
+ state, elapsedMs, MAX_COUNT_BY_STATE_SECONDS));
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 3 — GROUP BY summary query
+ // -----------------------------------------------------------------------
+
+ /**
+ * Runs the {@link ContainerHealthSchemaManager#getUnhealthyContainersSummary()}
+ * GROUP-BY query over all 1 M rows, which represents a typical API request
+ * to populate the Recon UI dashboard.
+ *
+ * Expected result: {@value #STATE_COUNT} state groups, each with
+ * {@value #CONTAINER_ID_RANGE} records.
+ */
+ @Test
+ @Order(4)
+ public void testGroupBySummaryQueryPerformance() {
+ LOG.info("--- Test 4: GROUP BY summary over {} rows ---", TOTAL_RECORDS);
+
+ long start = System.nanoTime();
+ List summary =
+ schemaManager.getUnhealthyContainersSummary();
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+ LOG.info("GROUP BY summary: {} state groups returned in {} ms",
+ summary.size(), elapsedMs);
+ summary.forEach(s ->
+ LOG.info(" state={} count={}", s.getContainerState(), s.getCount()));
+
+ assertEquals(STATE_COUNT, summary.size(),
+ "Summary must contain one entry per tested state");
+
+ for (UnhealthyContainersSummary entry : summary) {
+ assertEquals(CONTAINER_ID_RANGE, entry.getCount(),
+ "Each state must have " + CONTAINER_ID_RANGE + " records in the summary");
+ }
+
+ assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_SUMMARY_SECONDS),
+ String.format("GROUP BY took %d ms, exceeded %d s threshold",
+ elapsedMs, MAX_SUMMARY_SECONDS));
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 4 — Paginated read (cursor walk through 200 K rows per state)
+ // -----------------------------------------------------------------------
+
+ /**
+ * Reads all {@value #CONTAINER_ID_RANGE} records of one state
+ * ({@code UNDER_REPLICATED}) by walking through them page-by-page using
+ * the {@code minContainerId} cursor parameter. This simulates the Recon
+ * UI pagination pattern without holding the full result-set in heap memory.
+ *
+ * The test asserts:
+ *
+ * - Total records seen across all pages equals {@value #CONTAINER_ID_RANGE}
+ * - All pages are fetched within {@value #MAX_PAGINATED_READ_SECONDS} seconds
+ * - Records are returned in ascending container-ID order
+ *
+ */
+ @Test
+ @Order(5)
+ public void testPaginatedReadByStatePerformance() {
+ UnHealthyContainerStates targetState = UnHealthyContainerStates.UNDER_REPLICATED;
+ LOG.info("--- Test 5: Paginated read of {} ({} records, page size {}) ---",
+ targetState, CONTAINER_ID_RANGE, READ_PAGE_SIZE);
+
+ int totalRead = 0;
+ int pageCount = 0;
+ long minContainerId = 0;
+ long lastContainerId = -1;
+ boolean orderedCorrectly = true;
+
+ long start = System.nanoTime();
+
+ while (true) {
+ List page =
+ schemaManager.getUnhealthyContainers(
+ targetState, minContainerId, 0, READ_PAGE_SIZE);
+
+ if (page.isEmpty()) {
+ break;
+ }
+
+ for (ContainerHealthSchemaManager.UnhealthyContainerRecord rec : page) {
+ if (rec.getContainerId() <= lastContainerId) {
+ orderedCorrectly = false;
+ }
+ lastContainerId = rec.getContainerId();
+ }
+
+ totalRead += page.size();
+ pageCount++;
+ minContainerId = page.get(page.size() - 1).getContainerId();
+
+ if (page.size() < READ_PAGE_SIZE) {
+ break;
+ }
+ }
+
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ double throughput = totalRead / Math.max(1.0, elapsedMs / 1000.0);
+
+ LOG.info("Paginated read: {} records in {} pages, {} ms ({} rec/sec)",
+ totalRead, pageCount, elapsedMs, String.format("%.0f", throughput));
+
+ assertEquals(CONTAINER_ID_RANGE, totalRead,
+ "Paginated read must return all " + CONTAINER_ID_RANGE + " records for " + targetState);
+
+ assertTrue(orderedCorrectly,
+ "Records must be returned in ascending container_id order");
+
+ assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_PAGINATED_READ_SECONDS),
+ String.format("Paginated read took %d ms, exceeded %d s threshold",
+ elapsedMs, MAX_PAGINATED_READ_SECONDS));
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 5 — Read all states sequentially (full 1 M record scan via pages)
+ // -----------------------------------------------------------------------
+
+ /**
+ * Pages through all records for every tested state sequentially, effectively
+ * reading all 1 million rows from Derby through the application layer.
+ * This measures aggregate read throughput across the entire dataset.
+ */
+ @Test
+ @Order(6)
+ public void testFullDatasetReadThroughputAllStates() {
+ LOG.info("--- Test 6: Full {} M record read (all states, paged) ---",
+ TOTAL_RECORDS / 1_000_000);
+
+ long totalStart = System.nanoTime();
+ Map countPerState =
+ new EnumMap<>(UnHealthyContainerStates.class);
+
+ for (UnHealthyContainerStates state : TESTED_STATES) {
+ long stateStart = System.nanoTime();
+ int stateTotal = 0;
+ long minId = 0;
+
+ while (true) {
+ List page =
+ schemaManager.getUnhealthyContainers(state, minId, 0, READ_PAGE_SIZE);
+ if (page.isEmpty()) {
+ break;
+ }
+ stateTotal += page.size();
+ minId = page.get(page.size() - 1).getContainerId();
+ if (page.size() < READ_PAGE_SIZE) {
+ break;
+ }
+ }
+
+ long stateElapsedMs =
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - stateStart);
+ countPerState.put(state, stateTotal);
+ LOG.info(" State {}: {} records in {} ms", state, stateTotal, stateElapsedMs);
+ }
+
+ long totalElapsedMs =
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - totalStart);
+ int grandTotal = countPerState.values().stream().mapToInt(Integer::intValue).sum();
+ double overallThroughput = grandTotal / Math.max(1.0, totalElapsedMs / 1000.0);
+
+ LOG.info("Full dataset read: {} total records in {} ms ({} rec/sec)",
+ grandTotal, totalElapsedMs, String.format("%.0f", overallThroughput));
+
+ assertEquals(TOTAL_RECORDS, grandTotal,
+ "Full dataset read must return exactly " + TOTAL_RECORDS + " records");
+
+ countPerState.forEach((state, cnt) ->
+ assertEquals(CONTAINER_ID_RANGE, cnt,
+ "State " + state + " must have " + CONTAINER_ID_RANGE + " records"));
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 7 — Atomic replace (delete + insert) performance for 1M records
+ // -----------------------------------------------------------------------
+
+ /**
+ * Exercises the same persistence pattern used by Recon health scan chunks:
+ * delete and insert in a single transaction.
+ *
+ * This validates that {@link ContainerHealthSchemaManager#replaceUnhealthyContainerRecordsAtomically}
+ * can safely replace a large chunk without changing total row count and
+ * that rewritten records are visible with the new timestamp.
+ */
+ @Test
+ @Order(7)
+ public void testAtomicReplaceDeleteAndInsertInSingleTransaction() {
+ int replaceContainerCount = CONTAINER_ID_RANGE;
+ long replacementTimestamp = System.currentTimeMillis() + 10_000;
+ int expectedRowsReplaced = replaceContainerCount * STATE_COUNT;
+
+ LOG.info("--- Test 7: Atomic replace — {} IDs × {} states = {} rows in one tx ---",
+ replaceContainerCount, STATE_COUNT, expectedRowsReplaced);
+
+ List idsToReplace = new ArrayList<>(replaceContainerCount);
+ for (long id = 1; id <= replaceContainerCount; id++) {
+ idsToReplace.add(id);
+ }
+ List replacementRecords =
+ generateRecordsForRange(1, replaceContainerCount, replacementTimestamp);
+
+ long start = System.nanoTime();
+ schemaManager.replaceUnhealthyContainerRecordsAtomically(idsToReplace, replacementRecords);
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ LOG.info("Atomic replace completed in {} ms", elapsedMs);
+
+ assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_ATOMIC_REPLACE_SECONDS),
+ String.format("Atomic replace took %d ms, exceeded %d s threshold",
+ elapsedMs, MAX_ATOMIC_REPLACE_SECONDS));
+
+ long totalCount = dao.count();
+ assertEquals(TOTAL_RECORDS, totalCount,
+ "Atomic replace should not change total row count");
+
+ List firstPage =
+ schemaManager.getUnhealthyContainers(
+ UnHealthyContainerStates.UNDER_REPLICATED, 0, 0, 1);
+ assertEquals(1, firstPage.size(), "Expected first under-replicated row");
+ assertEquals(1L, firstPage.get(0).getContainerId(),
+ "Expected containerId=1 as first row for UNDER_REPLICATED");
+ assertEquals(replacementTimestamp, firstPage.get(0).getInStateSince(),
+ "Replaced rows should carry the replacement timestamp");
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 8 — Batch DELETE performance for 1M records
+ // -----------------------------------------------------------------------
+
+ /**
+ * Deletes records for all container IDs (1 – 200,000) across
+ * all five states by passing the complete ID list in one call to
+ * {@link ContainerHealthSchemaManager#batchDeleteSCMStatesForContainers}.
+ *
+ * {@code batchDeleteSCMStatesForContainers} now handles internal
+ * chunking at {@value #DELETE_CHUNK_SIZE}
+ * IDs per SQL statement to stay within Derby's 64 KB generated-bytecode
+ * limit (JVM ERROR XBCM4). Passing 100 K IDs in a single call is safe
+ * because the method partitions them internally into 200 statements of
+ * 1,000 IDs each — matching Recon's real scan-cycle pattern for large
+ * clusters.
+ *
+ * Expected outcome: 200 K × 5 states = 1 M rows deleted, 0 remain.
+ *
+ * Note: this test modifies the shared dataset, so it runs after
+ * all read-only tests.
+ */
+ @Test
+ @Order(8)
+ public void testBatchDeletePerformanceOneMillionRecords() {
+ int deleteCount = CONTAINER_ID_RANGE; // 200 000 container IDs
+ int expectedDeleted = deleteCount * STATE_COUNT; // 1 000 000 rows
+ int expectedRemaining = TOTAL_RECORDS - expectedDeleted;
+ int internalChunks = (int) Math.ceil(
+ (double) deleteCount / DELETE_CHUNK_SIZE);
+
+ LOG.info("--- Test 8: Batch DELETE — {} IDs × {} states = {} rows "
+ + "({} internal SQL statements of {} IDs) ---",
+ deleteCount, STATE_COUNT, expectedDeleted,
+ internalChunks, DELETE_CHUNK_SIZE);
+
+ long start = System.nanoTime();
+
+ // Build the full list of container IDs to delete and pass in one call.
+ // batchDeleteSCMStatesForContainers partitions them internally so the
+ // caller does not need to chunk manually.
+ List idsToDelete = new ArrayList<>(deleteCount);
+ for (long id = 1; id <= deleteCount; id++) {
+ idsToDelete.add(id);
+ }
+ schemaManager.batchDeleteSCMStatesForContainers(idsToDelete);
+
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ double deleteThroughput = expectedDeleted / Math.max(1.0, elapsedMs / 1000.0);
+ LOG.info("DELETE complete: {} IDs ({} rows) in {} ms via {} SQL statements ({} rows/sec)",
+ deleteCount, expectedDeleted, elapsedMs, internalChunks,
+ String.format("%.0f", deleteThroughput));
+
+ long remainingCount = dao.count();
+ LOG.info("Rows remaining after delete: {} (expected {})",
+ remainingCount, expectedRemaining);
+
+ assertEquals(expectedRemaining, remainingCount,
+ "After deleting " + deleteCount + " container IDs, "
+ + expectedRemaining + " rows should remain");
+
+ assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_DELETE_SECONDS),
+ String.format("DELETE took %d ms, exceeded %d s threshold",
+ elapsedMs, MAX_DELETE_SECONDS));
+ }
+
+ // -----------------------------------------------------------------------
+ // Test 9 — Re-read counts after full delete
+ // -----------------------------------------------------------------------
+
+ /**
+ * After full delete, verifies that each state has 0 records.
+ */
+ @Test
+ @Order(9)
+ public void testCountByStateAfterFullDelete() {
+ int expectedPerState = 0;
+ LOG.info("--- Test 9: COUNT by state after full delete (expected {} each) ---",
+ expectedPerState);
+
+ DSLContext dsl = schemaDefinition.getDSLContext();
+
+ for (UnHealthyContainerStates state : TESTED_STATES) {
+ long start = System.nanoTime();
+ int stateCount = dsl
+ .select(count())
+ .from(UNHEALTHY_CONTAINERS)
+ .where(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString()))
+ .fetchOne(0, int.class);
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+ LOG.info(" COUNT({}) = {} rows in {} ms", state, stateCount, elapsedMs);
+
+ assertEquals(expectedPerState, stateCount,
+ "After full delete, state " + state
+ + " should have exactly " + expectedPerState + " records");
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Helper — generate records for an inclusive container-ID range
+ // -----------------------------------------------------------------------
+
+ /**
+ * Generates records for container IDs {@code [startId, endId]} across all
+ * {@link #TESTED_STATES}. Returning a range-bounded list rather than the
+ * full 1 M rows keeps peak heap usage proportional to {@link #CONTAINERS_PER_TX}
+ * rather than to {@link #TOTAL_RECORDS}.
+ *
+ * @param startId first container ID (inclusive)
+ * @param endId last container ID (inclusive)
+ * @param timestamp epoch millis to use as {@code in_state_since}
+ * @return list of {@code (endId - startId + 1) × STATE_COUNT} records
+ */
+ private List generateRecordsForRange(
+ int startId, int endId, long timestamp) {
+ int size = (endId - startId + 1) * STATE_COUNT;
+ List records = new ArrayList<>(size);
+
+ for (int containerId = startId; containerId <= endId; containerId++) {
+ for (UnHealthyContainerStates state : TESTED_STATES) {
+ int expectedReplicas;
+ int actualReplicas;
+ String reason;
+
+ switch (state) {
+ case UNDER_REPLICATED:
+ expectedReplicas = 3;
+ actualReplicas = 2;
+ reason = "Insufficient replicas";
+ break;
+ case MISSING:
+ expectedReplicas = 3;
+ actualReplicas = 0;
+ reason = "No replicas available";
+ break;
+ case OVER_REPLICATED:
+ expectedReplicas = 3;
+ actualReplicas = 4;
+ reason = "Excess replicas";
+ break;
+ case MIS_REPLICATED:
+ expectedReplicas = 3;
+ actualReplicas = 3;
+ reason = "Placement policy violated";
+ break;
+ case EMPTY_MISSING:
+ expectedReplicas = 1;
+ actualReplicas = 0;
+ reason = "Container has no replicas and no keys";
+ break;
+ default:
+ expectedReplicas = 3;
+ actualReplicas = 0;
+ reason = "Unknown state";
+ }
+
+ records.add(new ContainerHealthSchemaManager.UnhealthyContainerRecord(
+ containerId,
+ state.toString(),
+ timestamp,
+ expectedReplicas,
+ actualReplicas,
+ expectedReplicas - actualReplicas,
+ reason));
+ }
+ }
+ return records;
+ }
+}
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
new file mode 100644
index 000000000000..b4cde1bb6738
--- /dev/null
+++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java
@@ -0,0 +1,117 @@
+/*
+ * 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.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME;
+import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK;
+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;
+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 UnhealthyContainersStateContainerIdIndexUpgradeAction.
+ */
+public class TestUnhealthyContainersStateContainerIdIndexUpgradeAction
+ extends AbstractReconSqlDBTest {
+
+ private static final String INDEX_NAME = "idx_state_container_id";
+
+ private DSLContext dslContext;
+ private DataSource dataSource;
+ private UnhealthyContainersStateContainerIdIndexUpgradeAction upgradeAction;
+
+ @BeforeEach
+ public void setUp() {
+ dslContext = getDslContext();
+ dataSource = getInjector().getInstance(DataSource.class);
+ upgradeAction = new UnhealthyContainersStateContainerIdIndexUpgradeAction();
+ }
+
+ @Test
+ public void testCreatesIndexWhenMissing() throws Exception {
+ createTableWithoutIndex();
+ assertFalse(indexExists(INDEX_NAME));
+
+ upgradeAction.execute(dataSource);
+
+ assertTrue(indexExists(INDEX_NAME));
+ }
+
+ @Test
+ public void testExecuteIsIdempotentWhenIndexAlreadyExists() throws Exception {
+ createTableWithoutIndex();
+ upgradeAction.execute(dataSource);
+ assertTrue(indexExists(INDEX_NAME));
+
+ assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+ assertTrue(indexExists(INDEX_NAME));
+ }
+
+ @Test
+ public void testNoOpWhenTableMissing() throws SQLException {
+ dropTableIfPresent();
+ assertDoesNotThrow(() -> upgradeAction.execute(dataSource));
+ }
+
+ private void createTableWithoutIndex() throws SQLException {
+ dropTableIfPresent();
+ dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME)
+ .column("container_id", SQLDataType.BIGINT.nullable(false))
+ .column("container_state", SQLDataType.VARCHAR(16).nullable(false))
+ .constraint(DSL.constraint("pk_container_id")
+ .primaryKey(name("container_id"), name("container_state")))
+ .execute();
+ }
+
+ private void dropTableIfPresent() throws SQLException {
+ try (Connection conn = dataSource.getConnection()) {
+ if (TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) {
+ dslContext.dropTable(UNHEALTHY_CONTAINERS_TABLE_NAME).execute();
+ }
+ }
+ }
+
+ 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;
+ }
+}