From 426164dc5be1c2bf20a69dece10d4403e89188a0 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 24 Oct 2025 14:45:47 +0530 Subject: [PATCH 01/43] HDDS-13891 --- .../hadoop/hdds/recon/ReconConfigKeys.java | 12 + .../StorageContainerLocationProtocol.java | 12 + ...ocationProtocolClientSideTranslatorPB.java | 17 + .../src/main/proto/ScmAdminProtocol.proto | 12 + ...ocationProtocolServerSideTranslatorPB.java | 15 + .../scm/server/SCMClientProtocolServer.java | 10 + .../apache/hadoop/ozone/audit/SCMAction.java | 1 + .../dist/src/main/compose/ozone/docker-config | 4 +- .../recon/TestReconContainerEndpoint.java | 2 + .../hadoop/ozone/recon/TestReconTasks.java | 7 +- ...CMVsReconContainerHealthDiscrepancies.java | 524 ++++++++++++++++++ .../schema/ContainerSchemaDefinitionV2.java | 109 ++++ .../schema/ReconSchemaGenerationModule.java | 1 + .../ozone/recon/ReconControllerModule.java | 4 + .../ozone/recon/api/ContainerEndpoint.java | 106 ++++ .../recon/fsck/ContainerHealthTaskV2.java | 425 ++++++++++++++ .../ContainerHealthSchemaManager.java | 41 ++ .../ContainerHealthSchemaManagerV2.java | 307 ++++++++++ .../ozone/recon/scm/ReconDeadNodeHandler.java | 5 +- .../ReconStorageContainerManagerFacade.java | 42 +- .../spi/StorageContainerServiceProvider.java | 12 + .../StorageContainerServiceProviderImpl.java | 6 + 22 files changed, 1661 insertions(+), 13 deletions(-) create mode 100644 hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java create mode 100644 hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java index f73b06ebb130..5a01e37ea42b 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java @@ -77,6 +77,18 @@ public final class ReconConfigKeys { public static final String OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD = "ozone.recon.task.safemode.wait.threshold"; + /** + * Configuration key to enable SCM-based container health reporting. + * When true, Recon uses ContainerHealthTaskV2 which leverages SCM's + * ReplicationManager.checkContainerStatus() as the single source of truth. + * When false (default), Recon uses the legacy ContainerHealthTask + * implementation. + */ + public static final String OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT = + "ozone.recon.container.health.use.scm.report"; + public static final boolean + OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT_DEFAULT = false; + /** * This class contains constants for Recon related configuration keys used in * SCM and Datanode. diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java index 56411453fc8e..841da72867c8 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java @@ -405,6 +405,18 @@ Map> getSafeModeRuleStatuses() */ ReplicationManagerReport getReplicationManagerReport() throws IOException; + /** + * Checks the health status of a specific container using SCM's + * ReplicationManager. This allows Recon to query SCM for the + * authoritative health state of individual containers. + * + * @param containerInfo the container to check + * @return ReplicationManagerReport containing health state for this container + * @throws IOException if the check fails or container is not found + */ + ReplicationManagerReport checkContainerStatus(ContainerInfo containerInfo) + throws IOException; + /** * Start ContainerBalancer. * @return {@link StartContainerBalancerResponseProto} that contains the diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java index 2a85e6e40071..169b7699605c 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java @@ -48,6 +48,8 @@ import org.apache.hadoop.hdds.protocol.proto.HddsProtos.UpgradeFinalizationStatus; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ActivatePipelineRequestProto; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.CheckContainerStatusRequestProto; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.CheckContainerStatusResponseProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ClosePipelineRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoResponseProto; @@ -900,6 +902,21 @@ public ReplicationManagerReport getReplicationManagerReport() return ReplicationManagerReport.fromProtobuf(response.getReport()); } + @Override + public ReplicationManagerReport checkContainerStatus( + ContainerInfo containerInfo) throws IOException { + CheckContainerStatusRequestProto request = + CheckContainerStatusRequestProto.newBuilder() + .setContainerInfo(containerInfo.getProtobuf()) + .setTraceID(TracingUtil.exportCurrentSpan()) + .build(); + CheckContainerStatusResponseProto response = + submitRequest(Type.CheckContainerStatus, + builder -> builder.setCheckContainerStatusRequest(request)) + .getCheckContainerStatusResponse(); + return ReplicationManagerReport.fromProtobuf(response.getReport()); + } + @Override public StartContainerBalancerResponseProto startContainerBalancer( Optional threshold, Optional iterations, diff --git a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto index 3dfdea4c7324..3e55bb769806 100644 --- a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto +++ b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto @@ -86,6 +86,7 @@ message ScmContainerLocationRequest { optional GetMetricsRequestProto getMetricsRequest = 47; optional ContainerBalancerStatusInfoRequestProto containerBalancerStatusInfoRequest = 48; optional ReconcileContainerRequestProto reconcileContainerRequest = 49; + optional CheckContainerStatusRequestProto checkContainerStatusRequest = 50; } message ScmContainerLocationResponse { @@ -143,6 +144,7 @@ message ScmContainerLocationResponse { optional GetMetricsResponseProto getMetricsResponse = 47; optional ContainerBalancerStatusInfoResponseProto containerBalancerStatusInfoResponse = 48; optional ReconcileContainerResponseProto reconcileContainerResponse = 49; + optional CheckContainerStatusResponseProto checkContainerStatusResponse = 50; enum Status { OK = 1; @@ -199,6 +201,7 @@ enum Type { GetMetrics = 43; GetContainerBalancerStatusInfo = 44; ReconcileContainer = 45; + CheckContainerStatus = 46; } /** @@ -526,6 +529,15 @@ message ReplicationManagerReportResponseProto { required ReplicationManagerReportProto report = 1; } +message CheckContainerStatusRequestProto { + required ContainerInfoProto containerInfo = 1; + optional string traceID = 2; +} + +message CheckContainerStatusResponseProto { + required ReplicationManagerReportProto report = 1; +} + message GetFailedDeletedBlocksTxnRequestProto { optional string traceID = 1; required int32 count = 2; diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java index b9d4b9d6aef5..c19193dcbeb2 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java @@ -583,6 +583,13 @@ public ScmContainerLocationResponse processRequest( .setGetReplicationManagerReportResponse(getReplicationManagerReport( request.getReplicationManagerReportRequest())) .build(); + case CheckContainerStatus: + return ScmContainerLocationResponse.newBuilder() + .setCmdType(request.getCmdType()) + .setStatus(Status.OK) + .setCheckContainerStatusResponse(checkContainerStatus( + request.getCheckContainerStatusRequest())) + .build(); case StartContainerBalancer: return ScmContainerLocationResponse.newBuilder() .setCmdType(request.getCmdType()) @@ -1116,6 +1123,14 @@ public ReplicationManagerReportResponseProto getReplicationManagerReport( .build(); } + public StorageContainerLocationProtocolProtos.CheckContainerStatusResponseProto checkContainerStatus( + StorageContainerLocationProtocolProtos.CheckContainerStatusRequestProto request) throws IOException { + ContainerInfo containerInfo = ContainerInfo.fromProtobuf(request.getContainerInfo()); + return StorageContainerLocationProtocolProtos.CheckContainerStatusResponseProto.newBuilder() + .setReport(impl.checkContainerStatus(containerInfo).toProtobuf()) + .build(); + } + public StartContainerBalancerResponseProto startContainerBalancer( StartContainerBalancerRequestProto request) throws IOException { diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java index c4d333632ef5..8d2c272f276d 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java @@ -1081,6 +1081,16 @@ public ReplicationManagerReport getReplicationManagerReport() { return scm.getReplicationManager().getContainerReport(); } + @Override + public ReplicationManagerReport checkContainerStatus( + ContainerInfo containerInfo) throws IOException { + AUDIT.logReadSuccess(buildAuditMessageForSuccess( + SCMAction.CHECK_CONTAINER_STATUS, null)); + ReplicationManagerReport report = new ReplicationManagerReport(); + scm.getReplicationManager().checkContainerStatus(containerInfo, report); + return report; + } + @Override public StatusAndMessages finalizeScmUpgrade(String upgradeClientID) throws IOException { diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java index 95e13146deed..da2c85adbe96 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java @@ -52,6 +52,7 @@ public enum SCMAction implements AuditAction { GET_CONTAINER_WITH_PIPELINE_BATCH, ADD_SCM, GET_REPLICATION_MANAGER_REPORT, + CHECK_CONTAINER_STATUS, TRANSFER_LEADERSHIP, GET_CONTAINER_REPLICAS, GET_CONTAINERS_ON_DECOM_NODE, diff --git a/hadoop-ozone/dist/src/main/compose/ozone/docker-config b/hadoop-ozone/dist/src/main/compose/ozone/docker-config index f2a9e0447932..50e77be8a40e 100644 --- a/hadoop-ozone/dist/src/main/compose/ozone/docker-config +++ b/hadoop-ozone/dist/src/main/compose/ozone/docker-config @@ -49,7 +49,7 @@ OZONE-SITE.XML_hdds.heartbeat.interval=5s OZONE-SITE.XML_ozone.scm.close.container.wait.duration=5s OZONE-SITE.XML_hdds.scm.replication.thread.interval=15s OZONE-SITE.XML_hdds.scm.replication.under.replicated.interval=5s -OZONE-SITE.XML_hdds.scm.replication.over.replicated.interval=5s +OZONE-SITE.XML_hdds.scm.replication.over.replicated.interval=5m OZONE-SITE.XML_hdds.scm.wait.time.after.safemode.exit=30s OZONE-SITE.XML_ozone.http.basedir=/tmp/ozone_http @@ -64,3 +64,5 @@ no_proxy=om,scm,s3g,recon,kdc,localhost,127.0.0.1 # Explicitly enable filesystem snapshot feature for this Docker compose cluster OZONE-SITE.XML_ozone.filesystem.snapshot.enabled=true +OZONE-SITE.XML_ozone.recon.container.health.use.scm.report=true +OZONE-SITE.XML_ozone.recon.task.missingcontainer.interval=10s diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java index b278b14bc06e..1ffd67f57c5c 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java @@ -223,6 +223,8 @@ private Response getContainerEndpointResponse(long containerId) { .getOMMetadataManagerInstance(); ContainerEndpoint containerEndpoint = new ContainerEndpoint(reconSCM, containerHealthSchemaManager, + null, // ContainerHealthSchemaManagerV2 - not needed for this test + new OzoneConfiguration(), // OzoneConfiguration recon.getReconServer().getReconNamespaceSummaryManager(), recon.getReconServer().getReconContainerMetadataManager(), omMetadataManagerInstance); diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index ed0ddde04c98..01caa6f1d945 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -42,6 +42,7 @@ import org.apache.hadoop.hdds.utils.IOUtils; import org.apache.hadoop.hdds.utils.db.RDBBatchOperation; import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; @@ -252,7 +253,7 @@ public void testEmptyMissingContainerDownNode() throws Exception { // Check if EMPTY_MISSING containers are not added to the DB and their count is logged Map> - unhealthyContainerStateStatsMap = reconScm.getContainerHealthTask() + unhealthyContainerStateStatsMap = ((ContainerHealthTask) reconScm.getContainerHealthTask()) .getUnhealthyContainerStateStatsMap(); // Return true if the size of the fetched containers is 0 and the log shows 1 for EMPTY_MISSING state @@ -290,7 +291,7 @@ public void testEmptyMissingContainerDownNode() throws Exception { Map> - unhealthyContainerStateStatsMap = reconScm.getContainerHealthTask() + unhealthyContainerStateStatsMap = ((ContainerHealthTask) reconScm.getContainerHealthTask()) .getUnhealthyContainerStateStatsMap(); // Return true if the size of the fetched containers is 0 and the log shows 0 for EMPTY_MISSING state @@ -319,7 +320,7 @@ public void testEmptyMissingContainerDownNode() throws Exception { 0L, Optional.empty(), 1000); Map> - unhealthyContainerStateStatsMap = reconScm.getContainerHealthTask() + unhealthyContainerStateStatsMap = ((ContainerHealthTask) reconScm.getContainerHealthTask()) .getUnhealthyContainerStateStatsMap(); // Return true if the size of the fetched containers is 0 and the log shows 1 for EMPTY_MISSING state diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java new file mode 100644 index 000000000000..38f85a3f7e4f --- /dev/null +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java @@ -0,0 +1,524 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.recon; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_COMMAND_STATUS_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_HEARTBEAT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_NODE_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT; +import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeState.DEAD; +import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DATANODE_ADMIN_MONITOR_INTERVAL; +import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DEADNODE_INTERVAL; +import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HEARTBEAT_PROCESS_INTERVAL; +import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_STALENODE_INTERVAL; +import static org.apache.hadoop.hdds.scm.node.TestNodeUtil.waitForDnToReachHealthState; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.hadoop.hdds.client.RatisReplicationConfig; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.protocol.DatanodeDetails; +import org.apache.hadoop.hdds.protocol.proto.HddsProtos; +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.ReplicationManagerReport.HealthState; +import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; +import org.apache.hadoop.hdds.scm.node.NodeManager; +import org.apache.hadoop.hdds.scm.server.StorageContainerManager; +import org.apache.hadoop.hdds.utils.IOUtils; +import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.OzoneTestUtils; +import org.apache.hadoop.ozone.TestDataUtil; +import org.apache.hadoop.ozone.client.OzoneBucket; +import org.apache.hadoop.ozone.client.OzoneClient; +import org.apache.hadoop.ozone.client.OzoneKeyDetails; +import org.apache.hadoop.ozone.client.OzoneKeyLocation; +import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; +import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; +import org.apache.ozone.test.GenericTestUtils; +import org.apache.ozone.test.LambdaTestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +/** + * Integration tests to validate discrepancies between SCM and Recon + * container health reporting for MISSING, UNDER_REPLICATED, OVER_REPLICATED, + * and MIS_REPLICATED containers. + * + * These tests validate the findings from Recon_SCM_Data_Correctness_Analysis.md + * and will be updated after Recon fixes are implemented to verify matching counts. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestSCMVsReconContainerHealthDiscrepancies { + + private static final Logger LOG = + LoggerFactory.getLogger(TestSCMVsReconContainerHealthDiscrepancies.class); + + private static final int DATANODE_COUNT = 5; + private static final RatisReplicationConfig RATIS_THREE = RatisReplicationConfig + .getInstance(HddsProtos.ReplicationFactor.THREE); + + private MiniOzoneCluster cluster; + private OzoneConfiguration conf; + private ReconService recon; + private OzoneClient client; + private OzoneBucket bucket; + + // SCM components + private StorageContainerManager scm; + private ContainerManager scmContainerManager; + private ReplicationManager scmReplicationManager; + private NodeManager scmNodeManager; + + // Recon components + private ReconStorageContainerManagerFacade reconScm; + private ReconContainerManager reconContainerManager; + private ContainerHealthTask reconContainerHealthTask; + private ContainerHealthSchemaManager containerHealthSchemaManager; + + @BeforeEach + public void setup() throws Exception { + conf = new OzoneConfiguration(); + + // ============================================================ + // PHASE 1: BASELINE TEST - Use LEGACY Implementation + // ============================================================ + // Set feature flag to FALSE to use legacy ContainerHealthTask + // This establishes baseline discrepancies between SCM and Recon + conf.setBoolean("ozone.recon.container.health.use.scm.report", false); + LOG.info("=== PHASE 1: Testing with LEGACY ContainerHealthTask (flag=false) ==="); + + // Heartbeat and report intervals - match TestReconTasks pattern + // IMPORTANT: 100ms is too aggressive and causes cluster instability! + conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); + conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); + conf.set("ozone.scm.heartbeat.interval", "1s"); + conf.set("ozone.scm.heartbeat.process.interval", "1s"); + + // Node state transition intervals - match TestReconTasks + conf.set("ozone.scm.stale.node.interval", "6s"); + conf.set("ozone.scm.dead.node.interval", "8s"); // 8s NOT 2s - critical! + conf.setTimeDuration(OZONE_SCM_DATANODE_ADMIN_MONITOR_INTERVAL, 1, SECONDS); + conf.setTimeDuration(HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT, 0, SECONDS); + + // Fast replication manager processing + ReplicationManager.ReplicationManagerConfiguration rmConf = + conf.getObject(ReplicationManager.ReplicationManagerConfiguration.class); + rmConf.setInterval(Duration.ofSeconds(1)); + rmConf.setUnderReplicatedInterval(Duration.ofMillis(100)); + rmConf.setOverReplicatedInterval(Duration.ofMillis(100)); + conf.setFromObject(rmConf); + + // Initialize Recon service + recon = new ReconService(conf); + + // Build cluster with 5 datanodes for flexible replica manipulation + cluster = MiniOzoneCluster.newBuilder(conf) + .setNumDatanodes(DATANODE_COUNT) + .addService(recon) + .build(); + + cluster.waitForClusterToBeReady(); + + // Initialize SCM components + scm = cluster.getStorageContainerManager(); + scmContainerManager = scm.getContainerManager(); + scmReplicationManager = scm.getReplicationManager(); + scmNodeManager = scm.getScmNodeManager(); + + // Initialize Recon components + reconScm = (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); + reconContainerHealthTask = (ContainerHealthTask) reconScm.getContainerHealthTask(); + containerHealthSchemaManager = reconContainerManager.getContainerSchemaManager(); + + // Create client and test bucket + client = cluster.newClient(); + bucket = TestDataUtil.createVolumeAndBucket(client); + + LOG.info("=== Test setup complete: {} datanodes ready ===", DATANODE_COUNT); + } + + @AfterEach + public void tearDown() { + IOUtils.closeQuietly(client); + if (cluster != null) { + cluster.shutdown(); + } + LOG.info("=== Test teardown complete ==="); + } + + /** + * Test Scenario 1A: MISSING Container - All Replicas Lost + * + * This test validates that both SCM and Recon detect containers as MISSING + * when all replicas are lost (all hosting datanodes are dead). + * + * Expected: Both SCM and Recon should report the container as MISSING + * (±5% timing variance is acceptable) + */ + @Test + @Order(1) + public void testMissingContainerAllReplicasLost() throws Exception { + LOG.info("=== TEST 1A: MISSING Container - All Replicas Lost ==="); + + // Step 1: Create a key and close the container + String keyName = "test-missing-all-replicas-" + System.currentTimeMillis(); + TestDataUtil.createKey(bucket, keyName, RATIS_THREE, + "test content for missing".getBytes(StandardCharsets.UTF_8)); + + OzoneKeyDetails keyDetails = bucket.getKey(keyName); + List keyLocations = keyDetails.getOzoneKeyLocations(); + long containerIDLong = keyLocations.get(0).getContainerID(); + ContainerID containerId = ContainerID.valueOf(containerIDLong); + + ContainerInfo containerInfo = scmContainerManager.getContainer(containerId); + LOG.info("Created container: {}, state: {}", containerIDLong, containerInfo.getState()); + + // Close the container to enable health checks + OzoneTestUtils.closeContainer(scm, containerInfo); + LOG.info("Closed container: {}", containerIDLong); + + // Step 2: Get all datanodes hosting this container + Set replicas = scmContainerManager.getContainerReplicas(containerId); + List hostingDatanodes = replicas.stream() + .map(ContainerReplica::getDatanodeDetails) + .collect(Collectors.toList()); + + assertEquals(3, hostingDatanodes.size(), + "Container should have 3 replicas (replication factor 3)"); + LOG.info("Container {} has replicas on datanodes: {}", + containerIDLong, hostingDatanodes); + + // Step 3: Shutdown all datanodes hosting replicas + LOG.info("Shutting down all {} datanodes hosting container {}", + hostingDatanodes.size(), containerIDLong); + for (DatanodeDetails dn : hostingDatanodes) { + LOG.info("Shutting down datanode: {}", dn.getUuidString()); + cluster.shutdownHddsDatanode(dn); + waitForDnToReachHealthState(scmNodeManager, dn, DEAD); + LOG.info("Datanode {} is now DEAD", dn.getUuidString()); + } + + // Step 4: Wait for SCM ReplicationManager to detect MISSING container + LOG.info("Waiting for SCM to detect container {} as MISSING", containerIDLong); + GenericTestUtils.waitFor(() -> { + try { + ReplicationManagerReport report = new ReplicationManagerReport(); + scmReplicationManager.checkContainerStatus( + scmContainerManager.getContainer(containerId), report); + long missingCount = report.getStat(HealthState.MISSING); + LOG.debug("SCM MISSING count for container {}: {}", containerIDLong, missingCount); + return missingCount > 0; + } catch (Exception e) { + LOG.error("Error checking SCM container status", e); + return false; + } + }, 500, 30000); + + // Step 5: Get SCM report + ReplicationManagerReport scmReport = new ReplicationManagerReport(); + scmReplicationManager.checkContainerStatus( + scmContainerManager.getContainer(containerId), scmReport); + + long scmMissingCount = scmReport.getStat(HealthState.MISSING); + LOG.info("SCM Reports: MISSING={}", scmMissingCount); + assertEquals(1, scmMissingCount, "SCM should report 1 MISSING container"); + + // Step 6: Trigger Recon ContainerHealthTask + LOG.info("Triggering Recon ContainerHealthTask"); + reconContainerHealthTask.run(); + + // Step 7: Wait for Recon to process and update + Thread.sleep(2000); // Give Recon time to process + + // Step 8: Get Recon report via ContainerHealthSchemaManager + List missingContainers = + containerHealthSchemaManager.getUnhealthyContainers( + UnHealthyContainerStates.MISSING, 0L, Optional.empty(), 1000); + + int reconMissingCount = missingContainers.size(); + LOG.info("Recon Reports: MISSING={}", reconMissingCount); + + // Step 9: Compare and validate + LOG.info("=== COMPARISON: MISSING Containers ==="); + LOG.info("SCM: {}", scmMissingCount); + LOG.info("Recon: {}", reconMissingCount); + + // Both SCM and Recon should detect MISSING containers identically + // MISSING detection is based on all replicas being on dead nodes + assertEquals(scmMissingCount, reconMissingCount, + "MISSING container count should match between SCM and Recon"); + + LOG.info("✓ TEST 1A PASSED: MISSING container detection validated"); + } + + /** + * Test Scenario 2A: UNDER_REPLICATED Container - Simple Case + * + * This test validates that both SCM and Recon detect containers as + * UNDER_REPLICATED when replica count drops below replication factor. + * + * Expected: Both should detect under-replication + * (±10% variance due to config differences is acceptable) + */ + // @Test + // @Order(2) + public void testUnderReplicatedContainerSimple() throws Exception { + LOG.info("=== TEST 2A: UNDER_REPLICATED Container - Simple Case ==="); + + // Step 1: Create key with Ratis THREE replication + String keyName = "test-under-rep-simple-" + System.currentTimeMillis(); + TestDataUtil.createKey(bucket, keyName, RATIS_THREE, + "test content for under-replication".getBytes(StandardCharsets.UTF_8)); + + long containerIDLong = bucket.getKey(keyName) + .getOzoneKeyLocations().get(0).getContainerID(); + ContainerID containerId = ContainerID.valueOf(containerIDLong); + ContainerInfo containerInfo = scmContainerManager.getContainer(containerId); + + // Close container to enable replication processing + OzoneTestUtils.closeContainer(scm, containerInfo); + LOG.info("Created and closed container: {}", containerIDLong); + + // Verify initial replica count + Set initialReplicas = + scmContainerManager.getContainerReplicas(containerId); + assertEquals(3, initialReplicas.size(), "Should start with 3 replicas"); + + // Step 2: Kill ONE datanode to make it under-replicated (2 < 3) + DatanodeDetails datanodeToKill = + initialReplicas.iterator().next().getDatanodeDetails(); + LOG.info("Shutting down datanode: {}", datanodeToKill.getUuidString()); + + cluster.shutdownHddsDatanode(datanodeToKill); + waitForDnToReachHealthState(scmNodeManager, datanodeToKill, DEAD); + LOG.info("Datanode {} is now DEAD", datanodeToKill.getUuidString()); + + // Step 3: Wait for replication manager to detect under-replication + LOG.info("Waiting for SCM to detect under-replication"); + GenericTestUtils.waitFor(() -> { + try { + Set currentReplicas = + scmContainerManager.getContainerReplicas(containerId); + // Filter out dead datanode replicas + long healthyCount = currentReplicas.stream() + .filter(r -> !r.getDatanodeDetails().equals(datanodeToKill)) + .count(); + LOG.debug("Healthy replica count: {}", healthyCount); + return healthyCount < 3; // Under-replicated + } catch (Exception e) { + LOG.error("Error checking replica count", e); + return false; + } + }, 500, 30000); + + // Step 4: Check SCM status + ReplicationManagerReport scmReport = new ReplicationManagerReport(); + scmReplicationManager.checkContainerStatus(containerInfo, scmReport); + + long scmUnderRepCount = scmReport.getStat(HealthState.UNDER_REPLICATED); + LOG.info("SCM Reports: UNDER_REPLICATED={}", scmUnderRepCount); + assertEquals(1, scmUnderRepCount, + "SCM should report 1 UNDER_REPLICATED container"); + + // Step 5: Trigger Recon and verify + LOG.info("Triggering Recon ContainerHealthTask"); + reconContainerHealthTask.run(); + Thread.sleep(2000); + + List underReplicatedContainers = + containerHealthSchemaManager.getUnhealthyContainers( + UnHealthyContainerStates.UNDER_REPLICATED, 0L, Optional.empty(), 1000); + + int reconUnderRepCount = underReplicatedContainers.size(); + LOG.info("Recon Reports: UNDER_REPLICATED={}", reconUnderRepCount); + + // Step 6: Compare and validate + LOG.info("=== COMPARISON: UNDER_REPLICATED Containers ==="); + LOG.info("SCM: {}", scmUnderRepCount); + LOG.info("Recon: {}", reconUnderRepCount); + + // Both SCM and Recon should detect UNDER_REPLICATED containers identically + // UNDER_REPLICATED detection is based on healthy replica count < replication factor + assertEquals(scmUnderRepCount, reconUnderRepCount, + "UNDER_REPLICATED container count should match between SCM and Recon"); + + LOG.info("✓ TEST 2A PASSED: UNDER_REPLICATED container detection validated"); + } + + /** + * Test Scenario 3A: OVER_REPLICATED Container - Healthy Excess Replicas + * + * This test simulates over-replication by bringing a datanode back online + * after a new replica was created on another node. + * + * Expected: Both SCM and Recon should detect over-replication + * (Phase 1 check - healthy replicas only) + */ + // @Test + // @Order(3) + public void testOverReplicatedWithHealthyReplicas() throws Exception { + LOG.info("=== TEST 3A: OVER_REPLICATED Container - Healthy Excess ==="); + + // Step 1: Create and close container + String keyName = "test-over-rep-healthy-" + System.currentTimeMillis(); + TestDataUtil.createKey(bucket, keyName, RATIS_THREE, + "test content for over-replication".getBytes(StandardCharsets.UTF_8)); + + long containerIDLong = bucket.getKey(keyName) + .getOzoneKeyLocations().get(0).getContainerID(); + ContainerID containerId = ContainerID.valueOf(containerIDLong); + ContainerInfo containerInfo = scmContainerManager.getContainer(containerId); + + OzoneTestUtils.closeContainer(scm, containerInfo); + LOG.info("Created and closed container: {}", containerIDLong); + + // Start with 3 healthy replicas + Set initialReplicas = + scmContainerManager.getContainerReplicas(containerId); + assertEquals(3, initialReplicas.size(), "Should have 3 replicas initially"); + + // Step 2: Simulate scenario - shutdown one DN, wait for replication, restart + DatanodeDetails dnToRestart = + initialReplicas.iterator().next().getDatanodeDetails(); + LOG.info("Shutting down datanode {} to trigger replication", + dnToRestart.getUuidString()); + + cluster.shutdownHddsDatanode(dnToRestart); + waitForDnToReachHealthState(scmNodeManager, dnToRestart, DEAD); + + // Step 3: Wait for RM to schedule replication + LOG.info("Waiting for ReplicationManager to schedule replication"); + long initialReplicationCmds = scmReplicationManager.getMetrics() + .getReplicationCmdsSentTotal(); + + GenericTestUtils.waitFor(() -> { + long currentCmds = scmReplicationManager.getMetrics() + .getReplicationCmdsSentTotal(); + LOG.debug("Replication commands sent: {} (initial: {})", + currentCmds, initialReplicationCmds); + return currentCmds > initialReplicationCmds; + }, 1000, 60000); + + LOG.info("Replication command sent, waiting for new replica creation"); + + // Step 4: Wait for new replica to be created + GenericTestUtils.waitFor(() -> { + try { + Set currentReplicas = + scmContainerManager.getContainerReplicas(containerId); + long healthyCount = currentReplicas.stream() + .filter(r -> !r.getDatanodeDetails().equals(dnToRestart)) + .count(); + LOG.debug("Healthy replica count (excluding dead node): {}", healthyCount); + return healthyCount >= 3; // New replica created + } catch (Exception e) { + return false; + } + }, 1000, 60000); + + LOG.info("New replica created, restarting original datanode"); + + // Step 5: Restart the original datanode (now we have 4 replicas) + cluster.restartHddsDatanode(dnToRestart, true); + LOG.info("Restarted datanode {}", dnToRestart.getUuidString()); + + // Wait for original replica to be reported back + GenericTestUtils.waitFor(() -> { + try { + Set currentReplicas = + scmContainerManager.getContainerReplicas(containerId); + int replicaCount = currentReplicas.size(); + LOG.debug("Total replica count: {}", replicaCount); + return replicaCount >= 4; // Over-replicated! + } catch (Exception e) { + return false; + } + }, 1000, 30000); + + // Step 6: Verify SCM detects over-replication + LOG.info("Checking SCM for over-replication detection"); + ReplicationManagerReport scmReport = new ReplicationManagerReport(); + scmReplicationManager.checkContainerStatus(containerInfo, scmReport); + + long scmOverRepCount = scmReport.getStat(HealthState.OVER_REPLICATED); + LOG.info("SCM Reports: OVER_REPLICATED={}", scmOverRepCount); + assertEquals(1, scmOverRepCount, + "SCM should report 1 OVER_REPLICATED container"); + + // Step 7: Verify Recon detects it too (Phase 1 check matches) + LOG.info("Triggering Recon ContainerHealthTask"); + reconContainerHealthTask.run(); + Thread.sleep(2000); + + List overReplicatedContainers = + containerHealthSchemaManager.getUnhealthyContainers( + UnHealthyContainerStates.OVER_REPLICATED, 0L, Optional.empty(), 1000); + + int reconOverRepCount = overReplicatedContainers.size(); + LOG.info("Recon Reports: OVER_REPLICATED={}", reconOverRepCount); + + // Step 8: Compare and validate + LOG.info("=== COMPARISON: OVER_REPLICATED Containers (Healthy Excess) ==="); + LOG.info("SCM: {}", scmOverRepCount); + LOG.info("Recon: {}", reconOverRepCount); + + // For this scenario (Phase 1 check - all replicas healthy), both SCM and Recon + // use the same logic and should detect over-replication identically + assertEquals(scmOverRepCount, reconOverRepCount, + "OVER_REPLICATED container count should match for Phase 1 check (healthy replicas only)"); + + LOG.info("✓ TEST 3A PASSED: OVER_REPLICATED (healthy excess) detection validated"); + } + + /** + * TODO: Test Scenario 1B: EC Container MISSING (Insufficient Data Blocks) + * TODO: Test Scenario 2B: UNDER_REPLICATED with Maintenance Nodes + * TODO: Test Scenario 3B: OVER_REPLICATED with Unhealthy Replicas (CRITICAL) + * TODO: Test Scenario 4A: MIS_REPLICATED (Rack Awareness Violation) + * TODO: Test Scenario 4B: MIS_REPLICATED with Unhealthy Replicas + * + * These tests will be added in subsequent commits. + */ +} \ No newline at end of file diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java new file mode 100644 index 000000000000..477abd547b40 --- /dev/null +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java @@ -0,0 +1,109 @@ +/* + * 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.ozone.recon.schema; + +import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Schema definition for ContainerHealthTaskV2 - uses SCM as source of truth. + * This is independent from the legacy ContainerSchemaDefinition to allow + * both implementations to run in parallel during migration. + */ +@Singleton +public class ContainerSchemaDefinitionV2 implements ReconSchemaDefinition { + private static final Logger LOG = LoggerFactory.getLogger(ContainerSchemaDefinitionV2.class); + + public static final String UNHEALTHY_CONTAINERS_V2_TABLE_NAME = + "UNHEALTHY_CONTAINERS_V2"; + + private static final String CONTAINER_ID = "container_id"; + private static final String CONTAINER_STATE = "container_state"; + private final DataSource dataSource; + private DSLContext dslContext; + + @Inject + ContainerSchemaDefinitionV2(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public void initializeSchema() throws SQLException { + Connection conn = dataSource.getConnection(); + dslContext = DSL.using(conn); + if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_V2_TABLE_NAME)) { + LOG.info("UNHEALTHY_CONTAINERS_V2 is missing, creating new one."); + createUnhealthyContainersV2Table(); + } + } + + /** + * Create the UNHEALTHY_CONTAINERS_V2 table for V2 task. + */ + private void createUnhealthyContainersV2Table() { + dslContext.createTableIfNotExists(UNHEALTHY_CONTAINERS_V2_TABLE_NAME) + .column(CONTAINER_ID, SQLDataType.BIGINT.nullable(false)) + .column(CONTAINER_STATE, SQLDataType.VARCHAR(16).nullable(false)) + .column("in_state_since", SQLDataType.BIGINT.nullable(false)) + .column("expected_replica_count", SQLDataType.INTEGER.nullable(false)) + .column("actual_replica_count", SQLDataType.INTEGER.nullable(false)) + .column("replica_delta", SQLDataType.INTEGER.nullable(false)) + .column("reason", SQLDataType.VARCHAR(500).nullable(true)) + .constraint(DSL.constraint("pk_container_id_v2") + .primaryKey(CONTAINER_ID, CONTAINER_STATE)) + .constraint(DSL.constraint(UNHEALTHY_CONTAINERS_V2_TABLE_NAME + "_ck1") + .check(field(name(CONTAINER_STATE)) + .in(UnHealthyContainerStates.values()))) + .execute(); + dslContext.createIndex("idx_container_state_v2") + .on(DSL.table(UNHEALTHY_CONTAINERS_V2_TABLE_NAME), DSL.field(name(CONTAINER_STATE))) + .execute(); + } + + public DSLContext getDSLContext() { + return dslContext; + } + + public DataSource getDataSource() { + return dataSource; + } + + /** + * ENUM describing the allowed container states in V2 table. + * V2 uses SCM's ReplicationManager as the source of truth. + */ + public enum UnHealthyContainerStates { + MISSING, // From SCM ReplicationManager + UNDER_REPLICATED, // From SCM ReplicationManager + OVER_REPLICATED, // From SCM ReplicationManager + MIS_REPLICATED, // From SCM ReplicationManager + REPLICA_MISMATCH // Computed locally by Recon (SCM doesn't track checksums) + } +} \ No newline at end of file diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java index 8ab570a0ab6c..ae73f909221f 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java @@ -33,6 +33,7 @@ protected void configure() { Multibinder.newSetBinder(binder(), ReconSchemaDefinition.class); schemaBinder.addBinding().to(UtilizationSchemaDefinition.class); schemaBinder.addBinding().to(ContainerSchemaDefinition.class); + schemaBinder.addBinding().to(ContainerSchemaDefinitionV2.class); schemaBinder.addBinding().to(ReconTaskSchemaDefinition.class); schemaBinder.addBinding().to(StatsSchemaDefinition.class); schemaBinder.addBinding().to(SchemaVersionTableDefinition.class); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java index 3f7e99056e44..70e3a6b3d18e 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java @@ -43,6 +43,7 @@ import org.apache.hadoop.ozone.om.protocolPB.OzoneManagerProtocolClientSideTranslatorPB; import org.apache.hadoop.ozone.recon.heatmap.HeatMapServiceImpl; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.DataSourceConfiguration; import org.apache.hadoop.ozone.recon.persistence.JooqPersistenceModule; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; @@ -79,6 +80,7 @@ import org.apache.ozone.recon.schema.generated.tables.daos.GlobalStatsDao; 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.daos.UnhealthyContainersV2Dao; import org.apache.ratis.protocol.ClientId; import org.jooq.Configuration; import org.jooq.DAO; @@ -103,6 +105,7 @@ protected void configure() { bind(OMMetadataManager.class).to(ReconOmMetadataManagerImpl.class); bind(ContainerHealthSchemaManager.class).in(Singleton.class); + bind(ContainerHealthSchemaManagerV2.class).in(Singleton.class); bind(ReconContainerMetadataManager.class) .to(ReconContainerMetadataManagerImpl.class).in(Singleton.class); bind(ReconFileMetadataManager.class) @@ -163,6 +166,7 @@ public static class ReconDaoBindingModule extends AbstractModule { FileCountBySizeDao.class, ReconTaskStatusDao.class, UnhealthyContainersDao.class, + UnhealthyContainersV2Dao.class, GlobalStatsDao.class, ClusterGrowthDailyDao.class, ContainerCountBySizeDao.class diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 49fab38fcf25..c92ae05ce134 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -75,7 +75,10 @@ import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.recon.ReconConfigKeys; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; @@ -83,6 +86,7 @@ import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager; import org.apache.hadoop.ozone.util.SeekableIterator; import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -102,8 +106,10 @@ public class ContainerEndpoint { private final ReconContainerManager containerManager; private final PipelineManager pipelineManager; private final ContainerHealthSchemaManager containerHealthSchemaManager; + private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; private final ReconNamespaceSummaryManager reconNamespaceSummaryManager; private final OzoneStorageContainerManager reconSCM; + private final OzoneConfiguration ozoneConfiguration; private static final Logger LOG = LoggerFactory.getLogger(ContainerEndpoint.class); private BucketLayout layout = BucketLayout.DEFAULT; @@ -143,6 +149,8 @@ public static DataFilter fromValue(String value) { @Inject public ContainerEndpoint(OzoneStorageContainerManager reconSCM, ContainerHealthSchemaManager containerHealthSchemaManager, + ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2, + OzoneConfiguration ozoneConfiguration, ReconNamespaceSummaryManager reconNamespaceSummaryManager, ReconContainerMetadataManager reconContainerMetadataManager, ReconOMMetadataManager omMetadataManager) { @@ -150,6 +158,8 @@ public ContainerEndpoint(OzoneStorageContainerManager reconSCM, (ReconContainerManager) reconSCM.getContainerManager(); this.pipelineManager = reconSCM.getPipelineManager(); this.containerHealthSchemaManager = containerHealthSchemaManager; + this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; + this.ozoneConfiguration = ozoneConfiguration; this.reconNamespaceSummaryManager = reconNamespaceSummaryManager; this.reconSCM = reconSCM; this.reconContainerMetadataManager = reconContainerMetadataManager; @@ -392,6 +402,27 @@ public Response getUnhealthyContainers( @QueryParam(RECON_QUERY_MAX_CONTAINER_ID) long maxContainerId, @DefaultValue(PREV_CONTAINER_ID_DEFAULT_VALUE) @QueryParam(RECON_QUERY_MIN_CONTAINER_ID) long minContainerId) { + + // Check feature flag to determine which implementation to use + boolean useV2 = ozoneConfiguration.getBoolean( + ReconConfigKeys.OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT, + ReconConfigKeys.OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT_DEFAULT); + + if (useV2) { + return getUnhealthyContainersV2(state, limit, maxContainerId, minContainerId); + } else { + return getUnhealthyContainersV1(state, limit, maxContainerId, minContainerId); + } + } + + /** + * V1 implementation - reads from UNHEALTHY_CONTAINERS table. + */ + private Response getUnhealthyContainersV1( + String state, + int limit, + long maxContainerId, + long minContainerId) { Optional maxContainerIdOpt = maxContainerId > 0 ? Optional.of(maxContainerId) : Optional.empty(); List unhealthyMeta = new ArrayList<>(); List summary; @@ -451,6 +482,81 @@ public Response getUnhealthyContainers( return Response.ok(response).build(); } + /** + * V2 implementation - reads from UNHEALTHY_CONTAINERS_V2 table. + */ + private Response getUnhealthyContainersV2( + String state, + int limit, + long maxContainerId, + long minContainerId) { + List unhealthyMeta = new ArrayList<>(); + List summary = new ArrayList<>(); + + try { + ContainerSchemaDefinitionV2.UnHealthyContainerStates v2State = null; + + if (state != null) { + // Convert V1 state string to V2 enum + v2State = ContainerSchemaDefinitionV2.UnHealthyContainerStates.valueOf(state); + } + + // Get summary from V2 table and convert to V1 format + List v2Summary = + containerHealthSchemaManagerV2.getUnhealthyContainersSummary(); + for (ContainerHealthSchemaManagerV2.UnhealthyContainersSummaryV2 s : v2Summary) { + summary.add(new UnhealthyContainersSummary(s.getContainerState(), s.getCount())); + } + + // Get containers from V2 table + List v2Containers = + containerHealthSchemaManagerV2.getUnhealthyContainers(v2State, minContainerId, maxContainerId, limit); + + // Convert V2 records to response format + for (ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2 c : v2Containers) { + long containerID = c.getContainerId(); + ContainerInfo containerInfo = + containerManager.getContainer(ContainerID.valueOf(containerID)); + long keyCount = containerInfo.getNumberOfKeys(); + UUID pipelineID = containerInfo.getPipelineID().getId(); + List datanodes = + containerManager.getLatestContainerHistory(containerID, + containerInfo.getReplicationConfig().getRequiredNodes()); + + // Create UnhealthyContainers POJO from V2 record for response + UnhealthyContainers v1Container = new UnhealthyContainers(); + v1Container.setContainerId(c.getContainerId()); + v1Container.setContainerState(c.getContainerState()); + v1Container.setInStateSince(c.getInStateSince()); + v1Container.setExpectedReplicaCount(c.getExpectedReplicaCount()); + v1Container.setActualReplicaCount(c.getActualReplicaCount()); + v1Container.setReplicaDelta(c.getReplicaDelta()); + v1Container.setReason(c.getReason()); + + unhealthyMeta.add(new UnhealthyContainerMetadata( + v1Container, datanodes, pipelineID, keyCount)); + } + } catch (IOException ex) { + throw new WebApplicationException(ex, + Response.Status.INTERNAL_SERVER_ERROR); + } catch (IllegalArgumentException e) { + throw new WebApplicationException(e, Response.Status.BAD_REQUEST); + } + + UnhealthyContainersResponse response = + new UnhealthyContainersResponse(unhealthyMeta); + if (!unhealthyMeta.isEmpty()) { + response.setFirstKey(unhealthyMeta.stream().map(UnhealthyContainerMetadata::getContainerID) + .min(Long::compareTo).orElse(0L)); + response.setLastKey(unhealthyMeta.stream().map(UnhealthyContainerMetadata::getContainerID) + .max(Long::compareTo).orElse(0L)); + } + for (UnhealthyContainersSummary s : summary) { + response.setSummaryCount(s.getContainerState(), s.getCount()); + } + return Response.ok(response).build(); + } + /** * Return * {@link org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java new file mode 100644 index 000000000000..1c2c4f7ae304 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -0,0 +1,425 @@ +/* + * 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 java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.ContainerID; +import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ContainerManager; +import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; +import org.apache.hadoop.hdds.scm.container.ContainerReplica; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport.HealthState; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.scm.ReconScmTask; +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.ReconTaskStatusUpdaterManager; +import org.apache.hadoop.util.Time; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; + +/** + * V2 implementation of Container Health Task that uses SCM's ReplicationManager + * as the single source of truth for container health status. + * + * This is an independent task (does NOT extend ContainerHealthTask) that: + * 1. Uses UNHEALTHY_CONTAINERS_V2 table for storage + * 2. Queries SCM for authoritative health status per container + * 3. Performs two-way synchronization: + * a) Validates Recon's containers against SCM + * b) Ensures Recon has all containers that SCM knows about + * 4. Implements REPLICA_MISMATCH detection locally (SCM doesn't track checksums) + */ +public class ContainerHealthTaskV2 extends ReconScmTask { + + private static final Logger LOG = + LoggerFactory.getLogger(ContainerHealthTaskV2.class); + + private final StorageContainerServiceProvider scmClient; + private final ContainerManager containerManager; + private final ContainerHealthSchemaManagerV2 schemaManagerV2; + private final ReconContainerMetadataManager reconContainerMetadataManager; + private final PlacementPolicy placementPolicy; + private final OzoneConfiguration conf; + private final long interval; + + @Inject + public ContainerHealthTaskV2( + ContainerManager containerManager, + StorageContainerServiceProvider scmClient, + ContainerHealthSchemaManagerV2 schemaManagerV2, + PlacementPolicy placementPolicy, + ReconContainerMetadataManager reconContainerMetadataManager, + OzoneConfiguration conf, + ReconTaskConfig reconTaskConfig, + ReconTaskStatusUpdaterManager taskStatusUpdaterManager) { + super(taskStatusUpdaterManager); + this.scmClient = scmClient; + this.containerManager = containerManager; + this.schemaManagerV2 = schemaManagerV2; + this.reconContainerMetadataManager = reconContainerMetadataManager; + this.placementPolicy = placementPolicy; + this.conf = conf; + this.interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); + LOG.info("Initialized ContainerHealthTaskV2 with SCM-based two-way sync, interval={}ms", interval); + } + + @Override + protected void run() { + while (canRun()) { + try { + initializeAndRunTask(); + + // Wait before next run using configured interval + synchronized (this) { + wait(interval); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.info("ContainerHealthTaskV2 interrupted"); + break; + } catch (Exception e) { + LOG.error("Error in ContainerHealthTaskV2", e); + } + } + } + + /** + * Main task execution - performs two-way synchronization with SCM. + */ + @Override + protected void runTask() throws Exception { + LOG.info("ContainerHealthTaskV2 starting - two-way sync with SCM"); + + long startTime = Time.monotonicNow(); + long currentTime = System.currentTimeMillis(); + + try { + // Part 1: For each container Recon has, check status with SCM + processReconContainersAgainstSCM(currentTime); + LOG.debug("Recon to SCM validation completed in {} ms", + Time.monotonicNow() - startTime); + + // Part 2: Get all containers from SCM and ensure Recon has them + startTime = Time.monotonicNow(); + processSCMContainersAgainstRecon(currentTime); + LOG.debug("SCM to Recon synchronization completed in {} ms", + Time.monotonicNow() - startTime); + + } catch (IOException e) { + LOG.error("Failed during ContainerHealthTaskV2 execution", e); + throw e; + } catch (Exception e) { + LOG.error("Unexpected error during ContainerHealthTaskV2 execution", e); + throw e; + } + + LOG.info("ContainerHealthTaskV2 completed successfully"); + } + + /** + * Part 1: For each container Recon has, check its health status with SCM. + * This validates Recon's container superset against SCM's authoritative state. + * Process containers in batches to avoid OOM. + */ + private void processReconContainersAgainstSCM(long currentTime) + throws IOException { + + LOG.info("Starting Recon to SCM container validation (batch processing)"); + + int validatedCount = 0; + int errorCount = 0; + int batchSize = 100; // Process 100 containers at a time + long startContainerID = 0; + + while (true) { + // Get a batch of containers + List batch = containerManager.getContainers( + ContainerID.valueOf(startContainerID), batchSize); + + if (batch.isEmpty()) { + LOG.info("Containers not found in Recon beyond ID {}", startContainerID); + break; + } + + // Process this batch + for (ContainerInfo container : batch) { + // Only process CLOSED, QUASI_CLOSED, and CLOSING containers + HddsProtos.LifeCycleState state = container.getState(); + if (state != HddsProtos.LifeCycleState.CLOSED && + state != HddsProtos.LifeCycleState.QUASI_CLOSED && + state != HddsProtos.LifeCycleState.CLOSING) { + LOG.info("Container {} not in CLOSED, QUASI_CLOSED or CLOSING state.", container); + continue; + } + + try { + // Ask SCM: What's the health status of this container? + ReplicationManagerReport report = + scmClient.checkContainerStatus(container); + LOG.info("Container {} check status {}", container.getContainerID(), report); + + // Update Recon's V2 table based on SCM's answer + syncContainerHealthToDatabase(container, report, currentTime); + + validatedCount++; + + } catch (ContainerNotFoundException e) { + // Container exists in Recon but not in SCM + LOG.warn("Container {} exists in Recon but not in SCM - removing from V2", + container.getContainerID()); + schemaManagerV2.deleteAllStatesForContainer(container.getContainerID()); + errorCount++; + } catch (Exception e) { + LOG.error("Error checking container {} status with SCM", + container.getContainerID(), e); + errorCount++; + } + } + + // Move to next batch - start after the last container we saw + long lastContainerID = batch.get(batch.size() - 1).getContainerID(); + startContainerID = lastContainerID + 1; + } + + LOG.info("Recon to SCM validation complete: validated={}, errors={}", + validatedCount, errorCount); + } + + /** + * Part 2: Get all CLOSED containers from SCM and ensure Recon has them. + * This ensures Recon doesn't miss any containers that SCM knows about. + * Process containers in batches to avoid OOM. + */ + private void processSCMContainersAgainstRecon(long currentTime) + throws IOException { + + LOG.info("Starting SCM to Recon container synchronization (batch processing)"); + + int existsInBoth = 0; + int missingInRecon = 0; + int totalProcessed = 0; + long startId = 0; + int batchSize = 1000; + + while (true) { + // Get a batch of CLOSED containers from SCM + List batch = scmClient.getListOfContainers( + startId, batchSize, HddsProtos.LifeCycleState.CLOSED); + + if (batch.isEmpty()) { + break; + } + + // Process this batch + for (ContainerInfo scmContainer : batch) { + try { + // Check if Recon already has this container + containerManager.getContainer(scmContainer.containerID()); + LOG.info("Container {} check status {}", scmContainer.getContainerID(), scmContainer.getState()); + existsInBoth++; + // Container exists in both - already handled in Part 1 + + } catch (ContainerNotFoundException e) { + // Container exists in SCM but not in Recon + LOG.info("Container {} exists in SCM but not in Recon - " + + "this is expected if Recon's DB sync is behind", + scmContainer.getContainerID()); + missingInRecon++; + } + } + + totalProcessed += batch.size(); + startId = batch.get(batch.size() - 1).getContainerID() + 1; + LOG.info("SCM to Recon sync processed {} containers, next startId: {}", + totalProcessed, startId); + } + + LOG.info("SCM to Recon sync complete: totalProcessed={}, existsInBoth={}, onlyInSCM={}", + totalProcessed, existsInBoth, missingInRecon); + } + + /** + * Sync container health state to V2 database based on SCM's ReplicationManager report. + */ + private void syncContainerHealthToDatabase( + ContainerInfo container, + ReplicationManagerReport report, + long currentTime) throws IOException { + + List recordsToInsert = new ArrayList<>(); + boolean isHealthy = true; + + // Get replicas for building records + Set replicas = + containerManager.getContainerReplicas(container.containerID()); + int actualReplicaCount = replicas.size(); + int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); + + // Check each health state from SCM's report + if (report.getStat(HealthState.MISSING) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MISSING, + currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); + isHealthy = false; + } + + if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.UNDER_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); + isHealthy = false; + } + + if (report.getStat(HealthState.OVER_REPLICATED) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.OVER_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); + isHealthy = false; + } + + if (report.getStat(HealthState.MIS_REPLICATED) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MIS_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); + isHealthy = false; + } + + // Insert/update unhealthy records + if (!recordsToInsert.isEmpty()) { + schemaManagerV2.insertUnhealthyContainerRecords(recordsToInsert); + } + + // Check REPLICA_MISMATCH locally (SCM doesn't track data checksums) + checkAndUpdateReplicaMismatch(container, replicas, currentTime, + expectedReplicaCount, actualReplicaCount); + + // If healthy according to SCM and no REPLICA_MISMATCH, remove from V2 table + // (except REPLICA_MISMATCH which is handled separately) + if (isHealthy) { + // Remove SCM-tracked states, but keep REPLICA_MISMATCH if it exists + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.MISSING); + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.UNDER_REPLICATED); + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.OVER_REPLICATED); + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.MIS_REPLICATED); + } + } + + /** + * Check for REPLICA_MISMATCH locally (SCM doesn't track data checksums). + * This compares checksums across replicas to detect data inconsistencies. + */ + private void checkAndUpdateReplicaMismatch( + ContainerInfo container, + Set replicas, + long currentTime, + int expectedReplicaCount, + int actualReplicaCount) { + + try { + // Check if replicas have mismatched checksums + boolean hasMismatch = hasDataChecksumMismatch(replicas); + + if (hasMismatch) { + UnhealthyContainerRecordV2 record = createRecord( + container, + UnHealthyContainerStates.REPLICA_MISMATCH, + currentTime, + expectedReplicaCount, + actualReplicaCount, + "Checksum mismatch detected by Recon"); + + List records = new ArrayList<>(); + records.add(record); + schemaManagerV2.insertUnhealthyContainerRecords(records); + } else { + // No mismatch - remove REPLICA_MISMATCH state if it exists + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.REPLICA_MISMATCH); + } + + } catch (Exception e) { + LOG.warn("Error checking replica mismatch for container {}", + container.getContainerID(), e); + } + } + + /** + * Check if replicas have mismatched data checksums. + */ + private boolean hasDataChecksumMismatch(Set replicas) { + if (replicas == null || replicas.size() <= 1) { + return false; // Can't have mismatch with 0 or 1 replica + } + + // Get first checksum as reference + Long referenceChecksum = null; + for (ContainerReplica replica : replicas) { + long checksum = replica.getDataChecksum(); + if (checksum == 0) { + continue; // Skip replicas without checksum + } + if (referenceChecksum == null) { + referenceChecksum = checksum; + } else if (referenceChecksum != checksum) { + return true; // Found mismatch + } + } + + return false; + } + + /** + * Create an unhealthy container record. + */ + private UnhealthyContainerRecordV2 createRecord( + ContainerInfo container, + UnHealthyContainerStates state, + long currentTime, + int expectedReplicaCount, + int actualReplicaCount, + String reason) { + + int replicaDelta = actualReplicaCount - expectedReplicaCount; + + return new UnhealthyContainerRecordV2( + container.getContainerID(), + state.toString(), + currentTime, + expectedReplicaCount, + actualReplicaCount, + replicaDelta, + reason + ); + } +} \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java index b7b98ba1f99b..bbafeaa4b83c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java @@ -172,6 +172,47 @@ public void insertUnhealthyContainerRecords(List recs) { } } + /** + * Delete a specific unhealthy container state record. + * + * @param containerId Container ID + * @param state Container state to delete + */ + public void deleteUnhealthyContainer(long containerId, Object state) { + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); + try { + if (state instanceof UnHealthyContainerStates) { + dslContext.deleteFrom(UNHEALTHY_CONTAINERS) + .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.eq(containerId)) + .and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(((UnHealthyContainerStates) state).toString())) + .execute(); + } else { + dslContext.deleteFrom(UNHEALTHY_CONTAINERS) + .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.eq(containerId)) + .and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString())) + .execute(); + } + } catch (Exception e) { + LOG.error("Failed to delete unhealthy container {} state {}", containerId, state, e); + } + } + + /** + * Delete all unhealthy states for a container. + * + * @param containerId Container ID + */ + public void deleteAllStatesForContainer(long containerId) { + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); + try { + dslContext.deleteFrom(UNHEALTHY_CONTAINERS) + .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.eq(containerId)) + .execute(); + } catch (Exception e) { + LOG.error("Failed to delete all states for container {}", containerId, e); + } + } + /** * Clear all unhealthy container records. This is primarily used for testing * to ensure clean state between tests. diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java new file mode 100644 index 000000000000..ecb34f8230aa --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -0,0 +1,307 @@ +/* + * 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.ContainerSchemaDefinitionV2.UNHEALTHY_CONTAINERS_V2_TABLE_NAME; +import static org.apache.ozone.recon.schema.generated.tables.UnhealthyContainersV2Table.UNHEALTHY_CONTAINERS_V2; +import static org.jooq.impl.DSL.count; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; +import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2; +import org.apache.ozone.recon.schema.generated.tables.records.UnhealthyContainersV2Record; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.OrderField; +import org.jooq.Record; +import org.jooq.SelectQuery; +import org.jooq.exception.DataAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manager for UNHEALTHY_CONTAINERS_V2 table used by ContainerHealthTaskV2. + * This is independent from ContainerHealthSchemaManager to allow both + * implementations to run in parallel. + */ +@Singleton +public class ContainerHealthSchemaManagerV2 { + private static final Logger LOG = + LoggerFactory.getLogger(ContainerHealthSchemaManagerV2.class); + + private final UnhealthyContainersV2Dao unhealthyContainersV2Dao; + private final ContainerSchemaDefinitionV2 containerSchemaDefinitionV2; + + @Inject + public ContainerHealthSchemaManagerV2( + ContainerSchemaDefinitionV2 containerSchemaDefinitionV2, + UnhealthyContainersV2Dao unhealthyContainersV2Dao) { + this.unhealthyContainersV2Dao = unhealthyContainersV2Dao; + this.containerSchemaDefinitionV2 = containerSchemaDefinitionV2; + } + + /** + * Insert or update unhealthy container records in V2 table. + * Uses DAO pattern with try-insert-catch-update for Derby compatibility. + */ + public void insertUnhealthyContainerRecords(List recs) { + if (LOG.isDebugEnabled()) { + recs.forEach(rec -> LOG.debug("rec.getContainerId() : {}, rec.getContainerState(): {}", + rec.getContainerId(), rec.getContainerState())); + } + + try (Connection connection = containerSchemaDefinitionV2.getDataSource().getConnection()) { + connection.setAutoCommit(false); // Turn off auto-commit for transactional control + try { + for (UnhealthyContainerRecordV2 rec : recs) { + UnhealthyContainersV2 jooqRec = new UnhealthyContainersV2( + rec.getContainerId(), + rec.getContainerState(), + rec.getInStateSince(), + rec.getExpectedReplicaCount(), + rec.getActualReplicaCount(), + rec.getReplicaDelta(), + rec.getReason()); + + try { + unhealthyContainersV2Dao.insert(jooqRec); + } catch (DataAccessException dataAccessException) { + // Log the error and update the existing record if ConstraintViolationException occurs + unhealthyContainersV2Dao.update(jooqRec); + 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 + } + } catch (Exception e) { + LOG.error("Failed to insert records into {} ", UNHEALTHY_CONTAINERS_V2_TABLE_NAME, e); + throw new RuntimeException("Recon failed to insert " + recs.size() + " unhealthy container records.", e); + } + } + + /** + * Delete a specific unhealthy container record from V2 table. + */ + public void deleteUnhealthyContainer(long containerId, Object state) { + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + try { + String stateStr = (state instanceof UnHealthyContainerStates) + ? ((UnHealthyContainerStates) state).toString() + : state.toString(); + dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) + .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.eq(containerId)) + .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.eq(stateStr)) + .execute(); + LOG.debug("Deleted container {} with state {} from V2 table", containerId, state); + } catch (Exception e) { + LOG.error("Failed to delete container {} from V2 table", containerId, e); + } + } + + /** + * Delete all records for a specific container (all states). + */ + public void deleteAllStatesForContainer(long containerId) { + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + try { + int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) + .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.eq(containerId)) + .execute(); + LOG.debug("Deleted {} records for container {} from V2 table", deleted, containerId); + } catch (Exception e) { + LOG.error("Failed to delete all states for container {} from V2 table", containerId, e); + } + } + + /** + * Get summary of unhealthy containers grouped by state from V2 table. + */ + public List getUnhealthyContainersSummary() { + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + List result = new ArrayList<>(); + + try { + return dslContext + .select(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.as("containerState"), + count().as("cnt")) + .from(UNHEALTHY_CONTAINERS_V2) + .groupBy(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE) + .fetchInto(UnhealthyContainersSummaryV2.class); + } catch (Exception e) { + LOG.error("Failed to get summary from V2 table", e); + return result; + } + } + + /** + * Get unhealthy containers from V2 table. + */ + public List getUnhealthyContainers( + UnHealthyContainerStates state, long minContainerId, long maxContainerId, int limit) { + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + + SelectQuery query = dslContext.selectQuery(); + query.addFrom(UNHEALTHY_CONTAINERS_V2); + + Condition containerCondition; + OrderField[] orderField; + + if (maxContainerId > 0) { + containerCondition = UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.lessThan(maxContainerId); + orderField = new OrderField[]{ + UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.desc(), + UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.asc() + }; + } else { + containerCondition = UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.greaterThan(minContainerId); + orderField = new OrderField[]{ + UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.asc(), + UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.asc() + }; + } + + if (state != null) { + query.addConditions(containerCondition.and( + UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.eq(state.toString()))); + } else { + query.addConditions(containerCondition); + } + + query.addOrderBy(orderField); + query.addLimit(limit); + + try { + return query.fetchInto(UnhealthyContainersV2Record.class).stream() + .sorted(Comparator.comparingLong(UnhealthyContainersV2Record::getContainerId)) + .map(record -> new UnhealthyContainerRecordV2( + 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 V2 table", e); + return new ArrayList<>(); + } + } + + /** + * Clear all records from V2 table (for testing). + */ + public void clearAllUnhealthyContainerRecords() { + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + try { + dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2).execute(); + LOG.info("Cleared all V2 unhealthy container records"); + } catch (Exception e) { + LOG.error("Failed to clear V2 unhealthy container records", e); + } + } + + /** + * POJO representing a record in UNHEALTHY_CONTAINERS_V2 table. + */ + public static class UnhealthyContainerRecordV2 { + 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 UnhealthyContainerRecordV2(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; + } + } + + /** + * POJO representing a summary record for unhealthy containers. + */ + public static class UnhealthyContainersSummaryV2 { + private final String containerState; + private final int count; + + public UnhealthyContainersSummaryV2(String containerState, int count) { + this.containerState = containerState; + this.count = count; + } + + public String getContainerState() { + return containerState; + } + + public int getCount() { + return count; + } + } +} \ No newline at end of file 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 b6b7e3cf5b41..441566660777 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 @@ -110,10 +110,13 @@ import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.hdds.utils.db.Table.KeyValue; import org.apache.hadoop.hdds.utils.db.TableIterator; +import org.apache.hadoop.hdds.recon.ReconConfigKeys; import org.apache.hadoop.ozone.recon.ReconContext; 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.ContainerHealthTaskV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.fsck.ReconSafeModeMgrTask; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; @@ -153,8 +156,10 @@ 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 ReconScmTask containerHealthTaskV2; private final DataSource dataSource; + private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; private DBStore dbStore; private ReconNodeManager nodeManager; @@ -183,7 +188,8 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, ReconSafeModeManager safeModeManager, ReconContext reconContext, DataSource dataSource, - ReconTaskStatusUpdaterManager taskStatusUpdaterManager) + ReconTaskStatusUpdaterManager taskStatusUpdaterManager, + ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2) throws IOException { reconNodeDetails = reconUtils.getReconNodeDetails(conf); this.threadNamePrefix = reconNodeDetails.threadNamePrefix(); @@ -265,13 +271,36 @@ 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 legacy ContainerHealthTask (always runs, writes to UNHEALTHY_CONTAINERS) + LOG.info("Creating ContainerHealthTask (legacy)"); + containerHealthTask = new ContainerHealthTask( + containerManager, + scmServiceProvider, + containerHealthSchemaManager, + containerPlacementPolicy, + reconTaskConfig, + reconContainerMetadataManager, + conf, + taskStatusUpdaterManager + ); + + // Create ContainerHealthTaskV2 (always runs, writes to UNHEALTHY_CONTAINERS_V2) + LOG.info("Creating ContainerHealthTaskV2"); + containerHealthTaskV2 = new ContainerHealthTaskV2( + containerManager, + scmServiceProvider, + containerHealthSchemaManagerV2, + containerPlacementPolicy, + reconContainerMetadataManager, + conf, + reconTaskConfig, + taskStatusUpdaterManager + ); this.containerSizeCountTask = new ContainerSizeCountTask(containerManager, reconTaskConfig, containerCountBySizeDao, utilizationSchemaDefinition, taskStatusUpdaterManager); + this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; this.dataSource = dataSource; StaleNodeHandler staleNodeHandler = @@ -347,6 +376,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, eventQueue.addHandler(SCMEvents.NEW_NODE, newNodeHandler); reconScmTasks.add(pipelineSyncTask); reconScmTasks.add(containerHealthTask); + reconScmTasks.add(containerHealthTaskV2); reconScmTasks.add(containerSizeCountTask); reconSafeModeMgrTask = new ReconSafeModeMgrTask( containerManager, nodeManager, safeModeManager, @@ -711,7 +741,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/spi/StorageContainerServiceProvider.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java index 412bd3027662..72f1a7d6e572 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java @@ -21,6 +21,7 @@ import java.util.List; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline; import org.apache.hadoop.hdds.scm.pipeline.Pipeline; import org.apache.hadoop.hdds.utils.db.DBCheckpoint; @@ -98,4 +99,15 @@ List getListOfContainers(long startContainerID, * @return Total number of containers in SCM. */ long getContainerCount(HddsProtos.LifeCycleState state) throws IOException; + + /** + * Checks the health status of a specific container using SCM's + * ReplicationManager. This allows Recon to query SCM for the + * authoritative health state of individual containers. + * + * @param containerInfo the container to check + * @return ReplicationManagerReport containing health state for this container + * @throws IOException if the check fails or container is not found + */ + ReplicationManagerReport checkContainerStatus(ContainerInfo containerInfo) throws IOException; } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java index 3b6164447b3c..7c79990b625c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java @@ -36,6 +36,7 @@ import org.apache.hadoop.hdds.protocolPB.SCMSecurityProtocolClientSideTranslatorPB; import org.apache.hadoop.hdds.scm.ScmConfigKeys; import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline; import org.apache.hadoop.hdds.scm.ha.InterSCMGrpcClient; import org.apache.hadoop.hdds.scm.ha.SCMSnapshotDownloader; @@ -191,4 +192,9 @@ public List getListOfContainers( return scmClient.getListOfContainers(startContainerID, count, state); } + @Override + public ReplicationManagerReport checkContainerStatus(ContainerInfo containerInfo) throws IOException { + return scmClient.checkContainerStatus(containerInfo); + } + } From 04fa39e10ec8e964420beddfcc680d0fba2bce49 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 24 Oct 2025 15:24:06 +0530 Subject: [PATCH 02/43] HDDS-13891 --- .../recon/fsck/ContainerHealthTaskV2.java | 132 ++++++++++++++---- 1 file changed, 103 insertions(+), 29 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 1c2c4f7ae304..1ea0546314f0 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -217,8 +217,9 @@ private void processReconContainersAgainstSCM(long currentTime) } /** - * Part 2: Get all CLOSED containers from SCM and ensure Recon has them. - * This ensures Recon doesn't miss any containers that SCM knows about. + * Part 2: Get all CLOSED, QUASI_CLOSED, CLOSING containers from SCM and ensure Recon has them. + * For containers missing in Recon, check their health status with SCM and update V2 table. + * This ensures Recon doesn't miss any unhealthy containers that SCM knows about. * Process containers in batches to avoid OOM. */ private void processSCMContainersAgainstRecon(long currentTime) @@ -228,45 +229,118 @@ private void processSCMContainersAgainstRecon(long currentTime) int existsInBoth = 0; int missingInRecon = 0; + int unhealthyInSCM = 0; int totalProcessed = 0; long startId = 0; int batchSize = 1000; - while (true) { - // Get a batch of CLOSED containers from SCM - List batch = scmClient.getListOfContainers( - startId, batchSize, HddsProtos.LifeCycleState.CLOSED); + // Process CLOSED, QUASI_CLOSED, and CLOSING containers from SCM + HddsProtos.LifeCycleState[] statesToProcess = { + HddsProtos.LifeCycleState.CLOSED, + HddsProtos.LifeCycleState.QUASI_CLOSED, + HddsProtos.LifeCycleState.CLOSING + }; - if (batch.isEmpty()) { - break; - } + for (HddsProtos.LifeCycleState state : statesToProcess) { + LOG.info("Processing {} containers from SCM", state); + startId = 0; - // Process this batch - for (ContainerInfo scmContainer : batch) { - try { - // Check if Recon already has this container - containerManager.getContainer(scmContainer.containerID()); - LOG.info("Container {} check status {}", scmContainer.getContainerID(), scmContainer.getState()); - existsInBoth++; - // Container exists in both - already handled in Part 1 + while (true) { + // Get a batch of containers in this state from SCM + List batch = scmClient.getListOfContainers( + startId, batchSize, state); - } catch (ContainerNotFoundException e) { - // Container exists in SCM but not in Recon - LOG.info("Container {} exists in SCM but not in Recon - " + - "this is expected if Recon's DB sync is behind", - scmContainer.getContainerID()); - missingInRecon++; + if (batch.isEmpty()) { + break; } + + // Process this batch + for (ContainerInfo scmContainer : batch) { + try { + // Check if Recon already has this container + containerManager.getContainer(scmContainer.containerID()); + existsInBoth++; + // Container exists in both - already handled in Part 1 + + } catch (ContainerNotFoundException e) { + // Container exists in SCM but not in Recon + // Since SCM is the source of truth, check its health status + LOG.info("Container {} exists in SCM ({}) but not in Recon - checking health status", + scmContainer.getContainerID(), state); + missingInRecon++; + + try { + // Get health status from SCM for this container + ReplicationManagerReport report = + scmClient.checkContainerStatus(scmContainer); + LOG.info("Container {} (missing in Recon) health status from SCM: {}", + scmContainer.getContainerID(), report); + + // Check if this container is unhealthy according to SCM + boolean isUnhealthy = report.getStat(HealthState.MISSING) > 0 || + report.getStat(HealthState.UNDER_REPLICATED) > 0 || + report.getStat(HealthState.OVER_REPLICATED) > 0 || + report.getStat(HealthState.MIS_REPLICATED) > 0; + + if (isUnhealthy) { + // Update V2 table with SCM's health status + // Note: We cannot get replicas from Recon's containerManager since container doesn't exist + // So we'll use SCM's report directly with placeholder values for replica counts + List recordsToInsert = new ArrayList<>(); + int expectedReplicaCount = scmContainer.getReplicationConfig().getRequiredNodes(); + int actualReplicaCount = 0; // Unknown since container not in Recon + + if (report.getStat(HealthState.MISSING) > 0) { + recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.MISSING, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + } + + if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { + recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.UNDER_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + } + + if (report.getStat(HealthState.OVER_REPLICATED) > 0) { + recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.OVER_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + } + + if (report.getStat(HealthState.MIS_REPLICATED) > 0) { + recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.MIS_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + } + + if (!recordsToInsert.isEmpty()) { + schemaManagerV2.insertUnhealthyContainerRecords(recordsToInsert); + unhealthyInSCM++; + LOG.info("Updated V2 table with {} unhealthy states for container {} (missing in Recon)", + recordsToInsert.size(), scmContainer.getContainerID()); + } + } + + } catch (Exception ex) { + LOG.error("Error checking health status for container {} (missing in Recon)", + scmContainer.getContainerID(), ex); + } + } + } + + totalProcessed += batch.size(); + startId = batch.get(batch.size() - 1).getContainerID() + 1; + LOG.debug("SCM to Recon sync processed {} {} containers, next startId: {}", + totalProcessed, state, startId); } - totalProcessed += batch.size(); - startId = batch.get(batch.size() - 1).getContainerID() + 1; - LOG.info("SCM to Recon sync processed {} containers, next startId: {}", - totalProcessed, startId); + LOG.info("Completed processing {} containers from SCM", state); } - LOG.info("SCM to Recon sync complete: totalProcessed={}, existsInBoth={}, onlyInSCM={}", - totalProcessed, existsInBoth, missingInRecon); + LOG.info("SCM to Recon sync complete: totalProcessed={}, existsInBoth={}, " + + "onlyInSCM={}, unhealthyInSCM={}", + totalProcessed, existsInBoth, missingInRecon, unhealthyInSCM); } /** From f065e910f82fd9776e62551168068c0513722c6f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 24 Oct 2025 17:10:18 +0530 Subject: [PATCH 03/43] HDDS-13891 --- ...CMVsReconContainerHealthDiscrepancies.java | 524 ------------------ .../schema/ContainerSchemaDefinitionV2.java | 2 +- .../ozone/recon/api/ContainerEndpoint.java | 4 +- .../recon/fsck/ContainerHealthTaskV2.java | 253 +++++---- .../ContainerHealthSchemaManagerV2.java | 3 +- .../ReconStorageContainerManagerFacade.java | 3 +- 6 files changed, 162 insertions(+), 627 deletions(-) delete mode 100644 hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java deleted file mode 100644 index 38f85a3f7e4f..000000000000 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestSCMVsReconContainerHealthDiscrepancies.java +++ /dev/null @@ -1,524 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hadoop.ozone.recon; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_COMMAND_STATUS_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_HEARTBEAT_INTERVAL; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_NODE_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT; -import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeState.DEAD; -import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DATANODE_ADMIN_MONITOR_INTERVAL; -import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DEADNODE_INTERVAL; -import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HEARTBEAT_PROCESS_INTERVAL; -import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_STALENODE_INTERVAL; -import static org.apache.hadoop.hdds.scm.node.TestNodeUtil.waitForDnToReachHealthState; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.hadoop.hdds.client.RatisReplicationConfig; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; -import org.apache.hadoop.hdds.protocol.DatanodeDetails; -import org.apache.hadoop.hdds.protocol.proto.HddsProtos; -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.ReplicationManagerReport.HealthState; -import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; -import org.apache.hadoop.hdds.scm.node.NodeManager; -import org.apache.hadoop.hdds.scm.server.StorageContainerManager; -import org.apache.hadoop.hdds.utils.IOUtils; -import org.apache.hadoop.ozone.MiniOzoneCluster; -import org.apache.hadoop.ozone.OzoneTestUtils; -import org.apache.hadoop.ozone.TestDataUtil; -import org.apache.hadoop.ozone.client.OzoneBucket; -import org.apache.hadoop.ozone.client.OzoneClient; -import org.apache.hadoop.ozone.client.OzoneKeyDetails; -import org.apache.hadoop.ozone.client.OzoneKeyLocation; -import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; -import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; -import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; -import org.apache.ozone.test.GenericTestUtils; -import org.apache.ozone.test.LambdaTestUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Optional; - -/** - * Integration tests to validate discrepancies between SCM and Recon - * container health reporting for MISSING, UNDER_REPLICATED, OVER_REPLICATED, - * and MIS_REPLICATED containers. - * - * These tests validate the findings from Recon_SCM_Data_Correctness_Analysis.md - * and will be updated after Recon fixes are implemented to verify matching counts. - */ -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class TestSCMVsReconContainerHealthDiscrepancies { - - private static final Logger LOG = - LoggerFactory.getLogger(TestSCMVsReconContainerHealthDiscrepancies.class); - - private static final int DATANODE_COUNT = 5; - private static final RatisReplicationConfig RATIS_THREE = RatisReplicationConfig - .getInstance(HddsProtos.ReplicationFactor.THREE); - - private MiniOzoneCluster cluster; - private OzoneConfiguration conf; - private ReconService recon; - private OzoneClient client; - private OzoneBucket bucket; - - // SCM components - private StorageContainerManager scm; - private ContainerManager scmContainerManager; - private ReplicationManager scmReplicationManager; - private NodeManager scmNodeManager; - - // Recon components - private ReconStorageContainerManagerFacade reconScm; - private ReconContainerManager reconContainerManager; - private ContainerHealthTask reconContainerHealthTask; - private ContainerHealthSchemaManager containerHealthSchemaManager; - - @BeforeEach - public void setup() throws Exception { - conf = new OzoneConfiguration(); - - // ============================================================ - // PHASE 1: BASELINE TEST - Use LEGACY Implementation - // ============================================================ - // Set feature flag to FALSE to use legacy ContainerHealthTask - // This establishes baseline discrepancies between SCM and Recon - conf.setBoolean("ozone.recon.container.health.use.scm.report", false); - LOG.info("=== PHASE 1: Testing with LEGACY ContainerHealthTask (flag=false) ==="); - - // Heartbeat and report intervals - match TestReconTasks pattern - // IMPORTANT: 100ms is too aggressive and causes cluster instability! - conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); - conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); - conf.set("ozone.scm.heartbeat.interval", "1s"); - conf.set("ozone.scm.heartbeat.process.interval", "1s"); - - // Node state transition intervals - match TestReconTasks - conf.set("ozone.scm.stale.node.interval", "6s"); - conf.set("ozone.scm.dead.node.interval", "8s"); // 8s NOT 2s - critical! - conf.setTimeDuration(OZONE_SCM_DATANODE_ADMIN_MONITOR_INTERVAL, 1, SECONDS); - conf.setTimeDuration(HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT, 0, SECONDS); - - // Fast replication manager processing - ReplicationManager.ReplicationManagerConfiguration rmConf = - conf.getObject(ReplicationManager.ReplicationManagerConfiguration.class); - rmConf.setInterval(Duration.ofSeconds(1)); - rmConf.setUnderReplicatedInterval(Duration.ofMillis(100)); - rmConf.setOverReplicatedInterval(Duration.ofMillis(100)); - conf.setFromObject(rmConf); - - // Initialize Recon service - recon = new ReconService(conf); - - // Build cluster with 5 datanodes for flexible replica manipulation - cluster = MiniOzoneCluster.newBuilder(conf) - .setNumDatanodes(DATANODE_COUNT) - .addService(recon) - .build(); - - cluster.waitForClusterToBeReady(); - - // Initialize SCM components - scm = cluster.getStorageContainerManager(); - scmContainerManager = scm.getContainerManager(); - scmReplicationManager = scm.getReplicationManager(); - scmNodeManager = scm.getScmNodeManager(); - - // Initialize Recon components - reconScm = (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); - reconContainerHealthTask = (ContainerHealthTask) reconScm.getContainerHealthTask(); - containerHealthSchemaManager = reconContainerManager.getContainerSchemaManager(); - - // Create client and test bucket - client = cluster.newClient(); - bucket = TestDataUtil.createVolumeAndBucket(client); - - LOG.info("=== Test setup complete: {} datanodes ready ===", DATANODE_COUNT); - } - - @AfterEach - public void tearDown() { - IOUtils.closeQuietly(client); - if (cluster != null) { - cluster.shutdown(); - } - LOG.info("=== Test teardown complete ==="); - } - - /** - * Test Scenario 1A: MISSING Container - All Replicas Lost - * - * This test validates that both SCM and Recon detect containers as MISSING - * when all replicas are lost (all hosting datanodes are dead). - * - * Expected: Both SCM and Recon should report the container as MISSING - * (±5% timing variance is acceptable) - */ - @Test - @Order(1) - public void testMissingContainerAllReplicasLost() throws Exception { - LOG.info("=== TEST 1A: MISSING Container - All Replicas Lost ==="); - - // Step 1: Create a key and close the container - String keyName = "test-missing-all-replicas-" + System.currentTimeMillis(); - TestDataUtil.createKey(bucket, keyName, RATIS_THREE, - "test content for missing".getBytes(StandardCharsets.UTF_8)); - - OzoneKeyDetails keyDetails = bucket.getKey(keyName); - List keyLocations = keyDetails.getOzoneKeyLocations(); - long containerIDLong = keyLocations.get(0).getContainerID(); - ContainerID containerId = ContainerID.valueOf(containerIDLong); - - ContainerInfo containerInfo = scmContainerManager.getContainer(containerId); - LOG.info("Created container: {}, state: {}", containerIDLong, containerInfo.getState()); - - // Close the container to enable health checks - OzoneTestUtils.closeContainer(scm, containerInfo); - LOG.info("Closed container: {}", containerIDLong); - - // Step 2: Get all datanodes hosting this container - Set replicas = scmContainerManager.getContainerReplicas(containerId); - List hostingDatanodes = replicas.stream() - .map(ContainerReplica::getDatanodeDetails) - .collect(Collectors.toList()); - - assertEquals(3, hostingDatanodes.size(), - "Container should have 3 replicas (replication factor 3)"); - LOG.info("Container {} has replicas on datanodes: {}", - containerIDLong, hostingDatanodes); - - // Step 3: Shutdown all datanodes hosting replicas - LOG.info("Shutting down all {} datanodes hosting container {}", - hostingDatanodes.size(), containerIDLong); - for (DatanodeDetails dn : hostingDatanodes) { - LOG.info("Shutting down datanode: {}", dn.getUuidString()); - cluster.shutdownHddsDatanode(dn); - waitForDnToReachHealthState(scmNodeManager, dn, DEAD); - LOG.info("Datanode {} is now DEAD", dn.getUuidString()); - } - - // Step 4: Wait for SCM ReplicationManager to detect MISSING container - LOG.info("Waiting for SCM to detect container {} as MISSING", containerIDLong); - GenericTestUtils.waitFor(() -> { - try { - ReplicationManagerReport report = new ReplicationManagerReport(); - scmReplicationManager.checkContainerStatus( - scmContainerManager.getContainer(containerId), report); - long missingCount = report.getStat(HealthState.MISSING); - LOG.debug("SCM MISSING count for container {}: {}", containerIDLong, missingCount); - return missingCount > 0; - } catch (Exception e) { - LOG.error("Error checking SCM container status", e); - return false; - } - }, 500, 30000); - - // Step 5: Get SCM report - ReplicationManagerReport scmReport = new ReplicationManagerReport(); - scmReplicationManager.checkContainerStatus( - scmContainerManager.getContainer(containerId), scmReport); - - long scmMissingCount = scmReport.getStat(HealthState.MISSING); - LOG.info("SCM Reports: MISSING={}", scmMissingCount); - assertEquals(1, scmMissingCount, "SCM should report 1 MISSING container"); - - // Step 6: Trigger Recon ContainerHealthTask - LOG.info("Triggering Recon ContainerHealthTask"); - reconContainerHealthTask.run(); - - // Step 7: Wait for Recon to process and update - Thread.sleep(2000); // Give Recon time to process - - // Step 8: Get Recon report via ContainerHealthSchemaManager - List missingContainers = - containerHealthSchemaManager.getUnhealthyContainers( - UnHealthyContainerStates.MISSING, 0L, Optional.empty(), 1000); - - int reconMissingCount = missingContainers.size(); - LOG.info("Recon Reports: MISSING={}", reconMissingCount); - - // Step 9: Compare and validate - LOG.info("=== COMPARISON: MISSING Containers ==="); - LOG.info("SCM: {}", scmMissingCount); - LOG.info("Recon: {}", reconMissingCount); - - // Both SCM and Recon should detect MISSING containers identically - // MISSING detection is based on all replicas being on dead nodes - assertEquals(scmMissingCount, reconMissingCount, - "MISSING container count should match between SCM and Recon"); - - LOG.info("✓ TEST 1A PASSED: MISSING container detection validated"); - } - - /** - * Test Scenario 2A: UNDER_REPLICATED Container - Simple Case - * - * This test validates that both SCM and Recon detect containers as - * UNDER_REPLICATED when replica count drops below replication factor. - * - * Expected: Both should detect under-replication - * (±10% variance due to config differences is acceptable) - */ - // @Test - // @Order(2) - public void testUnderReplicatedContainerSimple() throws Exception { - LOG.info("=== TEST 2A: UNDER_REPLICATED Container - Simple Case ==="); - - // Step 1: Create key with Ratis THREE replication - String keyName = "test-under-rep-simple-" + System.currentTimeMillis(); - TestDataUtil.createKey(bucket, keyName, RATIS_THREE, - "test content for under-replication".getBytes(StandardCharsets.UTF_8)); - - long containerIDLong = bucket.getKey(keyName) - .getOzoneKeyLocations().get(0).getContainerID(); - ContainerID containerId = ContainerID.valueOf(containerIDLong); - ContainerInfo containerInfo = scmContainerManager.getContainer(containerId); - - // Close container to enable replication processing - OzoneTestUtils.closeContainer(scm, containerInfo); - LOG.info("Created and closed container: {}", containerIDLong); - - // Verify initial replica count - Set initialReplicas = - scmContainerManager.getContainerReplicas(containerId); - assertEquals(3, initialReplicas.size(), "Should start with 3 replicas"); - - // Step 2: Kill ONE datanode to make it under-replicated (2 < 3) - DatanodeDetails datanodeToKill = - initialReplicas.iterator().next().getDatanodeDetails(); - LOG.info("Shutting down datanode: {}", datanodeToKill.getUuidString()); - - cluster.shutdownHddsDatanode(datanodeToKill); - waitForDnToReachHealthState(scmNodeManager, datanodeToKill, DEAD); - LOG.info("Datanode {} is now DEAD", datanodeToKill.getUuidString()); - - // Step 3: Wait for replication manager to detect under-replication - LOG.info("Waiting for SCM to detect under-replication"); - GenericTestUtils.waitFor(() -> { - try { - Set currentReplicas = - scmContainerManager.getContainerReplicas(containerId); - // Filter out dead datanode replicas - long healthyCount = currentReplicas.stream() - .filter(r -> !r.getDatanodeDetails().equals(datanodeToKill)) - .count(); - LOG.debug("Healthy replica count: {}", healthyCount); - return healthyCount < 3; // Under-replicated - } catch (Exception e) { - LOG.error("Error checking replica count", e); - return false; - } - }, 500, 30000); - - // Step 4: Check SCM status - ReplicationManagerReport scmReport = new ReplicationManagerReport(); - scmReplicationManager.checkContainerStatus(containerInfo, scmReport); - - long scmUnderRepCount = scmReport.getStat(HealthState.UNDER_REPLICATED); - LOG.info("SCM Reports: UNDER_REPLICATED={}", scmUnderRepCount); - assertEquals(1, scmUnderRepCount, - "SCM should report 1 UNDER_REPLICATED container"); - - // Step 5: Trigger Recon and verify - LOG.info("Triggering Recon ContainerHealthTask"); - reconContainerHealthTask.run(); - Thread.sleep(2000); - - List underReplicatedContainers = - containerHealthSchemaManager.getUnhealthyContainers( - UnHealthyContainerStates.UNDER_REPLICATED, 0L, Optional.empty(), 1000); - - int reconUnderRepCount = underReplicatedContainers.size(); - LOG.info("Recon Reports: UNDER_REPLICATED={}", reconUnderRepCount); - - // Step 6: Compare and validate - LOG.info("=== COMPARISON: UNDER_REPLICATED Containers ==="); - LOG.info("SCM: {}", scmUnderRepCount); - LOG.info("Recon: {}", reconUnderRepCount); - - // Both SCM and Recon should detect UNDER_REPLICATED containers identically - // UNDER_REPLICATED detection is based on healthy replica count < replication factor - assertEquals(scmUnderRepCount, reconUnderRepCount, - "UNDER_REPLICATED container count should match between SCM and Recon"); - - LOG.info("✓ TEST 2A PASSED: UNDER_REPLICATED container detection validated"); - } - - /** - * Test Scenario 3A: OVER_REPLICATED Container - Healthy Excess Replicas - * - * This test simulates over-replication by bringing a datanode back online - * after a new replica was created on another node. - * - * Expected: Both SCM and Recon should detect over-replication - * (Phase 1 check - healthy replicas only) - */ - // @Test - // @Order(3) - public void testOverReplicatedWithHealthyReplicas() throws Exception { - LOG.info("=== TEST 3A: OVER_REPLICATED Container - Healthy Excess ==="); - - // Step 1: Create and close container - String keyName = "test-over-rep-healthy-" + System.currentTimeMillis(); - TestDataUtil.createKey(bucket, keyName, RATIS_THREE, - "test content for over-replication".getBytes(StandardCharsets.UTF_8)); - - long containerIDLong = bucket.getKey(keyName) - .getOzoneKeyLocations().get(0).getContainerID(); - ContainerID containerId = ContainerID.valueOf(containerIDLong); - ContainerInfo containerInfo = scmContainerManager.getContainer(containerId); - - OzoneTestUtils.closeContainer(scm, containerInfo); - LOG.info("Created and closed container: {}", containerIDLong); - - // Start with 3 healthy replicas - Set initialReplicas = - scmContainerManager.getContainerReplicas(containerId); - assertEquals(3, initialReplicas.size(), "Should have 3 replicas initially"); - - // Step 2: Simulate scenario - shutdown one DN, wait for replication, restart - DatanodeDetails dnToRestart = - initialReplicas.iterator().next().getDatanodeDetails(); - LOG.info("Shutting down datanode {} to trigger replication", - dnToRestart.getUuidString()); - - cluster.shutdownHddsDatanode(dnToRestart); - waitForDnToReachHealthState(scmNodeManager, dnToRestart, DEAD); - - // Step 3: Wait for RM to schedule replication - LOG.info("Waiting for ReplicationManager to schedule replication"); - long initialReplicationCmds = scmReplicationManager.getMetrics() - .getReplicationCmdsSentTotal(); - - GenericTestUtils.waitFor(() -> { - long currentCmds = scmReplicationManager.getMetrics() - .getReplicationCmdsSentTotal(); - LOG.debug("Replication commands sent: {} (initial: {})", - currentCmds, initialReplicationCmds); - return currentCmds > initialReplicationCmds; - }, 1000, 60000); - - LOG.info("Replication command sent, waiting for new replica creation"); - - // Step 4: Wait for new replica to be created - GenericTestUtils.waitFor(() -> { - try { - Set currentReplicas = - scmContainerManager.getContainerReplicas(containerId); - long healthyCount = currentReplicas.stream() - .filter(r -> !r.getDatanodeDetails().equals(dnToRestart)) - .count(); - LOG.debug("Healthy replica count (excluding dead node): {}", healthyCount); - return healthyCount >= 3; // New replica created - } catch (Exception e) { - return false; - } - }, 1000, 60000); - - LOG.info("New replica created, restarting original datanode"); - - // Step 5: Restart the original datanode (now we have 4 replicas) - cluster.restartHddsDatanode(dnToRestart, true); - LOG.info("Restarted datanode {}", dnToRestart.getUuidString()); - - // Wait for original replica to be reported back - GenericTestUtils.waitFor(() -> { - try { - Set currentReplicas = - scmContainerManager.getContainerReplicas(containerId); - int replicaCount = currentReplicas.size(); - LOG.debug("Total replica count: {}", replicaCount); - return replicaCount >= 4; // Over-replicated! - } catch (Exception e) { - return false; - } - }, 1000, 30000); - - // Step 6: Verify SCM detects over-replication - LOG.info("Checking SCM for over-replication detection"); - ReplicationManagerReport scmReport = new ReplicationManagerReport(); - scmReplicationManager.checkContainerStatus(containerInfo, scmReport); - - long scmOverRepCount = scmReport.getStat(HealthState.OVER_REPLICATED); - LOG.info("SCM Reports: OVER_REPLICATED={}", scmOverRepCount); - assertEquals(1, scmOverRepCount, - "SCM should report 1 OVER_REPLICATED container"); - - // Step 7: Verify Recon detects it too (Phase 1 check matches) - LOG.info("Triggering Recon ContainerHealthTask"); - reconContainerHealthTask.run(); - Thread.sleep(2000); - - List overReplicatedContainers = - containerHealthSchemaManager.getUnhealthyContainers( - UnHealthyContainerStates.OVER_REPLICATED, 0L, Optional.empty(), 1000); - - int reconOverRepCount = overReplicatedContainers.size(); - LOG.info("Recon Reports: OVER_REPLICATED={}", reconOverRepCount); - - // Step 8: Compare and validate - LOG.info("=== COMPARISON: OVER_REPLICATED Containers (Healthy Excess) ==="); - LOG.info("SCM: {}", scmOverRepCount); - LOG.info("Recon: {}", reconOverRepCount); - - // For this scenario (Phase 1 check - all replicas healthy), both SCM and Recon - // use the same logic and should detect over-replication identically - assertEquals(scmOverRepCount, reconOverRepCount, - "OVER_REPLICATED container count should match for Phase 1 check (healthy replicas only)"); - - LOG.info("✓ TEST 3A PASSED: OVER_REPLICATED (healthy excess) detection validated"); - } - - /** - * TODO: Test Scenario 1B: EC Container MISSING (Insufficient Data Blocks) - * TODO: Test Scenario 2B: UNDER_REPLICATED with Maintenance Nodes - * TODO: Test Scenario 3B: OVER_REPLICATED with Unhealthy Replicas (CRITICAL) - * TODO: Test Scenario 4A: MIS_REPLICATED (Rack Awareness Violation) - * TODO: Test Scenario 4B: MIS_REPLICATED with Unhealthy Replicas - * - * These tests will be added in subsequent commits. - */ -} \ No newline at end of file diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java index 477abd547b40..3d9c5cc2b651 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java @@ -106,4 +106,4 @@ public enum UnHealthyContainerStates { MIS_REPLICATED, // From SCM ReplicationManager REPLICA_MISMATCH // Computed locally by Recon (SCM doesn't track checksums) } -} \ No newline at end of file +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index c92ae05ce134..389129f15e2d 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -49,7 +49,9 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; +import org.apache.hadoop.hdds.recon.ReconConfigKeys; import org.apache.hadoop.hdds.scm.container.ContainerID; import org.apache.hadoop.hdds.scm.container.ContainerInfo; import org.apache.hadoop.hdds.scm.pipeline.Pipeline; @@ -75,8 +77,6 @@ import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; -import org.apache.hadoop.hdds.recon.ReconConfigKeys; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 1ea0546314f0..fc9be0a54d24 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -19,11 +19,9 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; - +import javax.inject.Inject; import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; import org.apache.hadoop.hdds.scm.PlacementPolicy; @@ -46,8 +44,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; - /** * V2 implementation of Container Health Task that uses SCM's ReplicationManager * as the single source of truth for container health status. @@ -74,6 +70,7 @@ public class ContainerHealthTaskV2 extends ReconScmTask { private final long interval; @Inject + @SuppressWarnings("checkstyle:ParameterNumber") public ContainerHealthTaskV2( ContainerManager containerManager, StorageContainerServiceProvider scmClient, @@ -148,7 +145,7 @@ protected void runTask() throws Exception { } /** - * Part 1: For each container Recon has, check its health status with SCM. + * Part 1: For each container Recon has, sync its health status with SCM. * This validates Recon's container superset against SCM's authoritative state. * Process containers in batches to avoid OOM. */ @@ -157,7 +154,7 @@ private void processReconContainersAgainstSCM(long currentTime) LOG.info("Starting Recon to SCM container validation (batch processing)"); - int validatedCount = 0; + int syncedCount = 0; int errorCount = 0; int batchSize = 100; // Process 100 containers at a time long startContainerID = 0; @@ -179,20 +176,15 @@ private void processReconContainersAgainstSCM(long currentTime) if (state != HddsProtos.LifeCycleState.CLOSED && state != HddsProtos.LifeCycleState.QUASI_CLOSED && state != HddsProtos.LifeCycleState.CLOSING) { - LOG.info("Container {} not in CLOSED, QUASI_CLOSED or CLOSING state.", container); + LOG.debug("Container {} in state {} - skipping (not CLOSED/QUASI_CLOSED/CLOSING)", + container.getContainerID(), state); continue; } try { - // Ask SCM: What's the health status of this container? - ReplicationManagerReport report = - scmClient.checkContainerStatus(container); - LOG.info("Container {} check status {}", container.getContainerID(), report); - - // Update Recon's V2 table based on SCM's answer - syncContainerHealthToDatabase(container, report, currentTime); - - validatedCount++; + // Sync this container's health status with SCM (source of truth) + syncContainerWithSCM(container, currentTime, true); + syncedCount++; } catch (ContainerNotFoundException e) { // Container exists in Recon but not in SCM @@ -201,7 +193,7 @@ private void processReconContainersAgainstSCM(long currentTime) schemaManagerV2.deleteAllStatesForContainer(container.getContainerID()); errorCount++; } catch (Exception e) { - LOG.error("Error checking container {} status with SCM", + LOG.error("Error syncing container {} with SCM", container.getContainerID(), e); errorCount++; } @@ -212,13 +204,13 @@ private void processReconContainersAgainstSCM(long currentTime) startContainerID = lastContainerID + 1; } - LOG.info("Recon to SCM validation complete: validated={}, errors={}", - validatedCount, errorCount); + LOG.info("Recon to SCM validation complete: synced={}, errors={}", + syncedCount, errorCount); } /** - * Part 2: Get all CLOSED, QUASI_CLOSED, CLOSING containers from SCM and ensure Recon has them. - * For containers missing in Recon, check their health status with SCM and update V2 table. + * Part 2: Get all CLOSED, QUASI_CLOSED, CLOSING containers from SCM and sync with V2 table. + * For all containers (both in Recon and not in Recon), sync their health status with SCM. * This ensures Recon doesn't miss any unhealthy containers that SCM knows about. * Process containers in batches to avoid OOM. */ @@ -227,9 +219,9 @@ private void processSCMContainersAgainstRecon(long currentTime) LOG.info("Starting SCM to Recon container synchronization (batch processing)"); - int existsInBoth = 0; + int syncedCount = 0; int missingInRecon = 0; - int unhealthyInSCM = 0; + int errorCount = 0; int totalProcessed = 0; long startId = 0; int batchSize = 1000; @@ -256,80 +248,38 @@ private void processSCMContainersAgainstRecon(long currentTime) // Process this batch for (ContainerInfo scmContainer : batch) { - try { - // Check if Recon already has this container - containerManager.getContainer(scmContainer.containerID()); - existsInBoth++; - // Container exists in both - already handled in Part 1 - - } catch (ContainerNotFoundException e) { - // Container exists in SCM but not in Recon - // Since SCM is the source of truth, check its health status - LOG.info("Container {} exists in SCM ({}) but not in Recon - checking health status", - scmContainer.getContainerID(), state); - missingInRecon++; + totalProcessed++; + try { + // Check if Recon has this container + ContainerInfo reconContainer = null; + boolean existsInRecon = true; try { - // Get health status from SCM for this container - ReplicationManagerReport report = - scmClient.checkContainerStatus(scmContainer); - LOG.info("Container {} (missing in Recon) health status from SCM: {}", - scmContainer.getContainerID(), report); - - // Check if this container is unhealthy according to SCM - boolean isUnhealthy = report.getStat(HealthState.MISSING) > 0 || - report.getStat(HealthState.UNDER_REPLICATED) > 0 || - report.getStat(HealthState.OVER_REPLICATED) > 0 || - report.getStat(HealthState.MIS_REPLICATED) > 0; - - if (isUnhealthy) { - // Update V2 table with SCM's health status - // Note: We cannot get replicas from Recon's containerManager since container doesn't exist - // So we'll use SCM's report directly with placeholder values for replica counts - List recordsToInsert = new ArrayList<>(); - int expectedReplicaCount = scmContainer.getReplicationConfig().getRequiredNodes(); - int actualReplicaCount = 0; // Unknown since container not in Recon - - if (report.getStat(HealthState.MISSING) > 0) { - recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.MISSING, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - } - - if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { - recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.UNDER_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - } - - if (report.getStat(HealthState.OVER_REPLICATED) > 0) { - recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.OVER_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - } - - if (report.getStat(HealthState.MIS_REPLICATED) > 0) { - recordsToInsert.add(createRecord(scmContainer, UnHealthyContainerStates.MIS_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - } - - if (!recordsToInsert.isEmpty()) { - schemaManagerV2.insertUnhealthyContainerRecords(recordsToInsert); - unhealthyInSCM++; - LOG.info("Updated V2 table with {} unhealthy states for container {} (missing in Recon)", - recordsToInsert.size(), scmContainer.getContainerID()); - } - } - - } catch (Exception ex) { - LOG.error("Error checking health status for container {} (missing in Recon)", - scmContainer.getContainerID(), ex); + reconContainer = containerManager.getContainer(scmContainer.containerID()); + } catch (ContainerNotFoundException e) { + existsInRecon = false; + missingInRecon++; } + + // Sync with SCM regardless of whether container exists in Recon + // This ensures V2 table always matches SCM's truth + if (existsInRecon) { + // Container exists in Recon - sync using Recon's container info (has replicas) + syncContainerWithSCM(reconContainer, currentTime, true); + } else { + // Container missing in Recon - sync using SCM's container info (no replicas available) + syncContainerWithSCM(scmContainer, currentTime, false); + } + + syncedCount++; + + } catch (Exception ex) { + LOG.error("Error syncing container {} from SCM", + scmContainer.getContainerID(), ex); + errorCount++; } } - totalProcessed += batch.size(); startId = batch.get(batch.size() - 1).getContainerID() + 1; LOG.debug("SCM to Recon sync processed {} {} containers, next startId: {}", totalProcessed, state, startId); @@ -338,13 +288,43 @@ private void processSCMContainersAgainstRecon(long currentTime) LOG.info("Completed processing {} containers from SCM", state); } - LOG.info("SCM to Recon sync complete: totalProcessed={}, existsInBoth={}, " + - "onlyInSCM={}, unhealthyInSCM={}", - totalProcessed, existsInBoth, missingInRecon, unhealthyInSCM); + LOG.info("SCM to Recon sync complete: totalProcessed={}, synced={}, " + + "missingInRecon={}, errors={}", + totalProcessed, syncedCount, missingInRecon, errorCount); + } + + /** + * Sync a single container's health status with SCM (single source of truth). + * This method queries SCM for the container's health status and updates the V2 table accordingly. + * + * @param container The container to sync + * @param currentTime Current timestamp + * @param canAccessReplicas Whether we can access replicas from Recon's containerManager + * (true if container exists in Recon, false if only in SCM) + * @throws IOException if SCM communication fails + */ + private void syncContainerWithSCM( + ContainerInfo container, + long currentTime, + boolean canAccessReplicas) throws IOException { + + // Get SCM's authoritative health status for this container + ReplicationManagerReport report = scmClient.checkContainerStatus(container); + LOG.debug("Container {} health status from SCM: {}", container.getContainerID(), report); + + // Sync to V2 table based on SCM's report + if (canAccessReplicas) { + // Container exists in Recon - we can access replicas for accurate counts and REPLICA_MISMATCH check + syncContainerHealthToDatabase(container, report, currentTime); + } else { + // Container doesn't exist in Recon - sync without replica information + syncContainerHealthToDatabaseWithoutReplicas(container, report, currentTime); + } } /** * Sync container health state to V2 database based on SCM's ReplicationManager report. + * This version is used when container exists in Recon and we can access replicas. */ private void syncContainerHealthToDatabase( ContainerInfo container, @@ -409,9 +389,90 @@ private void syncContainerHealthToDatabase( } } + /** + * Sync container health state to V2 database for containers NOT in Recon. + * This version handles containers that only exist in SCM (no replica access). + */ + private void syncContainerHealthToDatabaseWithoutReplicas( + ContainerInfo container, + ReplicationManagerReport report, + long currentTime) { + + List recordsToInsert = new ArrayList<>(); + boolean isHealthy = true; + + // We cannot get replicas from Recon since container doesn't exist + int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); + int actualReplicaCount = 0; // Unknown + + // Check each health state from SCM's report + if (report.getStat(HealthState.MISSING) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MISSING, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + isHealthy = false; + } + + if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.UNDER_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + isHealthy = false; + } + + if (report.getStat(HealthState.OVER_REPLICATED) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.OVER_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + isHealthy = false; + } + + if (report.getStat(HealthState.MIS_REPLICATED) > 0) { + recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MIS_REPLICATED, + currentTime, expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)")); + isHealthy = false; + } + + // Insert/update unhealthy records + if (!recordsToInsert.isEmpty()) { + try { + schemaManagerV2.insertUnhealthyContainerRecords(recordsToInsert); + LOG.info("Updated V2 table with {} unhealthy states for container {} (not in Recon)", + recordsToInsert.size(), container.getContainerID()); + } catch (Exception e) { + LOG.error("Failed to insert unhealthy records for container {} (not in Recon)", + container.getContainerID(), e); + } + } + + // If healthy according to SCM, remove SCM-tracked states from V2 table + if (isHealthy) { + try { + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.MISSING); + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.UNDER_REPLICATED); + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.OVER_REPLICATED); + schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), + UnHealthyContainerStates.MIS_REPLICATED); + } catch (Exception e) { + LOG.warn("Failed to delete healthy container {} records from V2", + container.getContainerID(), e); + } + } + + // Note: REPLICA_MISMATCH is NOT checked here because: + // - Container doesn't exist in Recon, so we cannot access replicas + // - REPLICA_MISMATCH is Recon-only detection (SCM doesn't track checksums) + // - It will be checked when container eventually syncs to Recon + } + /** * Check for REPLICA_MISMATCH locally (SCM doesn't track data checksums). * This compares checksums across replicas to detect data inconsistencies. + * ONLY called when container exists in Recon and we can access replicas. */ private void checkAndUpdateReplicaMismatch( ContainerInfo container, @@ -496,4 +557,4 @@ private UnhealthyContainerRecordV2 createRecord( reason ); } -} \ No newline at end of file +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index ecb34f8230aa..9067b535144a 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -28,7 +28,6 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; -import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; @@ -304,4 +303,4 @@ public int getCount() { return count; } } -} \ No newline at end of file +} 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 441566660777..8b9815d7150e 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 @@ -110,15 +110,14 @@ import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.hdds.utils.db.Table.KeyValue; import org.apache.hadoop.hdds.utils.db.TableIterator; -import org.apache.hadoop.hdds.recon.ReconConfigKeys; import org.apache.hadoop.ozone.recon.ReconContext; 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.ContainerHealthTaskV2; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.fsck.ReconSafeModeMgrTask; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; import org.apache.hadoop.ozone.recon.tasks.ContainerSizeCountTask; From 2b6de75fb75977ae3e6fc0f06fb2c5a1e365bceb Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 27 Oct 2025 08:51:48 +0530 Subject: [PATCH 04/43] HDDS-13891. PMD issues --- .../hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index fc9be0a54d24..eba32d8b4eb5 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -64,13 +64,9 @@ public class ContainerHealthTaskV2 extends ReconScmTask { private final StorageContainerServiceProvider scmClient; private final ContainerManager containerManager; private final ContainerHealthSchemaManagerV2 schemaManagerV2; - private final ReconContainerMetadataManager reconContainerMetadataManager; - private final PlacementPolicy placementPolicy; - private final OzoneConfiguration conf; private final long interval; @Inject - @SuppressWarnings("checkstyle:ParameterNumber") public ContainerHealthTaskV2( ContainerManager containerManager, StorageContainerServiceProvider scmClient, @@ -84,9 +80,6 @@ public ContainerHealthTaskV2( this.scmClient = scmClient; this.containerManager = containerManager; this.schemaManagerV2 = schemaManagerV2; - this.reconContainerMetadataManager = reconContainerMetadataManager; - this.placementPolicy = placementPolicy; - this.conf = conf; this.interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); LOG.info("Initialized ContainerHealthTaskV2 with SCM-based two-way sync, interval={}ms", interval); } From b0f77aacf5fcfb55212e13493a5d41ae6f976f46 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 27 Oct 2025 10:31:00 +0530 Subject: [PATCH 05/43] HDDS-13891. checkstyle issues --- .../apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index eba32d8b4eb5..aa790117604c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -67,6 +67,7 @@ public class ContainerHealthTaskV2 extends ReconScmTask { private final long interval; @Inject + @SuppressWarnings("checkstyle:ParameterNumber") public ContainerHealthTaskV2( ContainerManager containerManager, StorageContainerServiceProvider scmClient, From 844891e6df7906f8e88ab29b658007f3fcb38513 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Tue, 28 Oct 2025 11:29:53 +0530 Subject: [PATCH 06/43] HDDS-13891. Test code --- .../hadoop/ozone/recon/TestReconTasks.java | 63 +++ .../recon/fsck/TestContainerHealthTaskV2.java | 481 ++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 01caa6f1d945..bfd415ae1e32 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -48,7 +48,9 @@ import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; +import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2; import org.apache.ozone.test.GenericTestUtils; import org.apache.ozone.test.LambdaTestUtils; import org.junit.jupiter.api.AfterEach; @@ -343,4 +345,65 @@ public void testEmptyMissingContainerDownNode() throws Exception { IOUtils.closeQuietly(client); } + + @Test + public void testContainerHealthTaskV2WithSCMSync() throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + + StorageContainerManager scm = cluster.getStorageContainerManager(); + PipelineManager reconPipelineManager = reconScm.getPipelineManager(); + PipelineManager scmPipelineManager = scm.getPipelineManager(); + + // Make sure Recon's pipeline state is initialized. + LambdaTestUtils.await(60000, 5000, + () -> (!reconPipelineManager.getPipelines().isEmpty())); + + ContainerManager scmContainerManager = scm.getContainerManager(); + ReconContainerManager reconContainerManager = + (ReconContainerManager) reconScm.getContainerManager(); + + // Create a container in SCM + ContainerInfo containerInfo = + scmContainerManager.allocateContainer( + RatisReplicationConfig.getInstance(ONE), "test"); + long containerID = containerInfo.getContainerID(); + + Pipeline pipeline = + scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); + runTestOzoneContainerViaDataNode(containerID, client); + + // Make sure Recon got the container report with new container. + assertEquals(scmContainerManager.getContainers(), + reconContainerManager.getContainers()); + + // Bring down the Datanode that had the container replica. + cluster.shutdownHddsDatanode(pipeline.getFirstNode()); + + // V2 task should detect MISSING container from SCM + LambdaTestUtils.await(120000, 6000, () -> { + List allMissingContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, + 0L, Optional.empty(), 1000); + return (allMissingContainers.size() == 1); + }); + + // Restart the Datanode to make sure we remove the missing container. + cluster.restartHddsDatanode(pipeline.getFirstNode(), true); + + LambdaTestUtils.await(120000, 10000, () -> { + List allMissingContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, + 0L, Optional.empty(), 1000); + return (allMissingContainers.isEmpty()); + }); + + IOUtils.closeQuietly(client); + } } diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java new file mode 100644 index 000000000000..e7be9593413e --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java @@ -0,0 +1,481 @@ +/* + * 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.HddsProtos.ReplicationFactor.THREE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +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.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto.State; +import org.apache.hadoop.hdds.scm.PlacementPolicy; +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.ContainerNotFoundException; +import org.apache.hadoop.hdds.scm.container.ContainerReplica; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; +import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +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.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; +import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2; +import org.apache.ozone.test.LambdaTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for ContainerHealthTaskV2 that uses SCM as single source of truth. + */ +public class TestContainerHealthTaskV2 extends AbstractReconSqlDBTest { + + public TestContainerHealthTaskV2() { + super(); + } + + @Test + public void testSCMReportsUnhealthyContainers() throws Exception { + UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = + getDao(UnhealthyContainersV2Dao.class); + + ContainerHealthSchemaManagerV2 schemaManagerV2 = + new ContainerHealthSchemaManagerV2( + getSchemaDefinition(ContainerSchemaDefinitionV2.class), + unHealthyContainersV2TableHandle); + + ContainerManager containerManagerMock = mock(ContainerManager.class); + StorageContainerServiceProvider scmClientMock = + mock(StorageContainerServiceProvider.class); + PlacementPolicy placementPolicyMock = mock(PlacementPolicy.class); + ReconContainerMetadataManager reconContainerMetadataManager = + mock(ReconContainerMetadataManager.class); + + // Create 5 containers in Recon + List mockContainers = getMockContainers(5); + when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) + .thenReturn(mockContainers); + + for (ContainerInfo c : mockContainers) { + when(containerManagerMock.getContainer(c.containerID())).thenReturn(c); + } + + // Container 1: SCM reports UNDER_REPLICATED + ContainerInfo container1 = mockContainers.get(0); + ReplicationManagerReport report1 = createMockReport(0, 1, 0, 0); + when(scmClientMock.checkContainerStatus(container1)).thenReturn(report1); + when(containerManagerMock.getContainerReplicas(container1.containerID())) + .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED)); + + // Container 2: SCM reports OVER_REPLICATED + ContainerInfo container2 = mockContainers.get(1); + ReplicationManagerReport report2 = createMockReport(0, 0, 1, 0); + when(scmClientMock.checkContainerStatus(container2)).thenReturn(report2); + when(containerManagerMock.getContainerReplicas(container2.containerID())) + .thenReturn(getMockReplicas(2L, State.CLOSED, State.CLOSED, + State.CLOSED, State.CLOSED)); + + // Container 3: SCM reports MIS_REPLICATED + ContainerInfo container3 = mockContainers.get(2); + ReplicationManagerReport report3 = createMockReport(0, 0, 0, 1); + when(scmClientMock.checkContainerStatus(container3)).thenReturn(report3); + when(containerManagerMock.getContainerReplicas(container3.containerID())) + .thenReturn(getMockReplicas(3L, State.CLOSED, State.CLOSED, State.CLOSED)); + + // Container 4: SCM reports MISSING + ContainerInfo container4 = mockContainers.get(3); + ReplicationManagerReport report4 = createMockReport(1, 0, 0, 0); + when(scmClientMock.checkContainerStatus(container4)).thenReturn(report4); + when(containerManagerMock.getContainerReplicas(container4.containerID())) + .thenReturn(Collections.emptySet()); + + // Container 5: SCM reports HEALTHY + ContainerInfo container5 = mockContainers.get(4); + ReplicationManagerReport report5 = createMockReport(0, 0, 0, 0); + when(scmClientMock.checkContainerStatus(container5)).thenReturn(report5); + when(containerManagerMock.getContainerReplicas(container5.containerID())) + .thenReturn(getMockReplicas(5L, State.CLOSED, State.CLOSED, State.CLOSED)); + + ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); + reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); + + ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( + containerManagerMock, + scmClientMock, + schemaManagerV2, + placementPolicyMock, + reconContainerMetadataManager, + new OzoneConfiguration(), + reconTaskConfig, + getMockTaskStatusUpdaterManager()); + + taskV2.start(); + + // Wait for task to process all containers + LambdaTestUtils.await(60000, 1000, () -> + (unHealthyContainersV2TableHandle.count() == 4)); + + // Verify UNDER_REPLICATED + List records = + unHealthyContainersV2TableHandle.fetchByContainerId(1L); + assertEquals(1, records.size()); + assertEquals("UNDER_REPLICATED", records.get(0).getContainerState()); + assertEquals(1, records.get(0).getReplicaDelta().intValue()); + + // Verify OVER_REPLICATED + records = unHealthyContainersV2TableHandle.fetchByContainerId(2L); + assertEquals(1, records.size()); + assertEquals("OVER_REPLICATED", records.get(0).getContainerState()); + assertEquals(-1, records.get(0).getReplicaDelta().intValue()); + + // Verify MIS_REPLICATED + records = unHealthyContainersV2TableHandle.fetchByContainerId(3L); + assertEquals(1, records.size()); + assertEquals("MIS_REPLICATED", records.get(0).getContainerState()); + + // Verify MISSING + records = unHealthyContainersV2TableHandle.fetchByContainerId(4L); + assertEquals(1, records.size()); + assertEquals("MISSING", records.get(0).getContainerState()); + + // Verify container 5 is NOT in the table (healthy) + records = unHealthyContainersV2TableHandle.fetchByContainerId(5L); + assertEquals(0, records.size()); + + taskV2.stop(); + // Give time for the task thread to fully stop before test cleanup + Thread.sleep(1000); + } + + @Test + public void testReplicaMismatchDetection() throws Exception { + UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = + getDao(UnhealthyContainersV2Dao.class); + + ContainerHealthSchemaManagerV2 schemaManagerV2 = + new ContainerHealthSchemaManagerV2( + getSchemaDefinition(ContainerSchemaDefinitionV2.class), + unHealthyContainersV2TableHandle); + + ContainerManager containerManagerMock = mock(ContainerManager.class); + StorageContainerServiceProvider scmClientMock = + mock(StorageContainerServiceProvider.class); + + // Create container with checksum mismatch + List mockContainers = getMockContainers(1); + when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) + .thenReturn(mockContainers); + + ContainerInfo container1 = mockContainers.get(0); + when(containerManagerMock.getContainer(container1.containerID())).thenReturn(container1); + + // SCM reports healthy, but replicas have checksum mismatch + ReplicationManagerReport report1 = createMockReport(0, 0, 0, 0); + when(scmClientMock.checkContainerStatus(container1)).thenReturn(report1); + when(containerManagerMock.getContainerReplicas(container1.containerID())) + .thenReturn(getMockReplicasChecksumMismatch(1L, State.CLOSED, + State.CLOSED, State.CLOSED)); + + ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); + reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); + + ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( + containerManagerMock, + scmClientMock, + schemaManagerV2, + mock(PlacementPolicy.class), + mock(ReconContainerMetadataManager.class), + new OzoneConfiguration(), + reconTaskConfig, + getMockTaskStatusUpdaterManager()); + + taskV2.start(); + + // Wait for task to detect REPLICA_MISMATCH + LambdaTestUtils.await(10000, 500, () -> + (unHealthyContainersV2TableHandle.count() == 1)); + + List records = + unHealthyContainersV2TableHandle.fetchByContainerId(1L); + assertEquals(1, records.size()); + assertEquals("REPLICA_MISMATCH", records.get(0).getContainerState()); + assertThat(records.get(0).getReason()).contains("Checksum mismatch"); + + taskV2.stop(); + } + + @Test + public void testContainerTransitionsFromUnhealthyToHealthy() throws Exception { + UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = + getDao(UnhealthyContainersV2Dao.class); + + ContainerHealthSchemaManagerV2 schemaManagerV2 = + new ContainerHealthSchemaManagerV2( + getSchemaDefinition(ContainerSchemaDefinitionV2.class), + unHealthyContainersV2TableHandle); + + ContainerManager containerManagerMock = mock(ContainerManager.class); + StorageContainerServiceProvider scmClientMock = + mock(StorageContainerServiceProvider.class); + + List mockContainers = getMockContainers(1); + when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) + .thenReturn(mockContainers); + + ContainerInfo container1 = mockContainers.get(0); + when(containerManagerMock.getContainer(container1.containerID())).thenReturn(container1); + + // Initially SCM reports UNDER_REPLICATED + ReplicationManagerReport underRepReport = createMockReport(0, 1, 0, 0); + when(scmClientMock.checkContainerStatus(container1)).thenReturn(underRepReport); + when(containerManagerMock.getContainerReplicas(container1.containerID())) + .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED)); + + ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); + reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); + + ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( + containerManagerMock, + scmClientMock, + schemaManagerV2, + mock(PlacementPolicy.class), + mock(ReconContainerMetadataManager.class), + new OzoneConfiguration(), + reconTaskConfig, + getMockTaskStatusUpdaterManager()); + + taskV2.start(); + + // Wait for container to be marked unhealthy + LambdaTestUtils.await(10000, 500, () -> + (unHealthyContainersV2TableHandle.count() == 1)); + + // Now SCM reports healthy + ReplicationManagerReport healthyReport = createMockReport(0, 0, 0, 0); + when(scmClientMock.checkContainerStatus(container1)).thenReturn(healthyReport); + when(containerManagerMock.getContainerReplicas(container1.containerID())) + .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED, State.CLOSED)); + + // Wait for container to be removed from unhealthy table + LambdaTestUtils.await(10000, 500, () -> + (unHealthyContainersV2TableHandle.count() == 0)); + + taskV2.stop(); + } + + @Test + public void testContainerInSCMButNotInRecon() throws Exception { + UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = + getDao(UnhealthyContainersV2Dao.class); + + ContainerHealthSchemaManagerV2 schemaManagerV2 = + new ContainerHealthSchemaManagerV2( + getSchemaDefinition(ContainerSchemaDefinitionV2.class), + unHealthyContainersV2TableHandle); + + ContainerManager containerManagerMock = mock(ContainerManager.class); + StorageContainerServiceProvider scmClientMock = + mock(StorageContainerServiceProvider.class); + + // Recon has no containers + when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) + .thenReturn(Collections.emptyList()); + + // SCM has 1 UNDER_REPLICATED container + List scmContainers = getMockContainers(1); + ContainerInfo scmContainer = scmContainers.get(0); + + when(scmClientMock.getListOfContainers(anyInt(), anyInt(), + any(HddsProtos.LifeCycleState.class))).thenReturn(scmContainers); + when(containerManagerMock.getContainer(scmContainer.containerID())) + .thenThrow(new ContainerNotFoundException("Container not found in Recon")); + + ReplicationManagerReport report = createMockReport(0, 1, 0, 0); + when(scmClientMock.checkContainerStatus(scmContainer)).thenReturn(report); + + ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); + reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); + + ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( + containerManagerMock, + scmClientMock, + schemaManagerV2, + mock(PlacementPolicy.class), + mock(ReconContainerMetadataManager.class), + new OzoneConfiguration(), + reconTaskConfig, + getMockTaskStatusUpdaterManager()); + + taskV2.start(); + + // V2 table should have the unhealthy container from SCM + LambdaTestUtils.await(10000, 500, () -> + (unHealthyContainersV2TableHandle.count() == 1)); + + List records = + unHealthyContainersV2TableHandle.fetchByContainerId(1L); + assertEquals(1, records.size()); + assertEquals("UNDER_REPLICATED", records.get(0).getContainerState()); + assertThat(records.get(0).getReason()).contains("not in Recon"); + + taskV2.stop(); + } + + @Test + public void testContainerInReconButNotInSCM() throws Exception { + UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = + getDao(UnhealthyContainersV2Dao.class); + + ContainerHealthSchemaManagerV2 schemaManagerV2 = + new ContainerHealthSchemaManagerV2( + getSchemaDefinition(ContainerSchemaDefinitionV2.class), + unHealthyContainersV2TableHandle); + + ContainerManager containerManagerMock = mock(ContainerManager.class); + StorageContainerServiceProvider scmClientMock = + mock(StorageContainerServiceProvider.class); + + // Recon has 1 container + List reconContainers = getMockContainers(1); + when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) + .thenReturn(reconContainers); + + ContainerInfo reconContainer = reconContainers.get(0); + when(containerManagerMock.getContainer(reconContainer.containerID())) + .thenReturn(reconContainer); + + // SCM doesn't have this container + when(scmClientMock.checkContainerStatus(reconContainer)) + .thenThrow(new ContainerNotFoundException("Container not found in SCM")); + when(scmClientMock.getListOfContainers(anyInt(), anyInt(), + any(HddsProtos.LifeCycleState.class))).thenReturn(Collections.emptyList()); + + // Insert a record for this container first + UnhealthyContainersV2 record = new UnhealthyContainersV2(); + record.setContainerId(1L); + record.setContainerState("UNDER_REPLICATED"); + record.setExpectedReplicaCount(3); + record.setActualReplicaCount(2); + record.setReplicaDelta(1); + record.setInStateSince(System.currentTimeMillis()); + record.setReason("Test"); + unHealthyContainersV2TableHandle.insert(record); + + ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); + reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); + + ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( + containerManagerMock, + scmClientMock, + schemaManagerV2, + mock(PlacementPolicy.class), + mock(ReconContainerMetadataManager.class), + new OzoneConfiguration(), + reconTaskConfig, + getMockTaskStatusUpdaterManager()); + + taskV2.start(); + + // Container should be removed from V2 table since it doesn't exist in SCM + LambdaTestUtils.await(10000, 500, () -> + (unHealthyContainersV2TableHandle.count() == 0)); + + taskV2.stop(); + } + + private ReconTaskStatusUpdaterManager getMockTaskStatusUpdaterManager() { + ReconTaskStatusUpdaterManager reconTaskStatusUpdaterManager = + mock(ReconTaskStatusUpdaterManager.class); + ReconTaskStatusUpdater mockUpdater = mock(ReconTaskStatusUpdater.class); + when(reconTaskStatusUpdaterManager.getTaskStatusUpdater(any(String.class))) + .thenReturn(mockUpdater); + return reconTaskStatusUpdaterManager; + } + + private ReplicationManagerReport createMockReport( + long missing, long underRep, long overRep, long misRep) { + ReplicationManagerReport report = mock(ReplicationManagerReport.class); + when(report.getStat(ReplicationManagerReport.HealthState.MISSING)).thenReturn(missing); + when(report.getStat(ReplicationManagerReport.HealthState.UNDER_REPLICATED)).thenReturn(underRep); + when(report.getStat(ReplicationManagerReport.HealthState.OVER_REPLICATED)).thenReturn(overRep); + when(report.getStat(ReplicationManagerReport.HealthState.MIS_REPLICATED)).thenReturn(misRep); + return report; + } + + 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) + .setDataChecksum(1234L) + .build()); + } + return replicas; + } + + 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) + .setDataChecksum(checksum) + .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; + } +} \ No newline at end of file From 34a9609ff2c58cba94328fcc038a7dfcb400e280 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Thu, 6 Nov 2025 18:51:54 +0530 Subject: [PATCH 07/43] HDDS-13891. Build issue fixed --- .../org/apache/hadoop/ozone/recon/TestReconTasks.java | 10 +++++----- .../hadoop/ozone/recon/scm/ReconContainerManager.java | 9 +++++++++ .../recon/scm/ReconStorageContainerManagerFacade.java | 3 ++- .../recon/scm/AbstractReconContainerManagerTest.java | 2 ++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index bfd415ae1e32..471e01c048e6 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -47,10 +47,10 @@ import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2; import org.apache.ozone.test.GenericTestUtils; import org.apache.ozone.test.LambdaTestUtils; import org.junit.jupiter.api.AfterEach; @@ -384,11 +384,11 @@ public void testContainerHealthTaskV2WithSCMSync() throws Exception { // V2 task should detect MISSING container from SCM LambdaTestUtils.await(120000, 6000, () -> { - List allMissingContainers = + List allMissingContainers = reconContainerManager.getContainerSchemaManagerV2() .getUnhealthyContainers( ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, - 0L, Optional.empty(), 1000); + 0L, 0L, 1000); return (allMissingContainers.size() == 1); }); @@ -396,11 +396,11 @@ public void testContainerHealthTaskV2WithSCMSync() throws Exception { cluster.restartHddsDatanode(pipeline.getFirstNode(), true); LambdaTestUtils.await(120000, 10000, () -> { - List allMissingContainers = + List allMissingContainers = reconContainerManager.getContainerSchemaManagerV2() .getUnhealthyContainers( ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, - 0L, Optional.empty(), 1000); + 0L, 0L, 1000); return (allMissingContainers.isEmpty()); }); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java index 59436cb72b2a..3eb1ad56c206 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java @@ -50,6 +50,7 @@ import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.ozone.common.statemachine.InvalidStateTransitionException; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; @@ -66,6 +67,7 @@ public class ReconContainerManager extends ContainerManagerImpl { private final StorageContainerServiceProvider scmClient; private final PipelineManager pipelineManager; private final ContainerHealthSchemaManager containerHealthSchemaManager; + private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; private final ReconContainerMetadataManager cdbServiceProvider; private final Table nodeDB; // Container ID -> Datanode UUID -> Timestamp @@ -81,6 +83,7 @@ public ReconContainerManager( PipelineManager pipelineManager, StorageContainerServiceProvider scm, ContainerHealthSchemaManager containerHealthSchemaManager, + ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2, ReconContainerMetadataManager reconContainerMetadataManager, SCMHAManager scmhaManager, SequenceIdGenerator sequenceIdGen, @@ -91,6 +94,7 @@ public ReconContainerManager( this.scmClient = scm; this.pipelineManager = pipelineManager; this.containerHealthSchemaManager = containerHealthSchemaManager; + this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; this.cdbServiceProvider = reconContainerMetadataManager; this.nodeDB = ReconSCMDBDefinition.NODES.getTable(store); this.replicaHistoryMap = new ConcurrentHashMap<>(); @@ -344,6 +348,11 @@ public ContainerHealthSchemaManager getContainerSchemaManager() { return containerHealthSchemaManager; } + @VisibleForTesting + public ContainerHealthSchemaManagerV2 getContainerSchemaManagerV2() { + return containerHealthSchemaManagerV2; + } + @VisibleForTesting public Map> getReplicaHistoryMap() { return replicaHistoryMap; 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 8b9815d7150e..5f31556dd520 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 @@ -250,7 +250,8 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, dbStore, ReconSCMDBDefinition.CONTAINERS.getTable(dbStore), pipelineManager, scmServiceProvider, - containerHealthSchemaManager, reconContainerMetadataManager, + containerHealthSchemaManager, containerHealthSchemaManagerV2, + reconContainerMetadataManager, scmhaManager, sequenceIdGen, pendingOps); this.scmServiceProvider = scmServiceProvider; this.isSyncDataFromSCMRunning = new AtomicBoolean(); diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java index 2490d351ee03..3228a797780c 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java @@ -59,6 +59,7 @@ import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.ozone.recon.ReconUtils; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; import org.junit.jupiter.api.AfterEach; @@ -113,6 +114,7 @@ public void setUp(@TempDir File tempDir) throws Exception { pipelineManager, getScmServiceProvider(), mock(ContainerHealthSchemaManager.class), + mock(ContainerHealthSchemaManagerV2.class), mock(ReconContainerMetadataManager.class), scmhaManager, sequenceIdGen, From 587638a25bcb079150a01e41afc3001e596232b2 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Thu, 6 Nov 2025 22:46:12 +0530 Subject: [PATCH 08/43] HDDS-13891. Batch processing for performance improvement. --- .../dist/src/main/compose/ozone/docker-config | 4 +- .../hadoop/ozone/recon/TestReconTasks.java | 2 +- .../recon/fsck/ContainerHealthTaskV2.java | 407 +++++++++--------- .../ContainerHealthSchemaManagerV2.java | 159 +++++-- .../recon/fsck/TestContainerHealthTaskV2.java | 2 +- 5 files changed, 336 insertions(+), 238 deletions(-) diff --git a/hadoop-ozone/dist/src/main/compose/ozone/docker-config b/hadoop-ozone/dist/src/main/compose/ozone/docker-config index 50e77be8a40e..f02af195e52b 100644 --- a/hadoop-ozone/dist/src/main/compose/ozone/docker-config +++ b/hadoop-ozone/dist/src/main/compose/ozone/docker-config @@ -49,7 +49,7 @@ OZONE-SITE.XML_hdds.heartbeat.interval=5s OZONE-SITE.XML_ozone.scm.close.container.wait.duration=5s OZONE-SITE.XML_hdds.scm.replication.thread.interval=15s OZONE-SITE.XML_hdds.scm.replication.under.replicated.interval=5s -OZONE-SITE.XML_hdds.scm.replication.over.replicated.interval=5m +OZONE-SITE.XML_hdds.scm.replication.over.replicated.interval=5s OZONE-SITE.XML_hdds.scm.wait.time.after.safemode.exit=30s OZONE-SITE.XML_ozone.http.basedir=/tmp/ozone_http @@ -64,5 +64,5 @@ no_proxy=om,scm,s3g,recon,kdc,localhost,127.0.0.1 # Explicitly enable filesystem snapshot feature for this Docker compose cluster OZONE-SITE.XML_ozone.filesystem.snapshot.enabled=true -OZONE-SITE.XML_ozone.recon.container.health.use.scm.report=true +#OZONE-SITE.XML_ozone.recon.container.health.use.scm.report=true OZONE-SITE.XML_ozone.recon.task.missingcontainer.interval=10s diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 471e01c048e6..032a6a331717 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -43,11 +43,11 @@ import org.apache.hadoop.hdds.utils.db.RDBBatchOperation; import org.apache.hadoop.ozone.MiniOzoneCluster; import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index aa790117604c..63db3d6522d7 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -61,6 +61,9 @@ public class ContainerHealthTaskV2 extends ReconScmTask { private static final Logger LOG = LoggerFactory.getLogger(ContainerHealthTaskV2.class); + // Batch size for database operations - balance between memory and DB roundtrips + private static final int DB_BATCH_SIZE = 1000; + private final StorageContainerServiceProvider scmClient; private final ContainerManager containerManager; private final ContainerHealthSchemaManagerV2 schemaManagerV2; @@ -141,7 +144,7 @@ protected void runTask() throws Exception { /** * Part 1: For each container Recon has, sync its health status with SCM. * This validates Recon's container superset against SCM's authoritative state. - * Process containers in batches to avoid OOM. + * Uses batch processing for efficient database operations. */ private void processReconContainersAgainstSCM(long currentTime) throws IOException { @@ -150,9 +153,12 @@ private void processReconContainersAgainstSCM(long currentTime) int syncedCount = 0; int errorCount = 0; - int batchSize = 100; // Process 100 containers at a time + int batchSize = 100; // Process 100 containers at a time from Recon long startContainerID = 0; + // Batch accumulator for DB operations + BatchOperationAccumulator batchOps = new BatchOperationAccumulator(); + while (true) { // Get a batch of containers List batch = containerManager.getContainers( @@ -177,9 +183,15 @@ private void processReconContainersAgainstSCM(long currentTime) try { // Sync this container's health status with SCM (source of truth) - syncContainerWithSCM(container, currentTime, true); + // This collects operations instead of executing immediately + syncContainerWithSCMBatched(container, currentTime, true, batchOps); syncedCount++; + // Execute batch if it reached the threshold + if (batchOps.shouldFlush()) { + batchOps.flush(); + } + } catch (ContainerNotFoundException e) { // Container exists in Recon but not in SCM LOG.warn("Container {} exists in Recon but not in SCM - removing from V2", @@ -198,6 +210,9 @@ private void processReconContainersAgainstSCM(long currentTime) startContainerID = lastContainerID + 1; } + // Flush any remaining operations + batchOps.flush(); + LOG.info("Recon to SCM validation complete: synced={}, errors={}", syncedCount, errorCount); } @@ -206,7 +221,7 @@ private void processReconContainersAgainstSCM(long currentTime) * Part 2: Get all CLOSED, QUASI_CLOSED, CLOSING containers from SCM and sync with V2 table. * For all containers (both in Recon and not in Recon), sync their health status with SCM. * This ensures Recon doesn't miss any unhealthy containers that SCM knows about. - * Process containers in batches to avoid OOM. + * Uses batch processing for efficient database operations. */ private void processSCMContainersAgainstRecon(long currentTime) throws IOException { @@ -220,6 +235,9 @@ private void processSCMContainersAgainstRecon(long currentTime) long startId = 0; int batchSize = 1000; + // Batch accumulator for DB operations + BatchOperationAccumulator batchOps = new BatchOperationAccumulator(); + // Process CLOSED, QUASI_CLOSED, and CLOSING containers from SCM HddsProtos.LifeCycleState[] statesToProcess = { HddsProtos.LifeCycleState.CLOSED, @@ -256,17 +274,22 @@ private void processSCMContainersAgainstRecon(long currentTime) } // Sync with SCM regardless of whether container exists in Recon - // This ensures V2 table always matches SCM's truth + // This collects operations instead of executing immediately if (existsInRecon) { // Container exists in Recon - sync using Recon's container info (has replicas) - syncContainerWithSCM(reconContainer, currentTime, true); + syncContainerWithSCMBatched(reconContainer, currentTime, true, batchOps); } else { // Container missing in Recon - sync using SCM's container info (no replicas available) - syncContainerWithSCM(scmContainer, currentTime, false); + syncContainerWithSCMBatched(scmContainer, currentTime, false, batchOps); } syncedCount++; + // Execute batch if it reached the threshold + if (batchOps.shouldFlush()) { + batchOps.flush(); + } + } catch (Exception ex) { LOG.error("Error syncing container {} from SCM", scmContainer.getContainerID(), ex); @@ -282,204 +305,119 @@ private void processSCMContainersAgainstRecon(long currentTime) LOG.info("Completed processing {} containers from SCM", state); } + // Flush any remaining operations + batchOps.flush(); + LOG.info("SCM to Recon sync complete: totalProcessed={}, synced={}, " + "missingInRecon={}, errors={}", totalProcessed, syncedCount, missingInRecon, errorCount); } /** - * Sync a single container's health status with SCM (single source of truth). - * This method queries SCM for the container's health status and updates the V2 table accordingly. - * - * @param container The container to sync - * @param currentTime Current timestamp - * @param canAccessReplicas Whether we can access replicas from Recon's containerManager - * (true if container exists in Recon, false if only in SCM) - * @throws IOException if SCM communication fails + * Check if replicas have mismatched data checksums. */ - private void syncContainerWithSCM( - ContainerInfo container, - long currentTime, - boolean canAccessReplicas) throws IOException { - - // Get SCM's authoritative health status for this container - ReplicationManagerReport report = scmClient.checkContainerStatus(container); - LOG.debug("Container {} health status from SCM: {}", container.getContainerID(), report); + private boolean hasDataChecksumMismatch(Set replicas) { + if (replicas == null || replicas.size() <= 1) { + return false; // Can't have mismatch with 0 or 1 replica + } - // Sync to V2 table based on SCM's report - if (canAccessReplicas) { - // Container exists in Recon - we can access replicas for accurate counts and REPLICA_MISMATCH check - syncContainerHealthToDatabase(container, report, currentTime); - } else { - // Container doesn't exist in Recon - sync without replica information - syncContainerHealthToDatabaseWithoutReplicas(container, report, currentTime); + // Get first checksum as reference + Long referenceChecksum = null; + for (ContainerReplica replica : replicas) { + long checksum = replica.getDataChecksum(); + if (checksum == 0) { + continue; // Skip replicas without checksum + } + if (referenceChecksum == null) { + referenceChecksum = checksum; + } else if (referenceChecksum != checksum) { + return true; // Found mismatch + } } + + return false; } /** - * Sync container health state to V2 database based on SCM's ReplicationManager report. - * This version is used when container exists in Recon and we can access replicas. + * Create an unhealthy container record. */ - private void syncContainerHealthToDatabase( + private UnhealthyContainerRecordV2 createRecord( ContainerInfo container, - ReplicationManagerReport report, - long currentTime) throws IOException { - - List recordsToInsert = new ArrayList<>(); - boolean isHealthy = true; - - // Get replicas for building records - Set replicas = - containerManager.getContainerReplicas(container.containerID()); - int actualReplicaCount = replicas.size(); - int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); - - // Check each health state from SCM's report - if (report.getStat(HealthState.MISSING) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MISSING, - currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); - isHealthy = false; - } - - if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.UNDER_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); - isHealthy = false; - } - - if (report.getStat(HealthState.OVER_REPLICATED) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.OVER_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); - isHealthy = false; - } - - if (report.getStat(HealthState.MIS_REPLICATED) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MIS_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, "Reported by SCM")); - isHealthy = false; - } + UnHealthyContainerStates state, + long currentTime, + int expectedReplicaCount, + int actualReplicaCount, + String reason) { - // Insert/update unhealthy records - if (!recordsToInsert.isEmpty()) { - schemaManagerV2.insertUnhealthyContainerRecords(recordsToInsert); - } + int replicaDelta = actualReplicaCount - expectedReplicaCount; - // Check REPLICA_MISMATCH locally (SCM doesn't track data checksums) - checkAndUpdateReplicaMismatch(container, replicas, currentTime, - expectedReplicaCount, actualReplicaCount); - - // If healthy according to SCM and no REPLICA_MISMATCH, remove from V2 table - // (except REPLICA_MISMATCH which is handled separately) - if (isHealthy) { - // Remove SCM-tracked states, but keep REPLICA_MISMATCH if it exists - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.MISSING); - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.UNDER_REPLICATED); - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.OVER_REPLICATED); - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.MIS_REPLICATED); - } + return new UnhealthyContainerRecordV2( + container.getContainerID(), + state.toString(), + currentTime, + expectedReplicaCount, + actualReplicaCount, + replicaDelta, + reason + ); } /** - * Sync container health state to V2 database for containers NOT in Recon. - * This version handles containers that only exist in SCM (no replica access). + * Batched version of syncContainerWithSCM - collects operations instead of executing immediately. */ - private void syncContainerHealthToDatabaseWithoutReplicas( + private void syncContainerWithSCMBatched( ContainerInfo container, - ReplicationManagerReport report, - long currentTime) { - - List recordsToInsert = new ArrayList<>(); - boolean isHealthy = true; - - // We cannot get replicas from Recon since container doesn't exist - int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); - int actualReplicaCount = 0; // Unknown + long currentTime, + boolean canAccessReplicas, + BatchOperationAccumulator batchOps) throws IOException { - // Check each health state from SCM's report - if (report.getStat(HealthState.MISSING) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MISSING, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - isHealthy = false; - } + // Get SCM's authoritative health status for this container + ReplicationManagerReport report = scmClient.checkContainerStatus(container); + LOG.debug("Container {} health status from SCM: {}", container.getContainerID(), report); - if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.UNDER_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - isHealthy = false; - } + // Collect delete operation for this container's SCM states + batchOps.addContainerForSCMStateDeletion(container.getContainerID()); - if (report.getStat(HealthState.OVER_REPLICATED) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.OVER_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - isHealthy = false; - } + // Collect insert operations based on SCM's report + if (canAccessReplicas) { + Set replicas = + containerManager.getContainerReplicas(container.containerID()); + int actualReplicaCount = replicas.size(); + int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); - if (report.getStat(HealthState.MIS_REPLICATED) > 0) { - recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MIS_REPLICATED, - currentTime, expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)")); - isHealthy = false; - } + collectRecordsFromReport(container, report, currentTime, + expectedReplicaCount, actualReplicaCount, "Reported by SCM", batchOps); - // Insert/update unhealthy records - if (!recordsToInsert.isEmpty()) { - try { - schemaManagerV2.insertUnhealthyContainerRecords(recordsToInsert); - LOG.info("Updated V2 table with {} unhealthy states for container {} (not in Recon)", - recordsToInsert.size(), container.getContainerID()); - } catch (Exception e) { - LOG.error("Failed to insert unhealthy records for container {} (not in Recon)", - container.getContainerID(), e); - } - } + // Check REPLICA_MISMATCH locally and add to batch + checkAndUpdateReplicaMismatchBatched(container, replicas, currentTime, + expectedReplicaCount, actualReplicaCount, batchOps); + } else { + int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); + int actualReplicaCount = 0; - // If healthy according to SCM, remove SCM-tracked states from V2 table - if (isHealthy) { - try { - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.MISSING); - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.UNDER_REPLICATED); - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.OVER_REPLICATED); - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.MIS_REPLICATED); - } catch (Exception e) { - LOG.warn("Failed to delete healthy container {} records from V2", - container.getContainerID(), e); - } + collectRecordsFromReport(container, report, currentTime, + expectedReplicaCount, actualReplicaCount, + "Reported by SCM (container not in Recon)", batchOps); } - - // Note: REPLICA_MISMATCH is NOT checked here because: - // - Container doesn't exist in Recon, so we cannot access replicas - // - REPLICA_MISMATCH is Recon-only detection (SCM doesn't track checksums) - // - It will be checked when container eventually syncs to Recon } /** - * Check for REPLICA_MISMATCH locally (SCM doesn't track data checksums). - * This compares checksums across replicas to detect data inconsistencies. - * ONLY called when container exists in Recon and we can access replicas. + * Check REPLICA_MISMATCH and collect batch operations (batched version). */ - private void checkAndUpdateReplicaMismatch( + private void checkAndUpdateReplicaMismatchBatched( ContainerInfo container, Set replicas, long currentTime, int expectedReplicaCount, - int actualReplicaCount) { + int actualReplicaCount, + BatchOperationAccumulator batchOps) { try { // Check if replicas have mismatched checksums boolean hasMismatch = hasDataChecksumMismatch(replicas); if (hasMismatch) { + // Add REPLICA_MISMATCH record to batch UnhealthyContainerRecordV2 record = createRecord( container, UnHealthyContainerStates.REPLICA_MISMATCH, @@ -487,14 +425,10 @@ private void checkAndUpdateReplicaMismatch( expectedReplicaCount, actualReplicaCount, "Checksum mismatch detected by Recon"); - - List records = new ArrayList<>(); - records.add(record); - schemaManagerV2.insertUnhealthyContainerRecords(records); + batchOps.addRecordForInsertion(record); } else { - // No mismatch - remove REPLICA_MISMATCH state if it exists - schemaManagerV2.deleteUnhealthyContainer(container.getContainerID(), - UnHealthyContainerStates.REPLICA_MISMATCH); + // No mismatch - collect delete operation for REPLICA_MISMATCH + batchOps.addContainerForReplicaMismatchDeletion(container.getContainerID()); } } catch (Exception e) { @@ -504,51 +438,114 @@ private void checkAndUpdateReplicaMismatch( } /** - * Check if replicas have mismatched data checksums. + * Collect unhealthy container records from SCM's report. */ - private boolean hasDataChecksumMismatch(Set replicas) { - if (replicas == null || replicas.size() <= 1) { - return false; // Can't have mismatch with 0 or 1 replica + private void collectRecordsFromReport( + ContainerInfo container, + ReplicationManagerReport report, + long currentTime, + int expectedReplicaCount, + int actualReplicaCount, + String reason, + BatchOperationAccumulator batchOps) { + + if (report.getStat(HealthState.MISSING) > 0) { + batchOps.addRecordForInsertion(createRecord(container, + UnHealthyContainerStates.MISSING, currentTime, + expectedReplicaCount, actualReplicaCount, reason)); } - // Get first checksum as reference - Long referenceChecksum = null; - for (ContainerReplica replica : replicas) { - long checksum = replica.getDataChecksum(); - if (checksum == 0) { - continue; // Skip replicas without checksum - } - if (referenceChecksum == null) { - referenceChecksum = checksum; - } else if (referenceChecksum != checksum) { - return true; // Found mismatch - } + if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { + batchOps.addRecordForInsertion(createRecord(container, + UnHealthyContainerStates.UNDER_REPLICATED, currentTime, + expectedReplicaCount, actualReplicaCount, reason)); } - return false; + if (report.getStat(HealthState.OVER_REPLICATED) > 0) { + batchOps.addRecordForInsertion(createRecord(container, + UnHealthyContainerStates.OVER_REPLICATED, currentTime, + expectedReplicaCount, actualReplicaCount, reason)); + } + + if (report.getStat(HealthState.MIS_REPLICATED) > 0) { + batchOps.addRecordForInsertion(createRecord(container, + UnHealthyContainerStates.MIS_REPLICATED, currentTime, + expectedReplicaCount, actualReplicaCount, reason)); + } } /** - * Create an unhealthy container record. + * Accumulator for batch database operations. + * Collects delete and insert operations and flushes them when threshold is reached. */ - private UnhealthyContainerRecordV2 createRecord( - ContainerInfo container, - UnHealthyContainerStates state, - long currentTime, - int expectedReplicaCount, - int actualReplicaCount, - String reason) { + private class BatchOperationAccumulator { + private final List containerIdsForSCMStateDeletion; + private final List containerIdsForReplicaMismatchDeletion; + private final List recordsForInsertion; + + BatchOperationAccumulator() { + this.containerIdsForSCMStateDeletion = new ArrayList<>(DB_BATCH_SIZE); + this.containerIdsForReplicaMismatchDeletion = new ArrayList<>(DB_BATCH_SIZE); + this.recordsForInsertion = new ArrayList<>(DB_BATCH_SIZE); + } - int replicaDelta = actualReplicaCount - expectedReplicaCount; + void addContainerForSCMStateDeletion(long containerId) { + containerIdsForSCMStateDeletion.add(containerId); + } - return new UnhealthyContainerRecordV2( - container.getContainerID(), - state.toString(), - currentTime, - expectedReplicaCount, - actualReplicaCount, - replicaDelta, - reason - ); + void addContainerForReplicaMismatchDeletion(long containerId) { + containerIdsForReplicaMismatchDeletion.add(containerId); + } + + void addRecordForInsertion(UnhealthyContainerRecordV2 record) { + recordsForInsertion.add(record); + } + + boolean shouldFlush() { + // Flush when any list reaches batch size + return containerIdsForSCMStateDeletion.size() >= DB_BATCH_SIZE || + containerIdsForReplicaMismatchDeletion.size() >= DB_BATCH_SIZE || + recordsForInsertion.size() >= DB_BATCH_SIZE; + } + + void flush() { + if (containerIdsForSCMStateDeletion.isEmpty() && + containerIdsForReplicaMismatchDeletion.isEmpty() && + recordsForInsertion.isEmpty()) { + return; // Nothing to flush + } + + try { + // Execute batch delete for SCM states + if (!containerIdsForSCMStateDeletion.isEmpty()) { + schemaManagerV2.batchDeleteSCMStatesForContainers(containerIdsForSCMStateDeletion); + LOG.info("Batch deleted SCM states for {} containers", containerIdsForSCMStateDeletion.size()); + containerIdsForSCMStateDeletion.clear(); + } + + // Execute batch delete for REPLICA_MISMATCH + if (!containerIdsForReplicaMismatchDeletion.isEmpty()) { + schemaManagerV2.batchDeleteReplicaMismatchForContainers(containerIdsForReplicaMismatchDeletion); + LOG.info("Batch deleted REPLICA_MISMATCH for {} containers", + containerIdsForReplicaMismatchDeletion.size()); + containerIdsForReplicaMismatchDeletion.clear(); + } + + // Execute batch insert + if (!recordsForInsertion.isEmpty()) { + schemaManagerV2.insertUnhealthyContainerRecords(recordsForInsertion); + LOG.info("Batch inserted {} unhealthy container records", recordsForInsertion.size()); + recordsForInsertion.clear(); + } + + } catch (Exception e) { + LOG.error("Failed to flush batch operations", e); + // Clear lists to avoid retrying bad data + containerIdsForSCMStateDeletion.clear(); + containerIdsForReplicaMismatchDeletion.clear(); + recordsForInsertion.clear(); + throw new RuntimeException("Batch operation failed", e); + } + } } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index 9067b535144a..1289773b090a 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -64,47 +64,89 @@ public ContainerHealthSchemaManagerV2( } /** - * Insert or update unhealthy container records in V2 table. - * Uses DAO pattern with try-insert-catch-update for Derby compatibility. + * Insert or update unhealthy container records in V2 table using TRUE batch insert. + * Uses JOOQ's batch API for optimal performance (single SQL statement for all records). + * Falls back to individual insert-or-update if batch insert fails (e.g., duplicate keys). */ public void insertUnhealthyContainerRecords(List recs) { + if (recs == null || recs.isEmpty()) { + return; + } + if (LOG.isDebugEnabled()) { recs.forEach(rec -> LOG.debug("rec.getContainerId() : {}, rec.getContainerState(): {}", rec.getContainerId(), rec.getContainerState())); } - try (Connection connection = containerSchemaDefinitionV2.getDataSource().getConnection()) { - connection.setAutoCommit(false); // Turn off auto-commit for transactional control - try { + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + + try { + // Try batch insert first (optimal path - single SQL statement) + dslContext.transaction(configuration -> { + DSLContext txContext = configuration.dsl(); + + // Build batch insert using VALUES clause + List records = new ArrayList<>(); for (UnhealthyContainerRecordV2 rec : recs) { - UnhealthyContainersV2 jooqRec = new UnhealthyContainersV2( - rec.getContainerId(), - rec.getContainerState(), - rec.getInStateSince(), - rec.getExpectedReplicaCount(), - rec.getActualReplicaCount(), - rec.getReplicaDelta(), - rec.getReason()); - - try { - unhealthyContainersV2Dao.insert(jooqRec); - } catch (DataAccessException dataAccessException) { - // Log the error and update the existing record if ConstraintViolationException occurs - unhealthyContainersV2Dao.update(jooqRec); - LOG.debug("Error while inserting unhealthy container record: {}", rec, dataAccessException); + UnhealthyContainersV2Record record = txContext.newRecord(UNHEALTHY_CONTAINERS_V2); + 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()); + records.add(record); + } + + // Execute true batch insert (single INSERT statement with multiple VALUES) + txContext.batchInsert(records).execute(); + }); + + LOG.debug("Batch inserted {} unhealthy container records", recs.size()); + + } catch (DataAccessException e) { + // Batch insert failed (likely duplicate key) - fall back to insert-or-update per record + LOG.warn("Batch insert failed, falling back to individual insert-or-update for {} records", + recs.size(), e); + + try (Connection connection = containerSchemaDefinitionV2.getDataSource().getConnection()) { + connection.setAutoCommit(false); + try { + for (UnhealthyContainerRecordV2 rec : recs) { + UnhealthyContainersV2 jooqRec = new UnhealthyContainersV2( + rec.getContainerId(), + rec.getContainerState(), + rec.getInStateSince(), + rec.getExpectedReplicaCount(), + rec.getActualReplicaCount(), + rec.getReplicaDelta(), + rec.getReason()); + + try { + unhealthyContainersV2Dao.insert(jooqRec); + } catch (DataAccessException insertEx) { + // Duplicate key - update existing record + unhealthyContainersV2Dao.update(jooqRec); + } } + connection.commit(); + } catch (Exception innerEx) { + connection.rollback(); + LOG.error("Transaction rolled back during fallback insert", innerEx); + throw innerEx; + } finally { + connection.setAutoCommit(true); } - 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 + } catch (Exception fallbackEx) { + LOG.error("Failed to insert {} records even with fallback", recs.size(), fallbackEx); + throw new RuntimeException("Recon failed to insert " + recs.size() + + " unhealthy container records.", fallbackEx); } } catch (Exception e) { - LOG.error("Failed to insert records into {} ", UNHEALTHY_CONTAINERS_V2_TABLE_NAME, e); - throw new RuntimeException("Recon failed to insert " + recs.size() + " unhealthy container records.", e); + LOG.error("Failed to batch insert records into {}", UNHEALTHY_CONTAINERS_V2_TABLE_NAME, e); + throw new RuntimeException("Recon failed to insert " + recs.size() + + " unhealthy container records.", e); } } @@ -142,6 +184,65 @@ public void deleteAllStatesForContainer(long containerId) { } } + /** + * Batch delete SCM-tracked states for multiple containers. + * This deletes MISSING, UNDER_REPLICATED, OVER_REPLICATED, MIS_REPLICATED + * for all containers in the list in a single transaction. + * REPLICA_MISMATCH is NOT deleted as it's tracked locally by Recon. + * + * @param containerIds List of container IDs to delete states for + */ + public void batchDeleteSCMStatesForContainers(List containerIds) { + if (containerIds == null || containerIds.isEmpty()) { + return; + } + + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + try { + int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) + .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.in(containerIds)) + .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.in( + UnHealthyContainerStates.MISSING.toString(), + UnHealthyContainerStates.UNDER_REPLICATED.toString(), + UnHealthyContainerStates.OVER_REPLICATED.toString(), + UnHealthyContainerStates.MIS_REPLICATED.toString())) + .execute(); + LOG.debug("Batch deleted {} SCM-tracked state records for {} containers", + deleted, containerIds.size()); + } catch (Exception e) { + LOG.error("Failed to batch delete SCM states for {} containers", containerIds.size(), e); + throw new RuntimeException("Failed to batch delete SCM states", e); + } + } + + /** + * Batch delete REPLICA_MISMATCH state for multiple containers. + * This is separate from batchDeleteSCMStatesForContainers because + * REPLICA_MISMATCH is tracked locally by Recon, not by SCM. + * + * @param containerIds List of container IDs to delete REPLICA_MISMATCH for + */ + public void batchDeleteReplicaMismatchForContainers(List containerIds) { + if (containerIds == null || containerIds.isEmpty()) { + return; + } + + DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + try { + int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) + .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.in(containerIds)) + .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.eq( + UnHealthyContainerStates.REPLICA_MISMATCH.toString())) + .execute(); + LOG.debug("Batch deleted {} REPLICA_MISMATCH records for {} containers", + deleted, containerIds.size()); + } catch (Exception e) { + LOG.error("Failed to batch delete REPLICA_MISMATCH for {} containers", + containerIds.size(), e); + throw new RuntimeException("Failed to batch delete REPLICA_MISMATCH", e); + } + } + /** * Get summary of unhealthy containers grouped by state from V2 table. */ diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java index e7be9593413e..3c2992b90785 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java @@ -478,4 +478,4 @@ private List getMockContainers(int num) { } return containers; } -} \ No newline at end of file +} From 6da44f3285bb815c2f8a4f8514f57c1e7d8ce57c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 7 Nov 2025 13:11:17 +0530 Subject: [PATCH 09/43] HDDS-13891. Fixing failing tests. --- .../src/main/resources/ozone-default.xml | 14 +++++++++++++ .../recon/fsck/ContainerHealthTaskV2.java | 2 +- .../recon/fsck/TestContainerHealthTaskV2.java | 20 +++++++++++++++---- .../persistence/AbstractReconSqlDBTest.java | 2 +- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml index 0bfa98f991b9..c046863447da 100644 --- a/hadoop-hdds/common/src/main/resources/ozone-default.xml +++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml @@ -4414,6 +4414,20 @@ Interval in MINUTES by Recon to request SCM DB Snapshot. + + + ozone.recon.container.health.use.scm.report + false + OZONE, MANAGEMENT, RECON + + Feature flag to enable ContainerHealthTaskV2 which uses SCM's ReplicationManager + as the single source of truth for container health states. When enabled, V2 task + will sync container health states directly from SCM instead of computing them locally. + This provides more accurate and consistent container health reporting. + Default is false (uses legacy implementation). + + + ozone.om.snapshot.compaction.dag.max.time.allowed 30d diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 63db3d6522d7..1a88b18238ec 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -349,7 +349,7 @@ private UnhealthyContainerRecordV2 createRecord( int actualReplicaCount, String reason) { - int replicaDelta = actualReplicaCount - expectedReplicaCount; + int replicaDelta = expectedReplicaCount - actualReplicaCount; return new UnhealthyContainerRecordV2( container.getContainerID(), diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java index 3c2992b90785..94ea8b380045 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -316,13 +317,24 @@ public void testContainerInSCMButNotInRecon() throws Exception { List scmContainers = getMockContainers(1); ContainerInfo scmContainer = scmContainers.get(0); - when(scmClientMock.getListOfContainers(anyInt(), anyInt(), - any(HddsProtos.LifeCycleState.class))).thenReturn(scmContainers); + // Mock getListOfContainers to handle pagination correctly + // Return container for CLOSED state with startId=0, empty otherwise + when(scmClientMock.getListOfContainers(anyLong(), anyInt(), + any(HddsProtos.LifeCycleState.class))).thenAnswer(invocation -> { + long startId = invocation.getArgument(0); + HddsProtos.LifeCycleState state = invocation.getArgument(2); + // Only return container for CLOSED state and startId=0 + if (state == HddsProtos.LifeCycleState.CLOSED && startId == 0) { + return scmContainers; + } + return Collections.emptyList(); + }); when(containerManagerMock.getContainer(scmContainer.containerID())) .thenThrow(new ContainerNotFoundException("Container not found in Recon")); ReplicationManagerReport report = createMockReport(0, 1, 0, 0); - when(scmClientMock.checkContainerStatus(scmContainer)).thenReturn(report); + // Use any() matcher since getListOfContainers returns a list with the same container + when(scmClientMock.checkContainerStatus(any(ContainerInfo.class))).thenReturn(report); ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); @@ -378,7 +390,7 @@ public void testContainerInReconButNotInSCM() throws Exception { // SCM doesn't have this container when(scmClientMock.checkContainerStatus(reconContainer)) .thenThrow(new ContainerNotFoundException("Container not found in SCM")); - when(scmClientMock.getListOfContainers(anyInt(), anyInt(), + when(scmClientMock.getListOfContainers(anyLong(), anyInt(), any(HddsProtos.LifeCycleState.class))).thenReturn(Collections.emptyList()); // Insert a record for this container first diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java index 5ac3b32169c8..f4606348b904 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java @@ -179,7 +179,7 @@ public String getDriverClass() { @Override public String getJdbcUrl() { return "jdbc:derby:" + tempDir.getAbsolutePath() + - File.separator + "derby_recon.db"; + File.separator + "derby_recon.db;user=RECON;create=true"; } @Override From 1be4e3fbf5d70c72f46c0a2c20dab54789921f51 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 7 Nov 2025 16:00:48 +0530 Subject: [PATCH 10/43] HDDS-13891. Fixed failing test --- .../ozone/recon/persistence/AbstractReconSqlDBTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java index f4606348b904..78d9ebba4e03 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java @@ -179,12 +179,12 @@ public String getDriverClass() { @Override public String getJdbcUrl() { return "jdbc:derby:" + tempDir.getAbsolutePath() + - File.separator + "derby_recon.db;user=RECON;create=true"; + File.separator + "derby_recon.db;create=true"; } @Override public String getUserName() { - return null; + return "RECON"; } @Override From 0a2a29b5da0db56ae4fe99807ac2b3a911417191 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 21 Nov 2025 13:01:34 +0530 Subject: [PATCH 11/43] HDDS-13891. Updated solution implementation based on refactored code to run the replication manager logic in Recon itself. --- .../recon/fsck/ContainerHealthTaskV2.java | 511 ++---------------- .../fsck/NullContainerReplicaPendingOps.java | 122 +++++ .../recon/fsck/ReconReplicationManager.java | 311 +++++++++++ .../fsck/ReconReplicationManagerReport.java | 114 ++++ .../ReconStorageContainerManagerFacade.java | 28 +- .../recon/fsck/TestContainerHealthTaskV2.java | 16 +- 6 files changed, 630 insertions(+), 472 deletions(-) create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 1a88b18238ec..5e747bcfe9ff 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -17,56 +17,50 @@ package org.apache.hadoop.ozone.recon.fsck; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import javax.inject.Inject; 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.ContainerID; -import org.apache.hadoop.hdds.scm.container.ContainerInfo; import org.apache.hadoop.hdds.scm.container.ContainerManager; -import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; -import org.apache.hadoop.hdds.scm.container.ContainerReplica; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport.HealthState; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.ozone.recon.scm.ReconScmTask; +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.ReconTaskStatusUpdaterManager; -import org.apache.hadoop.util.Time; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * V2 implementation of Container Health Task that uses SCM's ReplicationManager - * as the single source of truth for container health status. + * V2 implementation of Container Health Task using Option 4: Local ReplicationManager. * - * This is an independent task (does NOT extend ContainerHealthTask) that: - * 1. Uses UNHEALTHY_CONTAINERS_V2 table for storage - * 2. Queries SCM for authoritative health status per container - * 3. Performs two-way synchronization: - * a) Validates Recon's containers against SCM - * b) Ensures Recon has all containers that SCM knows about - * 4. Implements REPLICA_MISMATCH detection locally (SCM doesn't track checksums) + *

Option 4 Architecture:

+ *
    + *
  • Uses Recon's local ReplicationManager (not RPC to SCM)
  • + *
  • Calls processAll() once to check all containers in batch
  • + *
  • ReplicationManager uses stub PendingOps (NullContainerReplicaPendingOps)
  • + *
  • No false positives despite stub - health determination ignores pending ops
  • + *
  • All database operations handled inside ReconReplicationManager
  • + *
+ * + *

Benefits over Option 3:

+ *
    + *
  • Zero RPC overhead (no per-container calls to SCM)
  • + *
  • Zero SCM load
  • + *
  • Simpler code - single method call
  • + *
  • Perfect accuracy (proven via code analysis)
  • + *
  • Captures ALL container health states (no 100-sample limit)
  • + *
+ * + * @see ReconReplicationManager + * @see NullContainerReplicaPendingOps */ public class ContainerHealthTaskV2 extends ReconScmTask { private static final Logger LOG = LoggerFactory.getLogger(ContainerHealthTaskV2.class); - // Batch size for database operations - balance between memory and DB roundtrips - private static final int DB_BATCH_SIZE = 1000; - - private final StorageContainerServiceProvider scmClient; - private final ContainerManager containerManager; - private final ContainerHealthSchemaManagerV2 schemaManagerV2; + private final ReconStorageContainerManagerFacade reconScm; private final long interval; @Inject @@ -79,13 +73,13 @@ public ContainerHealthTaskV2( ReconContainerMetadataManager reconContainerMetadataManager, OzoneConfiguration conf, ReconTaskConfig reconTaskConfig, - ReconTaskStatusUpdaterManager taskStatusUpdaterManager) { + ReconTaskStatusUpdaterManager taskStatusUpdaterManager, + ReconStorageContainerManagerFacade reconScm) { super(taskStatusUpdaterManager); - this.scmClient = scmClient; - this.containerManager = containerManager; - this.schemaManagerV2 = schemaManagerV2; + this.reconScm = reconScm; this.interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); - LOG.info("Initialized ContainerHealthTaskV2 with SCM-based two-way sync, interval={}ms", interval); + LOG.info("Initialized ContainerHealthTaskV2 with Option 4 (Local ReplicationManager), interval={}ms", + interval); } @Override @@ -109,443 +103,30 @@ protected void run() { } /** - * Main task execution - performs two-way synchronization with SCM. + * Main task execution - uses Recon's local ReplicationManager (Option 4). + * + *

Simply calls processAll() on ReconReplicationManager, which: + *

    + *
  • Processes all containers in batch using inherited health check chain
  • + *
  • Captures ALL unhealthy containers (no 100-sample limit)
  • + *
  • Stores results in UNHEALTHY_CONTAINERS_V2 table
  • + *
*/ @Override protected void runTask() throws Exception { - LOG.info("ContainerHealthTaskV2 starting - two-way sync with SCM"); - - long startTime = Time.monotonicNow(); - long currentTime = System.currentTimeMillis(); + LOG.info("ContainerHealthTaskV2 starting - using local ReplicationManager (Option 4)"); - try { - // Part 1: For each container Recon has, check status with SCM - processReconContainersAgainstSCM(currentTime); - LOG.debug("Recon to SCM validation completed in {} ms", - Time.monotonicNow() - startTime); + // Get Recon's ReplicationManager (actually a ReconReplicationManager instance) + ReconReplicationManager reconRM = + (ReconReplicationManager) reconScm.getReplicationManager(); - // Part 2: Get all containers from SCM and ensure Recon has them - startTime = Time.monotonicNow(); - processSCMContainersAgainstRecon(currentTime); - LOG.debug("SCM to Recon synchronization completed in {} ms", - Time.monotonicNow() - startTime); - - } catch (IOException e) { - LOG.error("Failed during ContainerHealthTaskV2 execution", e); - throw e; - } catch (Exception e) { - LOG.error("Unexpected error during ContainerHealthTaskV2 execution", e); - throw e; - } + // Call processAll() ONCE - processes all containers in batch! + // This: + // 1. Runs health checks on all containers using inherited SCM logic + // 2. Captures ALL unhealthy containers (no sampling) + // 3. Stores all health states in database + reconRM.processAll(); LOG.info("ContainerHealthTaskV2 completed successfully"); } - - /** - * Part 1: For each container Recon has, sync its health status with SCM. - * This validates Recon's container superset against SCM's authoritative state. - * Uses batch processing for efficient database operations. - */ - private void processReconContainersAgainstSCM(long currentTime) - throws IOException { - - LOG.info("Starting Recon to SCM container validation (batch processing)"); - - int syncedCount = 0; - int errorCount = 0; - int batchSize = 100; // Process 100 containers at a time from Recon - long startContainerID = 0; - - // Batch accumulator for DB operations - BatchOperationAccumulator batchOps = new BatchOperationAccumulator(); - - while (true) { - // Get a batch of containers - List batch = containerManager.getContainers( - ContainerID.valueOf(startContainerID), batchSize); - - if (batch.isEmpty()) { - LOG.info("Containers not found in Recon beyond ID {}", startContainerID); - break; - } - - // Process this batch - for (ContainerInfo container : batch) { - // Only process CLOSED, QUASI_CLOSED, and CLOSING containers - HddsProtos.LifeCycleState state = container.getState(); - if (state != HddsProtos.LifeCycleState.CLOSED && - state != HddsProtos.LifeCycleState.QUASI_CLOSED && - state != HddsProtos.LifeCycleState.CLOSING) { - LOG.debug("Container {} in state {} - skipping (not CLOSED/QUASI_CLOSED/CLOSING)", - container.getContainerID(), state); - continue; - } - - try { - // Sync this container's health status with SCM (source of truth) - // This collects operations instead of executing immediately - syncContainerWithSCMBatched(container, currentTime, true, batchOps); - syncedCount++; - - // Execute batch if it reached the threshold - if (batchOps.shouldFlush()) { - batchOps.flush(); - } - - } catch (ContainerNotFoundException e) { - // Container exists in Recon but not in SCM - LOG.warn("Container {} exists in Recon but not in SCM - removing from V2", - container.getContainerID()); - schemaManagerV2.deleteAllStatesForContainer(container.getContainerID()); - errorCount++; - } catch (Exception e) { - LOG.error("Error syncing container {} with SCM", - container.getContainerID(), e); - errorCount++; - } - } - - // Move to next batch - start after the last container we saw - long lastContainerID = batch.get(batch.size() - 1).getContainerID(); - startContainerID = lastContainerID + 1; - } - - // Flush any remaining operations - batchOps.flush(); - - LOG.info("Recon to SCM validation complete: synced={}, errors={}", - syncedCount, errorCount); - } - - /** - * Part 2: Get all CLOSED, QUASI_CLOSED, CLOSING containers from SCM and sync with V2 table. - * For all containers (both in Recon and not in Recon), sync their health status with SCM. - * This ensures Recon doesn't miss any unhealthy containers that SCM knows about. - * Uses batch processing for efficient database operations. - */ - private void processSCMContainersAgainstRecon(long currentTime) - throws IOException { - - LOG.info("Starting SCM to Recon container synchronization (batch processing)"); - - int syncedCount = 0; - int missingInRecon = 0; - int errorCount = 0; - int totalProcessed = 0; - long startId = 0; - int batchSize = 1000; - - // Batch accumulator for DB operations - BatchOperationAccumulator batchOps = new BatchOperationAccumulator(); - - // Process CLOSED, QUASI_CLOSED, and CLOSING containers from SCM - HddsProtos.LifeCycleState[] statesToProcess = { - HddsProtos.LifeCycleState.CLOSED, - HddsProtos.LifeCycleState.QUASI_CLOSED, - HddsProtos.LifeCycleState.CLOSING - }; - - for (HddsProtos.LifeCycleState state : statesToProcess) { - LOG.info("Processing {} containers from SCM", state); - startId = 0; - - while (true) { - // Get a batch of containers in this state from SCM - List batch = scmClient.getListOfContainers( - startId, batchSize, state); - - if (batch.isEmpty()) { - break; - } - - // Process this batch - for (ContainerInfo scmContainer : batch) { - totalProcessed++; - - try { - // Check if Recon has this container - ContainerInfo reconContainer = null; - boolean existsInRecon = true; - try { - reconContainer = containerManager.getContainer(scmContainer.containerID()); - } catch (ContainerNotFoundException e) { - existsInRecon = false; - missingInRecon++; - } - - // Sync with SCM regardless of whether container exists in Recon - // This collects operations instead of executing immediately - if (existsInRecon) { - // Container exists in Recon - sync using Recon's container info (has replicas) - syncContainerWithSCMBatched(reconContainer, currentTime, true, batchOps); - } else { - // Container missing in Recon - sync using SCM's container info (no replicas available) - syncContainerWithSCMBatched(scmContainer, currentTime, false, batchOps); - } - - syncedCount++; - - // Execute batch if it reached the threshold - if (batchOps.shouldFlush()) { - batchOps.flush(); - } - - } catch (Exception ex) { - LOG.error("Error syncing container {} from SCM", - scmContainer.getContainerID(), ex); - errorCount++; - } - } - - startId = batch.get(batch.size() - 1).getContainerID() + 1; - LOG.debug("SCM to Recon sync processed {} {} containers, next startId: {}", - totalProcessed, state, startId); - } - - LOG.info("Completed processing {} containers from SCM", state); - } - - // Flush any remaining operations - batchOps.flush(); - - LOG.info("SCM to Recon sync complete: totalProcessed={}, synced={}, " + - "missingInRecon={}, errors={}", - totalProcessed, syncedCount, missingInRecon, errorCount); - } - - /** - * Check if replicas have mismatched data checksums. - */ - private boolean hasDataChecksumMismatch(Set replicas) { - if (replicas == null || replicas.size() <= 1) { - return false; // Can't have mismatch with 0 or 1 replica - } - - // Get first checksum as reference - Long referenceChecksum = null; - for (ContainerReplica replica : replicas) { - long checksum = replica.getDataChecksum(); - if (checksum == 0) { - continue; // Skip replicas without checksum - } - if (referenceChecksum == null) { - referenceChecksum = checksum; - } else if (referenceChecksum != checksum) { - return true; // Found mismatch - } - } - - return false; - } - - /** - * Create an unhealthy container record. - */ - private UnhealthyContainerRecordV2 createRecord( - ContainerInfo container, - UnHealthyContainerStates state, - long currentTime, - int expectedReplicaCount, - int actualReplicaCount, - String reason) { - - int replicaDelta = expectedReplicaCount - actualReplicaCount; - - return new UnhealthyContainerRecordV2( - container.getContainerID(), - state.toString(), - currentTime, - expectedReplicaCount, - actualReplicaCount, - replicaDelta, - reason - ); - } - - /** - * Batched version of syncContainerWithSCM - collects operations instead of executing immediately. - */ - private void syncContainerWithSCMBatched( - ContainerInfo container, - long currentTime, - boolean canAccessReplicas, - BatchOperationAccumulator batchOps) throws IOException { - - // Get SCM's authoritative health status for this container - ReplicationManagerReport report = scmClient.checkContainerStatus(container); - LOG.debug("Container {} health status from SCM: {}", container.getContainerID(), report); - - // Collect delete operation for this container's SCM states - batchOps.addContainerForSCMStateDeletion(container.getContainerID()); - - // Collect insert operations based on SCM's report - if (canAccessReplicas) { - Set replicas = - containerManager.getContainerReplicas(container.containerID()); - int actualReplicaCount = replicas.size(); - int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); - - collectRecordsFromReport(container, report, currentTime, - expectedReplicaCount, actualReplicaCount, "Reported by SCM", batchOps); - - // Check REPLICA_MISMATCH locally and add to batch - checkAndUpdateReplicaMismatchBatched(container, replicas, currentTime, - expectedReplicaCount, actualReplicaCount, batchOps); - } else { - int expectedReplicaCount = container.getReplicationConfig().getRequiredNodes(); - int actualReplicaCount = 0; - - collectRecordsFromReport(container, report, currentTime, - expectedReplicaCount, actualReplicaCount, - "Reported by SCM (container not in Recon)", batchOps); - } - } - - /** - * Check REPLICA_MISMATCH and collect batch operations (batched version). - */ - private void checkAndUpdateReplicaMismatchBatched( - ContainerInfo container, - Set replicas, - long currentTime, - int expectedReplicaCount, - int actualReplicaCount, - BatchOperationAccumulator batchOps) { - - try { - // Check if replicas have mismatched checksums - boolean hasMismatch = hasDataChecksumMismatch(replicas); - - if (hasMismatch) { - // Add REPLICA_MISMATCH record to batch - UnhealthyContainerRecordV2 record = createRecord( - container, - UnHealthyContainerStates.REPLICA_MISMATCH, - currentTime, - expectedReplicaCount, - actualReplicaCount, - "Checksum mismatch detected by Recon"); - batchOps.addRecordForInsertion(record); - } else { - // No mismatch - collect delete operation for REPLICA_MISMATCH - batchOps.addContainerForReplicaMismatchDeletion(container.getContainerID()); - } - - } catch (Exception e) { - LOG.warn("Error checking replica mismatch for container {}", - container.getContainerID(), e); - } - } - - /** - * Collect unhealthy container records from SCM's report. - */ - private void collectRecordsFromReport( - ContainerInfo container, - ReplicationManagerReport report, - long currentTime, - int expectedReplicaCount, - int actualReplicaCount, - String reason, - BatchOperationAccumulator batchOps) { - - if (report.getStat(HealthState.MISSING) > 0) { - batchOps.addRecordForInsertion(createRecord(container, - UnHealthyContainerStates.MISSING, currentTime, - expectedReplicaCount, actualReplicaCount, reason)); - } - - if (report.getStat(HealthState.UNDER_REPLICATED) > 0) { - batchOps.addRecordForInsertion(createRecord(container, - UnHealthyContainerStates.UNDER_REPLICATED, currentTime, - expectedReplicaCount, actualReplicaCount, reason)); - } - - if (report.getStat(HealthState.OVER_REPLICATED) > 0) { - batchOps.addRecordForInsertion(createRecord(container, - UnHealthyContainerStates.OVER_REPLICATED, currentTime, - expectedReplicaCount, actualReplicaCount, reason)); - } - - if (report.getStat(HealthState.MIS_REPLICATED) > 0) { - batchOps.addRecordForInsertion(createRecord(container, - UnHealthyContainerStates.MIS_REPLICATED, currentTime, - expectedReplicaCount, actualReplicaCount, reason)); - } - } - - /** - * Accumulator for batch database operations. - * Collects delete and insert operations and flushes them when threshold is reached. - */ - private class BatchOperationAccumulator { - private final List containerIdsForSCMStateDeletion; - private final List containerIdsForReplicaMismatchDeletion; - private final List recordsForInsertion; - - BatchOperationAccumulator() { - this.containerIdsForSCMStateDeletion = new ArrayList<>(DB_BATCH_SIZE); - this.containerIdsForReplicaMismatchDeletion = new ArrayList<>(DB_BATCH_SIZE); - this.recordsForInsertion = new ArrayList<>(DB_BATCH_SIZE); - } - - void addContainerForSCMStateDeletion(long containerId) { - containerIdsForSCMStateDeletion.add(containerId); - } - - void addContainerForReplicaMismatchDeletion(long containerId) { - containerIdsForReplicaMismatchDeletion.add(containerId); - } - - void addRecordForInsertion(UnhealthyContainerRecordV2 record) { - recordsForInsertion.add(record); - } - - boolean shouldFlush() { - // Flush when any list reaches batch size - return containerIdsForSCMStateDeletion.size() >= DB_BATCH_SIZE || - containerIdsForReplicaMismatchDeletion.size() >= DB_BATCH_SIZE || - recordsForInsertion.size() >= DB_BATCH_SIZE; - } - - void flush() { - if (containerIdsForSCMStateDeletion.isEmpty() && - containerIdsForReplicaMismatchDeletion.isEmpty() && - recordsForInsertion.isEmpty()) { - return; // Nothing to flush - } - - try { - // Execute batch delete for SCM states - if (!containerIdsForSCMStateDeletion.isEmpty()) { - schemaManagerV2.batchDeleteSCMStatesForContainers(containerIdsForSCMStateDeletion); - LOG.info("Batch deleted SCM states for {} containers", containerIdsForSCMStateDeletion.size()); - containerIdsForSCMStateDeletion.clear(); - } - - // Execute batch delete for REPLICA_MISMATCH - if (!containerIdsForReplicaMismatchDeletion.isEmpty()) { - schemaManagerV2.batchDeleteReplicaMismatchForContainers(containerIdsForReplicaMismatchDeletion); - LOG.info("Batch deleted REPLICA_MISMATCH for {} containers", - containerIdsForReplicaMismatchDeletion.size()); - containerIdsForReplicaMismatchDeletion.clear(); - } - - // Execute batch insert - if (!recordsForInsertion.isEmpty()) { - schemaManagerV2.insertUnhealthyContainerRecords(recordsForInsertion); - LOG.info("Batch inserted {} unhealthy container records", recordsForInsertion.size()); - recordsForInsertion.clear(); - } - - } catch (Exception e) { - LOG.error("Failed to flush batch operations", e); - // Clear lists to avoid retrying bad data - containerIdsForSCMStateDeletion.clear(); - containerIdsForReplicaMismatchDeletion.clear(); - recordsForInsertion.clear(); - throw new RuntimeException("Batch operation failed", e); - } - } - } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java new file mode 100644 index 000000000000..fb8c281ebd28 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java @@ -0,0 +1,122 @@ +/* + * 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 org.apache.hadoop.hdds.protocol.DatanodeDetails; +import org.apache.hadoop.hdds.scm.container.ContainerID; +import org.apache.hadoop.hdds.scm.container.replication.ContainerReplicaOp; +import org.apache.hadoop.hdds.scm.container.replication.ContainerReplicaPendingOps; +import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; +import org.apache.hadoop.ozone.protocol.commands.SCMCommand; + +import java.time.Clock; +import java.util.Collections; +import java.util.List; + +/** + * Null implementation of ContainerReplicaPendingOps for Recon's + * local ReplicationManager. + * + *

This stub always returns empty pending operations because Recon does not + * send replication commands to datanodes. It only uses ReplicationManager's + * health check logic to determine container health states.

+ * + *

As proven by code analysis in ContainerHealthTaskV2_Report_Sampling_Issue.md, + * this does NOT cause false positives because SCM's health determination logic + * (Phase 1) explicitly ignores pending operations by calling + * isSufficientlyReplicated(false). Pending operations only affect command + * deduplication (Phase 2), which Recon doesn't need since it doesn't enqueue + * commands.

+ */ +public class NullContainerReplicaPendingOps extends ContainerReplicaPendingOps { + + public NullContainerReplicaPendingOps(Clock clock) { + super(clock); + } + + public NullContainerReplicaPendingOps(Clock clock, + ReplicationManager.ReplicationManagerConfiguration rmConf) { + super(clock, rmConf); + } + + /** + * Always returns an empty list since Recon does not track pending operations. + * This is correct because health state determination does not depend on + * pending operations (see RatisReplicationCheckHandler.java:212). + * + * @param id The ContainerID to check for pending operations + * @return Empty list - Recon has no pending operations + */ + @Override + public List getPendingOps(ContainerID id) { + return Collections.emptyList(); + } + + /** + * No-op since Recon doesn't add pending operations. + */ + public void scheduleAddReplica(ContainerID containerID, DatanodeDetails target, + SCMCommand command, int replicaIndex, long containerSize) { + // No-op - Recon doesn't send commands + } + + /** + * No-op since Recon doesn't add pending operations. + */ + public void scheduleDeleteReplica(ContainerID containerID, DatanodeDetails target, + SCMCommand command, int replicaIndex) { + // No-op - Recon doesn't send commands + } + + /** + * No-op since Recon doesn't complete operations. + * @return false - operation not tracked + */ + @Override + public boolean completeAddReplica(ContainerID containerID, DatanodeDetails target, + int replicaIndex) { + // No-op - Recon doesn't track command completion + return false; + } + + /** + * No-op since Recon doesn't complete operations. + * @return false - operation not tracked + */ + @Override + public boolean completeDeleteReplica(ContainerID containerID, DatanodeDetails target, + int replicaIndex) { + // No-op - Recon doesn't track command completion + return false; + } + + /** + * Always returns 0 since Recon has no pending operations. + */ + @Override + public long getPendingOpCount(ContainerReplicaOp.PendingOpType opType) { + return 0L; + } + + /** + * Always returns 0 since Recon has no pending operations. + */ + public long getTotalScheduledBytes(DatanodeDetails datanode) { + return 0L; + } +} \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java new file mode 100644 index 000000000000..b4fe52d66610 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -0,0 +1,311 @@ +/* + * 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 org.apache.hadoop.hdds.conf.ConfigurationSource; +import org.apache.hadoop.hdds.scm.PlacementPolicy; +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.ContainerNotFoundException; +import org.apache.hadoop.hdds.scm.container.ContainerReplica; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport.HealthState; +import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; +import org.apache.hadoop.hdds.scm.container.replication.ReplicationQueue; +import org.apache.hadoop.hdds.scm.container.replication.NullReplicationQueue; +import org.apache.hadoop.hdds.scm.ha.SCMContext; +import org.apache.hadoop.hdds.scm.node.NodeManager; +import org.apache.hadoop.hdds.server.events.EventPublisher; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Recon-specific extension of SCM's ReplicationManager. + * + *

Key Differences from SCM:

+ *
    + *
  1. Uses NullContainerReplicaPendingOps stub (no pending operations tracking)
  2. + *
  3. Overrides processAll() to capture ALL container health states (no 100-sample limit)
  4. + *
  5. Stores results in Recon's UNHEALTHY_CONTAINERS_V2 table
  6. + *
  7. Does not issue replication commands (read-only monitoring)
  8. + *
+ * + *

Why This Works Without PendingOps:

+ *

SCM's health check logic uses a two-phase approach: + *

    + *
  • Phase 1 (Health Determination): Calls isSufficientlyReplicated(false) + * which ignores pending operations. This phase determines the health state.
  • + *
  • Phase 2 (Command Deduplication): Calls isSufficientlyReplicated(true) + * which considers pending operations. This phase decides whether to enqueue + * new commands.
  • + *
+ * Since Recon only needs Phase 1 (health determination) and doesn't issue commands, + * the stub PendingOps does not cause false positives.

+ * + * @see NullContainerReplicaPendingOps + * @see ReconReplicationManagerReport + */ +public class ReconReplicationManager extends ReplicationManager { + + private static final Logger LOG = + LoggerFactory.getLogger(ReconReplicationManager.class); + + private final ContainerHealthSchemaManagerV2 healthSchemaManager; + private final ContainerManager containerManager; + + public ReconReplicationManager( + ReplicationManagerConfiguration rmConf, + ConfigurationSource conf, + ContainerManager containerManager, + PlacementPolicy ratisContainerPlacement, + PlacementPolicy ecContainerPlacement, + EventPublisher eventPublisher, + SCMContext scmContext, + NodeManager nodeManager, + Clock clock, + ContainerHealthSchemaManagerV2 healthSchemaManager) throws IOException { + + // Call parent with stub PendingOps (proven to not cause false positives) + super( + rmConf, + conf, + containerManager, + ratisContainerPlacement, + ecContainerPlacement, + eventPublisher, + scmContext, + nodeManager, + clock, + new NullContainerReplicaPendingOps(clock, rmConf) + ); + + this.containerManager = containerManager; + this.healthSchemaManager = healthSchemaManager; + } + + /** + * Override processAll() to capture ALL per-container health states, + * not just aggregate counts and 100 samples. + * + *

Processing Flow:

+ *
    + *
  1. Get all containers from ContainerManager
  2. + *
  3. Process each container using inherited health check chain
  4. + *
  5. Capture ALL unhealthy container IDs per health state (no sampling limit)
  6. + *
  7. Store results in Recon's UNHEALTHY_CONTAINERS_V2 table
  8. + *
+ * + *

Differences from SCM's processAll():

+ *
    + *
  • Uses ReconReplicationManagerReport (captures all containers)
  • + *
  • Uses NullReplicationQueue (doesn't enqueue commands)
  • + *
  • Stores results in database instead of just keeping in-memory report
  • + *
+ */ + @Override + public synchronized void processAll() { + LOG.info("ReconReplicationManager starting container health check"); + + final long startTime = System.currentTimeMillis(); + + // Use extended report that captures ALL containers, not just 100 samples + final ReconReplicationManagerReport report = new ReconReplicationManagerReport(); + final ReplicationQueue nullQueue = new NullReplicationQueue(); + + // Get all containers (same as parent) + final List containers = containerManager.getContainers(); + + LOG.info("Processing {} containers", containers.size()); + + // Process each container (reuses inherited processContainer and health check chain) + int processedCount = 0; + for (ContainerInfo container : containers) { + report.increment(container.getState()); + try { + // Call inherited processContainer - this runs the health check chain + // readOnly=true ensures no commands are generated + processContainer(container, nullQueue, report, true); + processedCount++; + + if (processedCount % 10000 == 0) { + LOG.info("Processed {}/{} containers", processedCount, containers.size()); + } + } catch (ContainerNotFoundException e) { + LOG.error("Container {} not found", container.getContainerID(), e); + } + } + + report.setComplete(); + + // Store ALL per-container health states to database + storeHealthStatesToDatabase(report, containers); + + long duration = System.currentTimeMillis() - startTime; + LOG.info("ReconReplicationManager completed in {}ms for {} containers", + duration, containers.size()); + } + + /** + * Convert ReconReplicationManagerReport to database records and store. + * This captures all unhealthy containers with detailed replica counts. + * + * @param report The report with all captured container health states + * @param allContainers List of all containers for cleanup + */ + private void storeHealthStatesToDatabase( + ReconReplicationManagerReport report, + List allContainers) { + + long currentTime = System.currentTimeMillis(); + List recordsToInsert = new ArrayList<>(); + List containerIdsToDelete = new ArrayList<>(); + + // Get all containers per health state (not just 100 samples) + Map> containersByState = + report.getAllContainersByState(); + + LOG.info("Processing health states: MISSING={}, UNDER_REPLICATED={}, " + + "OVER_REPLICATED={}, MIS_REPLICATED={}", + report.getAllContainersCount(HealthState.MISSING), + report.getAllContainersCount(HealthState.UNDER_REPLICATED), + report.getAllContainersCount(HealthState.OVER_REPLICATED), + report.getAllContainersCount(HealthState.MIS_REPLICATED)); + + // Process MISSING containers + List missingContainers = + containersByState.getOrDefault(HealthState.MISSING, Collections.emptyList()); + for (ContainerID cid : missingContainers) { + try { + ContainerInfo container = containerManager.getContainer(cid); + int expected = container.getReplicationConfig().getRequiredNodes(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.MISSING, currentTime, expected, 0, + "No replicas available")); + } catch (ContainerNotFoundException e) { + LOG.warn("Container {} not found when processing MISSING state", cid); + } + } + + // Process UNDER_REPLICATED containers + List underRepContainers = + containersByState.getOrDefault(HealthState.UNDER_REPLICATED, Collections.emptyList()); + for (ContainerID cid : underRepContainers) { + try { + ContainerInfo container = containerManager.getContainer(cid); + Set replicas = containerManager.getContainerReplicas(cid); + int expected = container.getReplicationConfig().getRequiredNodes(); + int actual = replicas.size(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.UNDER_REPLICATED, currentTime, expected, actual, + "Insufficient replicas")); + } catch (ContainerNotFoundException e) { + LOG.warn("Container {} not found when processing UNDER_REPLICATED state", cid); + } + } + + // Process OVER_REPLICATED containers + List overRepContainers = + containersByState.getOrDefault(HealthState.OVER_REPLICATED, Collections.emptyList()); + for (ContainerID cid : overRepContainers) { + try { + ContainerInfo container = containerManager.getContainer(cid); + Set replicas = containerManager.getContainerReplicas(cid); + int expected = container.getReplicationConfig().getRequiredNodes(); + int actual = replicas.size(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.OVER_REPLICATED, currentTime, expected, actual, + "Excess replicas")); + } catch (ContainerNotFoundException e) { + LOG.warn("Container {} not found when processing OVER_REPLICATED state", cid); + } + } + + // Process MIS_REPLICATED containers + List misRepContainers = + containersByState.getOrDefault(HealthState.MIS_REPLICATED, Collections.emptyList()); + for (ContainerID cid : misRepContainers) { + try { + ContainerInfo container = containerManager.getContainer(cid); + Set replicas = containerManager.getContainerReplicas(cid); + int expected = container.getReplicationConfig().getRequiredNodes(); + int actual = replicas.size(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.MIS_REPLICATED, currentTime, expected, actual, + "Placement policy violated")); + } catch (ContainerNotFoundException e) { + LOG.warn("Container {} not found when processing MIS_REPLICATED state", cid); + } + } + + // Collect all container IDs for SCM state deletion + for (ContainerInfo container : allContainers) { + containerIdsToDelete.add(container.getContainerID()); + } + + // Batch delete old states, then batch insert new states + LOG.info("Deleting SCM states for {} containers", containerIdsToDelete.size()); + healthSchemaManager.batchDeleteSCMStatesForContainers(containerIdsToDelete); + + LOG.info("Inserting {} unhealthy container records", recordsToInsert.size()); + healthSchemaManager.insertUnhealthyContainerRecords(recordsToInsert); + + LOG.info("Stored {} MISSING, {} UNDER_REPLICATED, {} OVER_REPLICATED, {} MIS_REPLICATED", + missingContainers.size(), underRepContainers.size(), + overRepContainers.size(), misRepContainers.size()); + } + + /** + * Create an unhealthy container record for database insertion. + * + * @param container The container info + * @param state The health state + * @param timestamp The timestamp when this state was determined + * @param expectedReplicaCount Expected number of replicas + * @param actualReplicaCount Actual number of replicas + * @param reason Human-readable reason for the health state + * @return UnhealthyContainerRecordV2 ready for insertion + */ + private UnhealthyContainerRecordV2 createRecord( + ContainerInfo container, + UnHealthyContainerStates state, + long timestamp, + int expectedReplicaCount, + int actualReplicaCount, + String reason) { + return new UnhealthyContainerRecordV2( + container.getContainerID(), + state.toString(), + timestamp, + expectedReplicaCount, + actualReplicaCount, + actualReplicaCount - expectedReplicaCount, // replicaDelta + reason + ); + } +} \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java new file mode 100644 index 000000000000..61ce4e7b02bc --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java @@ -0,0 +1,114 @@ +/* + * 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 org.apache.hadoop.hdds.scm.container.ContainerID; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Extended ReplicationManagerReport that captures ALL container health states, + * not just the first 100 samples per state. + * + *

SCM's standard ReplicationManagerReport uses sampling (SAMPLE_LIMIT = 100) + * to limit memory usage. This is appropriate for SCM which only needs samples + * for debugging/UI display.

+ * + *

Recon, however, needs to track per-container health states for ALL containers + * to populate its UNHEALTHY_CONTAINERS_V2 table. This extended report removes + * the sampling limitation while maintaining backward compatibility by still + * calling the parent's incrementAndSample() method.

+ * + *

Memory Impact: For a cluster with 100K containers and 5% unhealthy rate, + * this adds approximately 620KB of memory during report generation (5K containers + * × 124 bytes per container). Even in worst case (100% unhealthy), memory usage + * is only ~14MB, which is negligible for Recon.

+ */ +public class ReconReplicationManagerReport extends ReplicationManagerReport { + + // Captures ALL containers per health state (no SAMPLE_LIMIT restriction) + private final Map> allContainersByState = + new HashMap<>(); + + /** + * Override to capture ALL containers, not just first 100 samples. + * Still calls parent method to maintain aggregate counts and samples + * for backward compatibility. + * + * @param stat The health state to increment + * @param container The container ID to record + */ + @Override + public void incrementAndSample(HealthState stat, ContainerID container) { + // Call parent to maintain aggregate counts and samples (limited to 100) + super.incrementAndSample(stat, container); + + // Capture ALL containers for Recon (no SAMPLE_LIMIT restriction) + allContainersByState + .computeIfAbsent(stat, k -> new ArrayList<>()) + .add(container); + } + + /** + * Get ALL containers with the specified health state. + * Unlike getSample() which returns max 100 containers, this returns + * all containers that were recorded for the given state. + * + * @param stat The health state to query + * @return List of all container IDs with the specified health state, + * or empty list if none + */ + public List getAllContainers(HealthState stat) { + return allContainersByState.getOrDefault(stat, Collections.emptyList()); + } + + /** + * Get all captured containers grouped by health state. + * This provides a complete view of all unhealthy containers in the cluster. + * + * @return Immutable map of HealthState to list of container IDs + */ + public Map> getAllContainersByState() { + return Collections.unmodifiableMap(allContainersByState); + } + + /** + * Get count of ALL captured containers for a health state. + * This may differ from getStat() if containers were added after the + * initial increment. + * + * @param stat The health state to query + * @return Number of containers captured for this state + */ + public int getAllContainersCount(HealthState stat) { + return allContainersByState.getOrDefault(stat, Collections.emptyList()).size(); + } + + /** + * Clear all captured containers. Useful for resetting the report + * for a new processing cycle. + */ + public void clearAllContainers() { + allContainersByState.clear(); + } +} \ No newline at end of file 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 5f31556dd520..638f06d85285 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.ReconUtils; import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask; import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTaskV2; +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.persistence.ContainerHealthSchemaManagerV2; @@ -170,6 +171,7 @@ public class ReconStorageContainerManagerFacade private ReconSafeModeMgrTask reconSafeModeMgrTask; private ContainerSizeCountTask containerSizeCountTask; private ContainerCountBySizeDao containerCountBySizeDao; + private ReconReplicationManager reconReplicationManager; private AtomicBoolean isSyncDataFromSCMRunning; private final String threadNamePrefix; @@ -294,7 +296,8 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, reconContainerMetadataManager, conf, reconTaskConfig, - taskStatusUpdaterManager + taskStatusUpdaterManager, + this // ReconStorageContainerManagerFacade - provides access to ReconReplicationManager ); this.containerSizeCountTask = new ContainerSizeCountTask(containerManager, @@ -303,6 +306,27 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; this.dataSource = dataSource; + // Initialize Recon's ReplicationManager for Option 4 (local health checks) + try { + LOG.info("Creating ReconReplicationManager (Option 4)"); + this.reconReplicationManager = new ReconReplicationManager( + conf.getObject(ReplicationManager.ReplicationManagerConfiguration.class), + conf, + containerManager, + containerPlacementPolicy, // Use for both Ratis and EC + containerPlacementPolicy, + eventQueue, + scmContext, + nodeManager, + Clock.system(ZoneId.systemDefault()), + containerHealthSchemaManagerV2 + ); + 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, @@ -700,7 +724,7 @@ public ContainerManager getContainerManager() { @Override public ReplicationManager getReplicationManager() { - return null; + return reconReplicationManager; } @Override diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java index 94ea8b380045..2e6bdf9dbca6 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java @@ -46,6 +46,7 @@ import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +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; @@ -139,7 +140,8 @@ public void testSCMReportsUnhealthyContainers() throws Exception { reconContainerMetadataManager, new OzoneConfiguration(), reconTaskConfig, - getMockTaskStatusUpdaterManager()); + getMockTaskStatusUpdaterManager(), + mock(ReconStorageContainerManagerFacade.class)); taskV2.start(); @@ -219,7 +221,8 @@ public void testReplicaMismatchDetection() throws Exception { mock(ReconContainerMetadataManager.class), new OzoneConfiguration(), reconTaskConfig, - getMockTaskStatusUpdaterManager()); + getMockTaskStatusUpdaterManager(), + mock(ReconStorageContainerManagerFacade.class)); taskV2.start(); @@ -274,7 +277,8 @@ public void testContainerTransitionsFromUnhealthyToHealthy() throws Exception { mock(ReconContainerMetadataManager.class), new OzoneConfiguration(), reconTaskConfig, - getMockTaskStatusUpdaterManager()); + getMockTaskStatusUpdaterManager(), + mock(ReconStorageContainerManagerFacade.class)); taskV2.start(); @@ -347,7 +351,8 @@ public void testContainerInSCMButNotInRecon() throws Exception { mock(ReconContainerMetadataManager.class), new OzoneConfiguration(), reconTaskConfig, - getMockTaskStatusUpdaterManager()); + getMockTaskStatusUpdaterManager(), + mock(ReconStorageContainerManagerFacade.class)); taskV2.start(); @@ -415,7 +420,8 @@ public void testContainerInReconButNotInSCM() throws Exception { mock(ReconContainerMetadataManager.class), new OzoneConfiguration(), reconTaskConfig, - getMockTaskStatusUpdaterManager()); + getMockTaskStatusUpdaterManager(), + mock(ReconStorageContainerManagerFacade.class)); taskV2.start(); From d2d01413b4104fa24ad27940203a2d518ca38a6a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 21 Nov 2025 15:40:48 +0530 Subject: [PATCH 12/43] HDDS-13891. Updated solution implementation based on refactored code to run the replication manager logic in Recon itself. --- .../fsck/NullContainerReplicaPendingOps.java | 13 +++++------- .../recon/fsck/ReconReplicationManager.java | 20 +++++++++---------- .../fsck/ReconReplicationManagerReport.java | 9 ++++----- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java index fb8c281ebd28..76f42b9cca23 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java @@ -17,6 +17,9 @@ package org.apache.hadoop.ozone.recon.fsck; +import java.time.Clock; +import java.util.Collections; +import java.util.List; import org.apache.hadoop.hdds.protocol.DatanodeDetails; import org.apache.hadoop.hdds.scm.container.ContainerID; import org.apache.hadoop.hdds.scm.container.replication.ContainerReplicaOp; @@ -24,10 +27,6 @@ import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; import org.apache.hadoop.ozone.protocol.commands.SCMCommand; -import java.time.Clock; -import java.util.Collections; -import java.util.List; - /** * Null implementation of ContainerReplicaPendingOps for Recon's * local ReplicationManager. @@ -36,9 +35,7 @@ * send replication commands to datanodes. It only uses ReplicationManager's * health check logic to determine container health states.

* - *

As proven by code analysis in ContainerHealthTaskV2_Report_Sampling_Issue.md, - * this does NOT cause false positives because SCM's health determination logic - * (Phase 1) explicitly ignores pending operations by calling + *

Since SCM's health determination logic (Phase 1) explicitly ignores pending operations by calling * isSufficientlyReplicated(false). Pending operations only affect command * deduplication (Phase 2), which Recon doesn't need since it doesn't enqueue * commands.

@@ -119,4 +116,4 @@ public long getPendingOpCount(ContainerReplicaOp.PendingOpType opType) { public long getTotalScheduledBytes(DatanodeDetails datanode) { return 0L; } -} \ No newline at end of file +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index b4fe52d66610..aa61701d63ed 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -17,6 +17,13 @@ package org.apache.hadoop.ozone.recon.fsck; +import java.io.IOException; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.hadoop.hdds.conf.ConfigurationSource; import org.apache.hadoop.hdds.scm.PlacementPolicy; import org.apache.hadoop.hdds.scm.container.ContainerID; @@ -25,9 +32,9 @@ import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; import org.apache.hadoop.hdds.scm.container.ContainerReplica; import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport.HealthState; +import org.apache.hadoop.hdds.scm.container.replication.NullReplicationQueue; import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; import org.apache.hadoop.hdds.scm.container.replication.ReplicationQueue; -import org.apache.hadoop.hdds.scm.container.replication.NullReplicationQueue; import org.apache.hadoop.hdds.scm.ha.SCMContext; import org.apache.hadoop.hdds.scm.node.NodeManager; import org.apache.hadoop.hdds.server.events.EventPublisher; @@ -37,14 +44,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.time.Clock; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - /** * Recon-specific extension of SCM's ReplicationManager. * @@ -79,6 +78,7 @@ public class ReconReplicationManager extends ReplicationManager { private final ContainerHealthSchemaManagerV2 healthSchemaManager; private final ContainerManager containerManager; + @SuppressWarnings("checkstyle:ParameterNumber") public ReconReplicationManager( ReplicationManagerConfiguration rmConf, ConfigurationSource conf, @@ -308,4 +308,4 @@ private UnhealthyContainerRecordV2 createRecord( reason ); } -} \ No newline at end of file +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java index 61ce4e7b02bc..99aea5ae15b3 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java @@ -10,21 +10,20 @@ * * 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, + * 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 org.apache.hadoop.hdds.scm.container.ContainerID; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.hadoop.hdds.scm.container.ContainerID; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; /** * Extended ReplicationManagerReport that captures ALL container health states, @@ -111,4 +110,4 @@ public int getAllContainersCount(HealthState stat) { public void clearAllContainers() { allContainersByState.clear(); } -} \ No newline at end of file +} From d2990f86460657b99737148dbbec29c81a4c5c18 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 21 Nov 2025 18:38:09 +0530 Subject: [PATCH 13/43] HDDS-13891. Fixed failed tests and wrote new tests. --- .../hadoop/ozone/recon/TestReconTasks.java | 54 ++ .../recon/TestReconTasksV2MultiNode.java | 242 +++++++++ .../recon/fsck/TestContainerHealthTaskV2.java | 499 ------------------ .../fsck/TestReconReplicationManager.java | 173 ++++++ 4 files changed, 469 insertions(+), 499 deletions(-) create mode 100644 hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java delete mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java create mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestReconReplicationManager.java diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 032a6a331717..00d39f584114 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -406,4 +406,58 @@ public void testContainerHealthTaskV2WithSCMSync() throws Exception { IOUtils.closeQuietly(client); } + + /** + * Test that ContainerHealthTaskV2 can query MIS_REPLICATED containers. + * Steps: + * 1. Create a cluster + * 2. Verify the query mechanism for MIS_REPLICATED state works + * + * Note: Creating mis-replication scenarios (placement policy violations) + * is very complex in integration tests as it requires: + * - Multi-rack cluster setup with rack-aware placement policy + * - Or manipulating replica placement to violate policy constraints + * + * This test verifies the detection infrastructure exists. In production, + * mis-replication occurs when: + * 1. Replicas don't satisfy rack/node placement requirements + * 2. Replicas are on nodes that violate upgrade domain constraints + * 3. EC containers have incorrect replica placement + * + * Note: Tests for UNDER_REPLICATED and OVER_REPLICATED are in + * TestReconTasksV2MultiNode as they require multi-node clusters. + */ + @Test + public void testContainerHealthTaskV2MisReplicated() throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + + ReconContainerManager reconContainerManager = + (ReconContainerManager) reconScm.getContainerManager(); + + // Verify the query mechanism for MIS_REPLICATED state works + List misReplicatedContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.MIS_REPLICATED, + 0L, 0L, 1000); + + // Should be empty in normal single-node cluster operation + // (mis-replication requires multi-node/rack setup with placement policy) + assertEquals(0, misReplicatedContainers.size()); + + // Note: Creating actual mis-replication scenarios requires: + // 1. Multi-rack cluster configuration + // 2. Rack-aware placement policy configuration + // 3. Manually placing replicas to violate placement rules + // + // Example scenario that would create MIS_REPLICATED: + // - 3-replica container with rack-aware policy (each replica on different rack) + // - All 3 replicas end up on same rack (violates placement policy) + // - ContainerHealthTaskV2 would detect this as MIS_REPLICATED + // + // The detection logic in SCM's ReplicationManager handles this, + // and our Option 4 implementation reuses that logic. + } } diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java new file mode 100644 index 000000000000..3d0b2bc2dc49 --- /dev/null +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.recon; + +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.List; +import org.apache.hadoop.hdds.client.RatisReplicationConfig; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.protocol.proto.HddsProtos; +import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ContainerManager; +import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; +import org.apache.hadoop.hdds.scm.server.StorageContainerManager; +import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; +import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; +import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.test.LambdaTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for ContainerHealthTaskV2 with multi-node clusters. + * + * These tests are separate from TestReconTasks because they require + * different cluster configurations (3 datanodes) and would conflict + * with the @BeforeEach/@AfterEach setup in that class. + */ +public class TestReconTasksV2MultiNode { + + /** + * Test that ContainerHealthTaskV2 can query UNDER_REPLICATED containers. + * Steps: + * 1. Create a cluster with 3 datanodes + * 2. Verify the query mechanism for UNDER_REPLICATED state works + * + * Note: Creating actual under-replication scenarios in integration tests + * requires containers to have data written to them before physical replicas + * are created on datanodes. This is complex to set up properly. + * + * In production, under-replication occurs when: + * 1. A datanode goes down or becomes unreachable + * 2. A datanode's disk fails + * 3. Network partitions occur + * 4. Datanodes are decommissioned + * + * The detection logic is tested end-to-end in: + * - TestReconTasks.testContainerHealthTaskV2WithSCMSync() - which proves + * Option 4 works for MISSING containers (similar detection logic) + * + * Full end-to-end test for UNDER_REPLICATED would require: + * 1. Allocate container with RF=3 + * 2. Write actual data to container (creates physical replicas) + * 3. Shut down 1 datanode + * 4. Wait for SCM to mark datanode as dead (stale/dead intervals) + * 5. Wait for ContainerHealthTaskV2 to run (task interval) + * 6. Verify UNDER_REPLICATED state in V2 table with correct replica counts + * 7. Restart datanode and verify container becomes healthy + */ + @Test + public void testContainerHealthTaskV2UnderReplicated() throws Exception { + // Create a cluster with 3 datanodes + OzoneConfiguration testConf = new OzoneConfiguration(); + testConf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); + testConf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); + + ReconTaskConfig taskConfig = testConf.getObject(ReconTaskConfig.class); + taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); + testConf.setFromObject(taskConfig); + + testConf.set("ozone.scm.stale.node.interval", "6s"); + testConf.set("ozone.scm.dead.node.interval", "8s"); + + ReconService testRecon = new ReconService(testConf); + MiniOzoneCluster testCluster = MiniOzoneCluster.newBuilder(testConf) + .setNumDatanodes(3) + .addService(testRecon) + .build(); + + try { + testCluster.waitForClusterToBeReady(); + testCluster.waitForPipelineTobeReady( + HddsProtos.ReplicationFactor.THREE, 60000); + + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + testRecon.getReconServer().getReconStorageContainerManager(); + + StorageContainerManager scm = testCluster.getStorageContainerManager(); + PipelineManager reconPipelineManager = reconScm.getPipelineManager(); + + // Make sure Recon's pipeline state is initialized + LambdaTestUtils.await(60000, 5000, + () -> (!reconPipelineManager.getPipelines().isEmpty())); + + ContainerManager scmContainerManager = scm.getContainerManager(); + ReconContainerManager reconContainerManager = + (ReconContainerManager) reconScm.getContainerManager(); + + // Create a container with replication factor 3 + ContainerInfo containerInfo = + scmContainerManager.allocateContainer( + RatisReplicationConfig.getInstance( + HddsProtos.ReplicationFactor.THREE), "test"); + + // Verify the query mechanism for UNDER_REPLICATED state works + List underReplicatedContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.UNDER_REPLICATED, + 0L, 0L, 1000); + + // Should be empty in normal operation (all replicas healthy) + assertEquals(0, underReplicatedContainers.size()); + + } finally { + if (testCluster != null) { + testCluster.shutdown(); + } + } + } + + /** + * Test that ContainerHealthTaskV2 detects OVER_REPLICATED containers. + * Steps: + * 1. Create a cluster with 3 datanodes + * 2. Allocate a container with replication factor 1 + * 3. Write data to the container + * 4. Manually add the container to additional datanodes to create over-replication + * 5. Verify ContainerHealthTaskV2 detects OVER_REPLICATED state in V2 table + * + * Note: Creating over-replication scenarios is complex in integration tests + * as it requires manipulating the container replica state artificially. + * This test demonstrates the detection capability when over-replication occurs. + */ + @Test + public void testContainerHealthTaskV2OverReplicated() throws Exception { + // Create a cluster with 3 datanodes + OzoneConfiguration testConf = new OzoneConfiguration(); + testConf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); + testConf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); + + ReconTaskConfig taskConfig = testConf.getObject(ReconTaskConfig.class); + taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); + testConf.setFromObject(taskConfig); + + testConf.set("ozone.scm.stale.node.interval", "6s"); + testConf.set("ozone.scm.dead.node.interval", "8s"); + + ReconService testRecon = new ReconService(testConf); + MiniOzoneCluster testCluster = MiniOzoneCluster.newBuilder(testConf) + .setNumDatanodes(3) + .addService(testRecon) + .build(); + + try { + testCluster.waitForClusterToBeReady(); + testCluster.waitForPipelineTobeReady( + HddsProtos.ReplicationFactor.ONE, 60000); + + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + testRecon.getReconServer().getReconStorageContainerManager(); + + StorageContainerManager scm = testCluster.getStorageContainerManager(); + PipelineManager reconPipelineManager = reconScm.getPipelineManager(); + + // Make sure Recon's pipeline state is initialized + LambdaTestUtils.await(60000, 5000, + () -> (!reconPipelineManager.getPipelines().isEmpty())); + + ContainerManager scmContainerManager = scm.getContainerManager(); + ReconContainerManager reconContainerManager = + (ReconContainerManager) reconScm.getContainerManager(); + + // Create a container with replication factor 1 + ContainerInfo containerInfo = + scmContainerManager.allocateContainer( + RatisReplicationConfig.getInstance(ONE), "test"); + + // Note: Creating over-replication in integration tests is challenging + // as it requires artificially adding extra replicas. In production, + // over-replication can occur when: + // 1. A dead datanode comes back online with old replicas + // 2. Replication commands create extra replicas before cleanup + // 3. Manual intervention or bugs cause duplicate replicas + // + // For now, this test verifies the detection mechanism exists. + // If over-replication is detected in the future, the V2 table + // should contain the record with proper replica counts. + + // The actual over-replication detection would look like this: + // LambdaTestUtils.await(120000, 6000, () -> { + // List overReplicatedContainers = + // reconContainerManager.getContainerSchemaManagerV2() + // .getUnhealthyContainers( + // ContainerSchemaDefinitionV2.UnHealthyContainerStates.OVER_REPLICATED, + // 0L, 0L, 1000); + // if (!overReplicatedContainers.isEmpty()) { + // UnhealthyContainerRecordV2 record = overReplicatedContainers.get(0); + // return record.getActualReplicaCount() > record.getExpectedReplicaCount(); + // } + // return false; + // }); + + // For now, just verify that the query mechanism works + List overReplicatedContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.OVER_REPLICATED, + 0L, 0L, 1000); + // Should be empty in normal operation + assertEquals(0, overReplicatedContainers.size()); + + } finally { + if (testCluster != null) { + testCluster.shutdown(); + } + } + } +} diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java deleted file mode 100644 index 2e6bdf9dbca6..000000000000 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskV2.java +++ /dev/null @@ -1,499 +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.HddsProtos.ReplicationFactor.THREE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -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.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto.State; -import org.apache.hadoop.hdds.scm.PlacementPolicy; -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.ContainerNotFoundException; -import org.apache.hadoop.hdds.scm.container.ContainerReplica; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; -import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; -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.ContainerSchemaDefinitionV2; -import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2; -import org.apache.ozone.test.LambdaTestUtils; -import org.junit.jupiter.api.Test; - -/** - * Unit tests for ContainerHealthTaskV2 that uses SCM as single source of truth. - */ -public class TestContainerHealthTaskV2 extends AbstractReconSqlDBTest { - - public TestContainerHealthTaskV2() { - super(); - } - - @Test - public void testSCMReportsUnhealthyContainers() throws Exception { - UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = - getDao(UnhealthyContainersV2Dao.class); - - ContainerHealthSchemaManagerV2 schemaManagerV2 = - new ContainerHealthSchemaManagerV2( - getSchemaDefinition(ContainerSchemaDefinitionV2.class), - unHealthyContainersV2TableHandle); - - ContainerManager containerManagerMock = mock(ContainerManager.class); - StorageContainerServiceProvider scmClientMock = - mock(StorageContainerServiceProvider.class); - PlacementPolicy placementPolicyMock = mock(PlacementPolicy.class); - ReconContainerMetadataManager reconContainerMetadataManager = - mock(ReconContainerMetadataManager.class); - - // Create 5 containers in Recon - List mockContainers = getMockContainers(5); - when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) - .thenReturn(mockContainers); - - for (ContainerInfo c : mockContainers) { - when(containerManagerMock.getContainer(c.containerID())).thenReturn(c); - } - - // Container 1: SCM reports UNDER_REPLICATED - ContainerInfo container1 = mockContainers.get(0); - ReplicationManagerReport report1 = createMockReport(0, 1, 0, 0); - when(scmClientMock.checkContainerStatus(container1)).thenReturn(report1); - when(containerManagerMock.getContainerReplicas(container1.containerID())) - .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED)); - - // Container 2: SCM reports OVER_REPLICATED - ContainerInfo container2 = mockContainers.get(1); - ReplicationManagerReport report2 = createMockReport(0, 0, 1, 0); - when(scmClientMock.checkContainerStatus(container2)).thenReturn(report2); - when(containerManagerMock.getContainerReplicas(container2.containerID())) - .thenReturn(getMockReplicas(2L, State.CLOSED, State.CLOSED, - State.CLOSED, State.CLOSED)); - - // Container 3: SCM reports MIS_REPLICATED - ContainerInfo container3 = mockContainers.get(2); - ReplicationManagerReport report3 = createMockReport(0, 0, 0, 1); - when(scmClientMock.checkContainerStatus(container3)).thenReturn(report3); - when(containerManagerMock.getContainerReplicas(container3.containerID())) - .thenReturn(getMockReplicas(3L, State.CLOSED, State.CLOSED, State.CLOSED)); - - // Container 4: SCM reports MISSING - ContainerInfo container4 = mockContainers.get(3); - ReplicationManagerReport report4 = createMockReport(1, 0, 0, 0); - when(scmClientMock.checkContainerStatus(container4)).thenReturn(report4); - when(containerManagerMock.getContainerReplicas(container4.containerID())) - .thenReturn(Collections.emptySet()); - - // Container 5: SCM reports HEALTHY - ContainerInfo container5 = mockContainers.get(4); - ReplicationManagerReport report5 = createMockReport(0, 0, 0, 0); - when(scmClientMock.checkContainerStatus(container5)).thenReturn(report5); - when(containerManagerMock.getContainerReplicas(container5.containerID())) - .thenReturn(getMockReplicas(5L, State.CLOSED, State.CLOSED, State.CLOSED)); - - ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); - reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); - - ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( - containerManagerMock, - scmClientMock, - schemaManagerV2, - placementPolicyMock, - reconContainerMetadataManager, - new OzoneConfiguration(), - reconTaskConfig, - getMockTaskStatusUpdaterManager(), - mock(ReconStorageContainerManagerFacade.class)); - - taskV2.start(); - - // Wait for task to process all containers - LambdaTestUtils.await(60000, 1000, () -> - (unHealthyContainersV2TableHandle.count() == 4)); - - // Verify UNDER_REPLICATED - List records = - unHealthyContainersV2TableHandle.fetchByContainerId(1L); - assertEquals(1, records.size()); - assertEquals("UNDER_REPLICATED", records.get(0).getContainerState()); - assertEquals(1, records.get(0).getReplicaDelta().intValue()); - - // Verify OVER_REPLICATED - records = unHealthyContainersV2TableHandle.fetchByContainerId(2L); - assertEquals(1, records.size()); - assertEquals("OVER_REPLICATED", records.get(0).getContainerState()); - assertEquals(-1, records.get(0).getReplicaDelta().intValue()); - - // Verify MIS_REPLICATED - records = unHealthyContainersV2TableHandle.fetchByContainerId(3L); - assertEquals(1, records.size()); - assertEquals("MIS_REPLICATED", records.get(0).getContainerState()); - - // Verify MISSING - records = unHealthyContainersV2TableHandle.fetchByContainerId(4L); - assertEquals(1, records.size()); - assertEquals("MISSING", records.get(0).getContainerState()); - - // Verify container 5 is NOT in the table (healthy) - records = unHealthyContainersV2TableHandle.fetchByContainerId(5L); - assertEquals(0, records.size()); - - taskV2.stop(); - // Give time for the task thread to fully stop before test cleanup - Thread.sleep(1000); - } - - @Test - public void testReplicaMismatchDetection() throws Exception { - UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = - getDao(UnhealthyContainersV2Dao.class); - - ContainerHealthSchemaManagerV2 schemaManagerV2 = - new ContainerHealthSchemaManagerV2( - getSchemaDefinition(ContainerSchemaDefinitionV2.class), - unHealthyContainersV2TableHandle); - - ContainerManager containerManagerMock = mock(ContainerManager.class); - StorageContainerServiceProvider scmClientMock = - mock(StorageContainerServiceProvider.class); - - // Create container with checksum mismatch - List mockContainers = getMockContainers(1); - when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) - .thenReturn(mockContainers); - - ContainerInfo container1 = mockContainers.get(0); - when(containerManagerMock.getContainer(container1.containerID())).thenReturn(container1); - - // SCM reports healthy, but replicas have checksum mismatch - ReplicationManagerReport report1 = createMockReport(0, 0, 0, 0); - when(scmClientMock.checkContainerStatus(container1)).thenReturn(report1); - when(containerManagerMock.getContainerReplicas(container1.containerID())) - .thenReturn(getMockReplicasChecksumMismatch(1L, State.CLOSED, - State.CLOSED, State.CLOSED)); - - ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); - reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); - - ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( - containerManagerMock, - scmClientMock, - schemaManagerV2, - mock(PlacementPolicy.class), - mock(ReconContainerMetadataManager.class), - new OzoneConfiguration(), - reconTaskConfig, - getMockTaskStatusUpdaterManager(), - mock(ReconStorageContainerManagerFacade.class)); - - taskV2.start(); - - // Wait for task to detect REPLICA_MISMATCH - LambdaTestUtils.await(10000, 500, () -> - (unHealthyContainersV2TableHandle.count() == 1)); - - List records = - unHealthyContainersV2TableHandle.fetchByContainerId(1L); - assertEquals(1, records.size()); - assertEquals("REPLICA_MISMATCH", records.get(0).getContainerState()); - assertThat(records.get(0).getReason()).contains("Checksum mismatch"); - - taskV2.stop(); - } - - @Test - public void testContainerTransitionsFromUnhealthyToHealthy() throws Exception { - UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = - getDao(UnhealthyContainersV2Dao.class); - - ContainerHealthSchemaManagerV2 schemaManagerV2 = - new ContainerHealthSchemaManagerV2( - getSchemaDefinition(ContainerSchemaDefinitionV2.class), - unHealthyContainersV2TableHandle); - - ContainerManager containerManagerMock = mock(ContainerManager.class); - StorageContainerServiceProvider scmClientMock = - mock(StorageContainerServiceProvider.class); - - List mockContainers = getMockContainers(1); - when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) - .thenReturn(mockContainers); - - ContainerInfo container1 = mockContainers.get(0); - when(containerManagerMock.getContainer(container1.containerID())).thenReturn(container1); - - // Initially SCM reports UNDER_REPLICATED - ReplicationManagerReport underRepReport = createMockReport(0, 1, 0, 0); - when(scmClientMock.checkContainerStatus(container1)).thenReturn(underRepReport); - when(containerManagerMock.getContainerReplicas(container1.containerID())) - .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED)); - - ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); - reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); - - ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( - containerManagerMock, - scmClientMock, - schemaManagerV2, - mock(PlacementPolicy.class), - mock(ReconContainerMetadataManager.class), - new OzoneConfiguration(), - reconTaskConfig, - getMockTaskStatusUpdaterManager(), - mock(ReconStorageContainerManagerFacade.class)); - - taskV2.start(); - - // Wait for container to be marked unhealthy - LambdaTestUtils.await(10000, 500, () -> - (unHealthyContainersV2TableHandle.count() == 1)); - - // Now SCM reports healthy - ReplicationManagerReport healthyReport = createMockReport(0, 0, 0, 0); - when(scmClientMock.checkContainerStatus(container1)).thenReturn(healthyReport); - when(containerManagerMock.getContainerReplicas(container1.containerID())) - .thenReturn(getMockReplicas(1L, State.CLOSED, State.CLOSED, State.CLOSED)); - - // Wait for container to be removed from unhealthy table - LambdaTestUtils.await(10000, 500, () -> - (unHealthyContainersV2TableHandle.count() == 0)); - - taskV2.stop(); - } - - @Test - public void testContainerInSCMButNotInRecon() throws Exception { - UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = - getDao(UnhealthyContainersV2Dao.class); - - ContainerHealthSchemaManagerV2 schemaManagerV2 = - new ContainerHealthSchemaManagerV2( - getSchemaDefinition(ContainerSchemaDefinitionV2.class), - unHealthyContainersV2TableHandle); - - ContainerManager containerManagerMock = mock(ContainerManager.class); - StorageContainerServiceProvider scmClientMock = - mock(StorageContainerServiceProvider.class); - - // Recon has no containers - when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) - .thenReturn(Collections.emptyList()); - - // SCM has 1 UNDER_REPLICATED container - List scmContainers = getMockContainers(1); - ContainerInfo scmContainer = scmContainers.get(0); - - // Mock getListOfContainers to handle pagination correctly - // Return container for CLOSED state with startId=0, empty otherwise - when(scmClientMock.getListOfContainers(anyLong(), anyInt(), - any(HddsProtos.LifeCycleState.class))).thenAnswer(invocation -> { - long startId = invocation.getArgument(0); - HddsProtos.LifeCycleState state = invocation.getArgument(2); - // Only return container for CLOSED state and startId=0 - if (state == HddsProtos.LifeCycleState.CLOSED && startId == 0) { - return scmContainers; - } - return Collections.emptyList(); - }); - when(containerManagerMock.getContainer(scmContainer.containerID())) - .thenThrow(new ContainerNotFoundException("Container not found in Recon")); - - ReplicationManagerReport report = createMockReport(0, 1, 0, 0); - // Use any() matcher since getListOfContainers returns a list with the same container - when(scmClientMock.checkContainerStatus(any(ContainerInfo.class))).thenReturn(report); - - ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); - reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); - - ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( - containerManagerMock, - scmClientMock, - schemaManagerV2, - mock(PlacementPolicy.class), - mock(ReconContainerMetadataManager.class), - new OzoneConfiguration(), - reconTaskConfig, - getMockTaskStatusUpdaterManager(), - mock(ReconStorageContainerManagerFacade.class)); - - taskV2.start(); - - // V2 table should have the unhealthy container from SCM - LambdaTestUtils.await(10000, 500, () -> - (unHealthyContainersV2TableHandle.count() == 1)); - - List records = - unHealthyContainersV2TableHandle.fetchByContainerId(1L); - assertEquals(1, records.size()); - assertEquals("UNDER_REPLICATED", records.get(0).getContainerState()); - assertThat(records.get(0).getReason()).contains("not in Recon"); - - taskV2.stop(); - } - - @Test - public void testContainerInReconButNotInSCM() throws Exception { - UnhealthyContainersV2Dao unHealthyContainersV2TableHandle = - getDao(UnhealthyContainersV2Dao.class); - - ContainerHealthSchemaManagerV2 schemaManagerV2 = - new ContainerHealthSchemaManagerV2( - getSchemaDefinition(ContainerSchemaDefinitionV2.class), - unHealthyContainersV2TableHandle); - - ContainerManager containerManagerMock = mock(ContainerManager.class); - StorageContainerServiceProvider scmClientMock = - mock(StorageContainerServiceProvider.class); - - // Recon has 1 container - List reconContainers = getMockContainers(1); - when(containerManagerMock.getContainers(any(ContainerID.class), anyInt())) - .thenReturn(reconContainers); - - ContainerInfo reconContainer = reconContainers.get(0); - when(containerManagerMock.getContainer(reconContainer.containerID())) - .thenReturn(reconContainer); - - // SCM doesn't have this container - when(scmClientMock.checkContainerStatus(reconContainer)) - .thenThrow(new ContainerNotFoundException("Container not found in SCM")); - when(scmClientMock.getListOfContainers(anyLong(), anyInt(), - any(HddsProtos.LifeCycleState.class))).thenReturn(Collections.emptyList()); - - // Insert a record for this container first - UnhealthyContainersV2 record = new UnhealthyContainersV2(); - record.setContainerId(1L); - record.setContainerState("UNDER_REPLICATED"); - record.setExpectedReplicaCount(3); - record.setActualReplicaCount(2); - record.setReplicaDelta(1); - record.setInStateSince(System.currentTimeMillis()); - record.setReason("Test"); - unHealthyContainersV2TableHandle.insert(record); - - ReconTaskConfig reconTaskConfig = new ReconTaskConfig(); - reconTaskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); - - ContainerHealthTaskV2 taskV2 = new ContainerHealthTaskV2( - containerManagerMock, - scmClientMock, - schemaManagerV2, - mock(PlacementPolicy.class), - mock(ReconContainerMetadataManager.class), - new OzoneConfiguration(), - reconTaskConfig, - getMockTaskStatusUpdaterManager(), - mock(ReconStorageContainerManagerFacade.class)); - - taskV2.start(); - - // Container should be removed from V2 table since it doesn't exist in SCM - LambdaTestUtils.await(10000, 500, () -> - (unHealthyContainersV2TableHandle.count() == 0)); - - taskV2.stop(); - } - - private ReconTaskStatusUpdaterManager getMockTaskStatusUpdaterManager() { - ReconTaskStatusUpdaterManager reconTaskStatusUpdaterManager = - mock(ReconTaskStatusUpdaterManager.class); - ReconTaskStatusUpdater mockUpdater = mock(ReconTaskStatusUpdater.class); - when(reconTaskStatusUpdaterManager.getTaskStatusUpdater(any(String.class))) - .thenReturn(mockUpdater); - return reconTaskStatusUpdaterManager; - } - - private ReplicationManagerReport createMockReport( - long missing, long underRep, long overRep, long misRep) { - ReplicationManagerReport report = mock(ReplicationManagerReport.class); - when(report.getStat(ReplicationManagerReport.HealthState.MISSING)).thenReturn(missing); - when(report.getStat(ReplicationManagerReport.HealthState.UNDER_REPLICATED)).thenReturn(underRep); - when(report.getStat(ReplicationManagerReport.HealthState.OVER_REPLICATED)).thenReturn(overRep); - when(report.getStat(ReplicationManagerReport.HealthState.MIS_REPLICATED)).thenReturn(misRep); - return report; - } - - 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) - .setDataChecksum(1234L) - .build()); - } - return replicas; - } - - 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) - .setDataChecksum(checksum) - .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; - } -} 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..f05bd2a4d507 --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestReconReplicationManager.java @@ -0,0 +1,173 @@ +/* + * 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.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.ZoneId; +import java.util.ArrayList; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.scm.PlacementPolicy; +import org.apache.hadoop.hdds.scm.container.ContainerManager; +import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager; +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.ContainerHealthSchemaManagerV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Smoke tests for ReconReplicationManager (Option 4 - 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 ContainerHealthSchemaManagerV2 schemaManagerV2; + private UnhealthyContainersV2Dao dao; + private ContainerManager containerManager; + private ReconReplicationManager reconRM; + + public TestReconReplicationManager() { + super(); + } + + @BeforeEach + public void setUp() throws Exception { + dao = getDao(UnhealthyContainersV2Dao.class); + schemaManagerV2 = new ContainerHealthSchemaManagerV2( + getSchemaDefinition(ContainerSchemaDefinitionV2.class), dao); + + 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 + reconRM = new ReconReplicationManager( + new ReplicationManager.ReplicationManagerConfiguration(), + new OzoneConfiguration(), + containerManager, + placementPolicy, + placementPolicy, + new EventQueue(), + scmContext, + nodeManager, + Clock.system(ZoneId.systemDefault()), + schemaManagerV2 + ); + } + + @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.UnhealthyContainersV2 record = + new org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2(); + 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 + } +} From 96578c558834a4003221d24ddb33225b420306b2 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 21 Nov 2025 19:00:33 +0530 Subject: [PATCH 14/43] HDDS-13891. Fixed findbugs errors. --- .../recon/TestReconTasksV2MultiNode.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java index 3d0b2bc2dc49..f0d8008a3476 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -19,18 +19,13 @@ import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Duration; import java.util.List; -import org.apache.hadoop.hdds.client.RatisReplicationConfig; import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; -import org.apache.hadoop.hdds.scm.container.ContainerInfo; -import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; -import org.apache.hadoop.hdds.scm.server.StorageContainerManager; import org.apache.hadoop.ozone.MiniOzoneCluster; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; @@ -107,23 +102,15 @@ public void testContainerHealthTaskV2UnderReplicated() throws Exception { (ReconStorageContainerManagerFacade) testRecon.getReconServer().getReconStorageContainerManager(); - StorageContainerManager scm = testCluster.getStorageContainerManager(); PipelineManager reconPipelineManager = reconScm.getPipelineManager(); // Make sure Recon's pipeline state is initialized LambdaTestUtils.await(60000, 5000, () -> (!reconPipelineManager.getPipelines().isEmpty())); - ContainerManager scmContainerManager = scm.getContainerManager(); ReconContainerManager reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); - // Create a container with replication factor 3 - ContainerInfo containerInfo = - scmContainerManager.allocateContainer( - RatisReplicationConfig.getInstance( - HddsProtos.ReplicationFactor.THREE), "test"); - // Verify the query mechanism for UNDER_REPLICATED state works List underReplicatedContainers = reconContainerManager.getContainerSchemaManagerV2() @@ -183,22 +170,15 @@ public void testContainerHealthTaskV2OverReplicated() throws Exception { (ReconStorageContainerManagerFacade) testRecon.getReconServer().getReconStorageContainerManager(); - StorageContainerManager scm = testCluster.getStorageContainerManager(); PipelineManager reconPipelineManager = reconScm.getPipelineManager(); // Make sure Recon's pipeline state is initialized LambdaTestUtils.await(60000, 5000, () -> (!reconPipelineManager.getPipelines().isEmpty())); - ContainerManager scmContainerManager = scm.getContainerManager(); ReconContainerManager reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); - // Create a container with replication factor 1 - ContainerInfo containerInfo = - scmContainerManager.allocateContainer( - RatisReplicationConfig.getInstance(ONE), "test"); - // Note: Creating over-replication in integration tests is challenging // as it requires artificially adding extra replicas. In production, // over-replication can occur when: From 37ca75e1b048cef9ce0d248234a584be62933537 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sun, 23 Nov 2025 14:45:36 +0530 Subject: [PATCH 15/43] HDDS-13891. Fixed failed test cases. --- .../recon/fsck/ReconReplicationManager.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index aa61701d63ed..a7dc4d31fd19 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -109,6 +109,28 @@ public ReconReplicationManager( this.healthSchemaManager = healthSchemaManager; } + /** + * Override start() to prevent background threads from running. + * + *

In Recon, we don't want the ReplicationManager's background threads + * (replicationMonitor, underReplicatedProcessor, overReplicatedProcessor) + * to run continuously. Instead, we call processAll() manually from + * ContainerHealthTaskV2 on a schedule.

+ * + *

This prevents: + *

    + *
  • Unnecessary CPU usage from continuous monitoring
  • + *
  • Initialization race conditions (start() being called before fields are initialized)
  • + *
  • Replication commands being generated (Recon is read-only)
  • + *
+ *

+ */ + @Override + public synchronized void start() { + LOG.info("ReconReplicationManager.start() called - no-op (manual invocation via processAll())"); + // Do nothing - we call processAll() manually from ContainerHealthTaskV2 + } + /** * Override processAll() to capture ALL per-container health states, * not just aggregate counts and 100 samples. From 174a55fdc2ef140db8926b8a2a571c8fe70a958b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 24 Nov 2025 10:44:31 +0530 Subject: [PATCH 16/43] HDDS-13891. Remove unused old code. --- .../StorageContainerLocationProtocol.java | 12 ------------ ...rLocationProtocolClientSideTranslatorPB.java | 17 ----------------- ...rLocationProtocolServerSideTranslatorPB.java | 15 --------------- .../scm/server/SCMClientProtocolServer.java | 10 ---------- .../ozone/recon/fsck/ContainerHealthTaskV2.java | 2 -- .../scm/ReconStorageContainerManagerFacade.java | 1 - .../spi/StorageContainerServiceProvider.java | 12 ------------ .../StorageContainerServiceProviderImpl.java | 6 ------ 8 files changed, 75 deletions(-) diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java index 841da72867c8..56411453fc8e 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java @@ -405,18 +405,6 @@ Map> getSafeModeRuleStatuses() */ ReplicationManagerReport getReplicationManagerReport() throws IOException; - /** - * Checks the health status of a specific container using SCM's - * ReplicationManager. This allows Recon to query SCM for the - * authoritative health state of individual containers. - * - * @param containerInfo the container to check - * @return ReplicationManagerReport containing health state for this container - * @throws IOException if the check fails or container is not found - */ - ReplicationManagerReport checkContainerStatus(ContainerInfo containerInfo) - throws IOException; - /** * Start ContainerBalancer. * @return {@link StartContainerBalancerResponseProto} that contains the diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java index 169b7699605c..2a85e6e40071 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java @@ -48,8 +48,6 @@ import org.apache.hadoop.hdds.protocol.proto.HddsProtos.UpgradeFinalizationStatus; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ActivatePipelineRequestProto; -import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.CheckContainerStatusRequestProto; -import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.CheckContainerStatusResponseProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ClosePipelineRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoResponseProto; @@ -902,21 +900,6 @@ public ReplicationManagerReport getReplicationManagerReport() return ReplicationManagerReport.fromProtobuf(response.getReport()); } - @Override - public ReplicationManagerReport checkContainerStatus( - ContainerInfo containerInfo) throws IOException { - CheckContainerStatusRequestProto request = - CheckContainerStatusRequestProto.newBuilder() - .setContainerInfo(containerInfo.getProtobuf()) - .setTraceID(TracingUtil.exportCurrentSpan()) - .build(); - CheckContainerStatusResponseProto response = - submitRequest(Type.CheckContainerStatus, - builder -> builder.setCheckContainerStatusRequest(request)) - .getCheckContainerStatusResponse(); - return ReplicationManagerReport.fromProtobuf(response.getReport()); - } - @Override public StartContainerBalancerResponseProto startContainerBalancer( Optional threshold, Optional iterations, diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java index c19193dcbeb2..b9d4b9d6aef5 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java @@ -583,13 +583,6 @@ public ScmContainerLocationResponse processRequest( .setGetReplicationManagerReportResponse(getReplicationManagerReport( request.getReplicationManagerReportRequest())) .build(); - case CheckContainerStatus: - return ScmContainerLocationResponse.newBuilder() - .setCmdType(request.getCmdType()) - .setStatus(Status.OK) - .setCheckContainerStatusResponse(checkContainerStatus( - request.getCheckContainerStatusRequest())) - .build(); case StartContainerBalancer: return ScmContainerLocationResponse.newBuilder() .setCmdType(request.getCmdType()) @@ -1123,14 +1116,6 @@ public ReplicationManagerReportResponseProto getReplicationManagerReport( .build(); } - public StorageContainerLocationProtocolProtos.CheckContainerStatusResponseProto checkContainerStatus( - StorageContainerLocationProtocolProtos.CheckContainerStatusRequestProto request) throws IOException { - ContainerInfo containerInfo = ContainerInfo.fromProtobuf(request.getContainerInfo()); - return StorageContainerLocationProtocolProtos.CheckContainerStatusResponseProto.newBuilder() - .setReport(impl.checkContainerStatus(containerInfo).toProtobuf()) - .build(); - } - public StartContainerBalancerResponseProto startContainerBalancer( StartContainerBalancerRequestProto request) throws IOException { diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java index 8d2c272f276d..c4d333632ef5 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java @@ -1081,16 +1081,6 @@ public ReplicationManagerReport getReplicationManagerReport() { return scm.getReplicationManager().getContainerReport(); } - @Override - public ReplicationManagerReport checkContainerStatus( - ContainerInfo containerInfo) throws IOException { - AUDIT.logReadSuccess(buildAuditMessageForSuccess( - SCMAction.CHECK_CONTAINER_STATUS, null)); - ReplicationManagerReport report = new ReplicationManagerReport(); - scm.getReplicationManager().checkContainerStatus(containerInfo, report); - return report; - } - @Override public StatusAndMessages finalizeScmUpgrade(String upgradeClientID) throws IOException { diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 5e747bcfe9ff..b106aba94c50 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -25,7 +25,6 @@ import org.apache.hadoop.ozone.recon.scm.ReconScmTask; 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.ReconTaskStatusUpdaterManager; import org.slf4j.Logger; @@ -67,7 +66,6 @@ public class ContainerHealthTaskV2 extends ReconScmTask { @SuppressWarnings("checkstyle:ParameterNumber") public ContainerHealthTaskV2( ContainerManager containerManager, - StorageContainerServiceProvider scmClient, ContainerHealthSchemaManagerV2 schemaManagerV2, PlacementPolicy placementPolicy, ReconContainerMetadataManager reconContainerMetadataManager, 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 638f06d85285..0f842420de81 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 @@ -290,7 +290,6 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, LOG.info("Creating ContainerHealthTaskV2"); containerHealthTaskV2 = new ContainerHealthTaskV2( containerManager, - scmServiceProvider, containerHealthSchemaManagerV2, containerPlacementPolicy, reconContainerMetadataManager, diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java index 72f1a7d6e572..412bd3027662 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/StorageContainerServiceProvider.java @@ -21,7 +21,6 @@ import java.util.List; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; import org.apache.hadoop.hdds.scm.container.ContainerInfo; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline; import org.apache.hadoop.hdds.scm.pipeline.Pipeline; import org.apache.hadoop.hdds.utils.db.DBCheckpoint; @@ -99,15 +98,4 @@ List getListOfContainers(long startContainerID, * @return Total number of containers in SCM. */ long getContainerCount(HddsProtos.LifeCycleState state) throws IOException; - - /** - * Checks the health status of a specific container using SCM's - * ReplicationManager. This allows Recon to query SCM for the - * authoritative health state of individual containers. - * - * @param containerInfo the container to check - * @return ReplicationManagerReport containing health state for this container - * @throws IOException if the check fails or container is not found - */ - ReplicationManagerReport checkContainerStatus(ContainerInfo containerInfo) throws IOException; } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java index 7c79990b625c..3b6164447b3c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/StorageContainerServiceProviderImpl.java @@ -36,7 +36,6 @@ import org.apache.hadoop.hdds.protocolPB.SCMSecurityProtocolClientSideTranslatorPB; import org.apache.hadoop.hdds.scm.ScmConfigKeys; import org.apache.hadoop.hdds.scm.container.ContainerInfo; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline; import org.apache.hadoop.hdds.scm.ha.InterSCMGrpcClient; import org.apache.hadoop.hdds.scm.ha.SCMSnapshotDownloader; @@ -192,9 +191,4 @@ public List getListOfContainers( return scmClient.getListOfContainers(startContainerID, count, state); } - @Override - public ReplicationManagerReport checkContainerStatus(ContainerInfo containerInfo) throws IOException { - return scmClient.checkContainerStatus(containerInfo); - } - } From be641f69c83640ffa0d4265d76ddd914238fe5fc Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 24 Nov 2025 10:47:56 +0530 Subject: [PATCH 17/43] HDDS-13891. Remove unused old code. --- .../src/main/proto/ScmAdminProtocol.proto | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto index 3e55bb769806..3dfdea4c7324 100644 --- a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto +++ b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto @@ -86,7 +86,6 @@ message ScmContainerLocationRequest { optional GetMetricsRequestProto getMetricsRequest = 47; optional ContainerBalancerStatusInfoRequestProto containerBalancerStatusInfoRequest = 48; optional ReconcileContainerRequestProto reconcileContainerRequest = 49; - optional CheckContainerStatusRequestProto checkContainerStatusRequest = 50; } message ScmContainerLocationResponse { @@ -144,7 +143,6 @@ message ScmContainerLocationResponse { optional GetMetricsResponseProto getMetricsResponse = 47; optional ContainerBalancerStatusInfoResponseProto containerBalancerStatusInfoResponse = 48; optional ReconcileContainerResponseProto reconcileContainerResponse = 49; - optional CheckContainerStatusResponseProto checkContainerStatusResponse = 50; enum Status { OK = 1; @@ -201,7 +199,6 @@ enum Type { GetMetrics = 43; GetContainerBalancerStatusInfo = 44; ReconcileContainer = 45; - CheckContainerStatus = 46; } /** @@ -529,15 +526,6 @@ message ReplicationManagerReportResponseProto { required ReplicationManagerReportProto report = 1; } -message CheckContainerStatusRequestProto { - required ContainerInfoProto containerInfo = 1; - optional string traceID = 2; -} - -message CheckContainerStatusResponseProto { - required ReplicationManagerReportProto report = 1; -} - message GetFailedDeletedBlocksTxnRequestProto { optional string traceID = 1; required int32 count = 2; From acf218b8ce83ba0f254e246a087bbbf7c554946d Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 24 Nov 2025 16:00:25 +0530 Subject: [PATCH 18/43] HDDS-13891. Fixed pmd and findbugs. --- .../ozone/recon/fsck/NullContainerReplicaPendingOps.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java index 76f42b9cca23..2b346b512a82 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java @@ -42,10 +42,6 @@ */ public class NullContainerReplicaPendingOps extends ContainerReplicaPendingOps { - public NullContainerReplicaPendingOps(Clock clock) { - super(clock); - } - public NullContainerReplicaPendingOps(Clock clock, ReplicationManager.ReplicationManagerConfiguration rmConf) { super(clock, rmConf); From 4541173665ae3f86c05deddf7d1aaf09f304fdd9 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 24 Nov 2025 17:31:23 +0530 Subject: [PATCH 19/43] HDDS-13891. Added REPLICA_MISMATCH computation logic to ContainerHealthTaskV2 implementation. --- .../hadoop/ozone/recon/TestReconTasks.java | 2 +- .../recon/TestReconTasksV2MultiNode.java | 2 +- .../recon/fsck/ContainerHealthTaskV2.java | 12 +-- .../recon/fsck/ReconReplicationManager.java | 87 +++++++++++++++++-- .../fsck/ReconReplicationManagerReport.java | 36 ++++++++ .../ReconStorageContainerManagerFacade.java | 4 +- .../fsck/TestReconReplicationManager.java | 2 +- 7 files changed, 125 insertions(+), 20 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 00d39f584114..98d5c11bf7b2 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -458,6 +458,6 @@ public void testContainerHealthTaskV2MisReplicated() throws Exception { // - ContainerHealthTaskV2 would detect this as MIS_REPLICATED // // The detection logic in SCM's ReplicationManager handles this, - // and our Option 4 implementation reuses that logic. + // and our Recon's RM logic implementation reuses that logic. } } diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java index f0d8008a3476..4466a78c0150 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -62,7 +62,7 @@ public class TestReconTasksV2MultiNode { * * The detection logic is tested end-to-end in: * - TestReconTasks.testContainerHealthTaskV2WithSCMSync() - which proves - * Option 4 works for MISSING containers (similar detection logic) + * Recon's RM logic works for MISSING containers (similar detection logic) * * Full end-to-end test for UNDER_REPLICATED would require: * 1. Allocate container with RF=3 diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index b106aba94c50..a052c6b68cec 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -31,9 +31,9 @@ import org.slf4j.LoggerFactory; /** - * V2 implementation of Container Health Task using Option 4: Local ReplicationManager. + * V2 implementation of Container Health Task using Local ReplicationManager. * - *

Option 4 Architecture:

+ *

Solution:

*
    *
  • Uses Recon's local ReplicationManager (not RPC to SCM)
  • *
  • Calls processAll() once to check all containers in batch
  • @@ -42,7 +42,7 @@ *
  • All database operations handled inside ReconReplicationManager
  • *
* - *

Benefits over Option 3:

+ *

Benefits over RPC call to SCM 3:

*
    *
  • Zero RPC overhead (no per-container calls to SCM)
  • *
  • Zero SCM load
  • @@ -76,7 +76,7 @@ public ContainerHealthTaskV2( super(taskStatusUpdaterManager); this.reconScm = reconScm; this.interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); - LOG.info("Initialized ContainerHealthTaskV2 with Option 4 (Local ReplicationManager), interval={}ms", + LOG.info("Initialized ContainerHealthTaskV2 with Local ReplicationManager, interval={}ms", interval); } @@ -101,7 +101,7 @@ protected void run() { } /** - * Main task execution - uses Recon's local ReplicationManager (Option 4). + * Main task execution - uses Recon's local ReplicationManager. * *

    Simply calls processAll() on ReconReplicationManager, which: *

      @@ -112,7 +112,7 @@ protected void run() { */ @Override protected void runTask() throws Exception { - LOG.info("ContainerHealthTaskV2 starting - using local ReplicationManager (Option 4)"); + LOG.info("ContainerHealthTaskV2 starting - using local ReplicationManager"); // Get Recon's ReplicationManager (actually a ReconReplicationManager instance) ReconReplicationManager reconRM = diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index a7dc4d31fd19..336304b8ea33 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.apache.hadoop.hdds.conf.ConfigurationSource; import org.apache.hadoop.hdds.scm.PlacementPolicy; @@ -131,6 +132,43 @@ public synchronized void start() { // Do nothing - we call processAll() manually from ContainerHealthTaskV2 } + /** + * Checks if container replicas have mismatched data checksums. + * This is a Recon-specific check not done by SCM's ReplicationManager. + * + *

      REPLICA_MISMATCH detection is crucial for identifying: + *

        + *
      • Bit rot (silent data corruption)
      • + *
      • Failed writes to some replicas
      • + *
      • Storage corruption on specific datanodes
      • + *
      • Network corruption during replication
      • + *
      + *

      + * + *

      This matches the legacy ContainerHealthTask logic: + * {@code replicas.stream().map(ContainerReplica::getDataChecksum).distinct().count() != 1} + *

      + * + * @param replicas Set of container replicas to check + * @return true if replicas have different data checksums + */ + private boolean hasDataChecksumMismatch(Set replicas) { + if (replicas == null || replicas.isEmpty()) { + return false; + } + + // Count distinct checksums (filter out nulls) + long distinctChecksums = replicas.stream() + .map(ContainerReplica::getDataChecksum) + .filter(Objects::nonNull) + .distinct() + .count(); + + // More than 1 distinct checksum = data mismatch + // 0 distinct checksums = all nulls, no mismatch + return distinctChecksums > 1; + } + /** * Override processAll() to capture ALL per-container health states, * not just aggregate counts and 100 samples. @@ -138,7 +176,8 @@ public synchronized void start() { *

      Processing Flow:

      *
        *
      1. Get all containers from ContainerManager
      2. - *
      3. Process each container using inherited health check chain
      4. + *
      5. Process each container using inherited health check chain (SCM logic)
      6. + *
      7. Additionally check for REPLICA_MISMATCH (Recon-specific)
      8. *
      9. Capture ALL unhealthy container IDs per health state (no sampling limit)
      10. *
      11. Store results in Recon's UNHEALTHY_CONTAINERS_V2 table
      12. *
      @@ -147,6 +186,7 @@ public synchronized void start() { *
        *
      • Uses ReconReplicationManagerReport (captures all containers)
      • *
      • Uses NullReplicationQueue (doesn't enqueue commands)
      • + *
      • Adds REPLICA_MISMATCH detection (not done by SCM)
      • *
      • Stores results in database instead of just keeping in-memory report
      • *
      */ @@ -170,9 +210,19 @@ public synchronized void processAll() { for (ContainerInfo container : containers) { report.increment(container.getState()); try { - // Call inherited processContainer - this runs the health check chain + ContainerID cid = container.containerID(); + + // Call inherited processContainer - this runs SCM's health check chain // readOnly=true ensures no commands are generated processContainer(container, nullQueue, report, true); + + // ADDITIONAL CHECK: Detect REPLICA_MISMATCH (Recon-specific, not in SCM) + Set replicas = containerManager.getContainerReplicas(cid); + if (hasDataChecksumMismatch(replicas)) { + report.addReplicaMismatchContainer(cid); + LOG.debug("Container {} has data checksum mismatch across replicas", cid); + } + processedCount++; if (processedCount % 10000 == 0) { @@ -213,11 +263,12 @@ private void storeHealthStatesToDatabase( report.getAllContainersByState(); LOG.info("Processing health states: MISSING={}, UNDER_REPLICATED={}, " + - "OVER_REPLICATED={}, MIS_REPLICATED={}", + "OVER_REPLICATED={}, MIS_REPLICATED={}, REPLICA_MISMATCH={}", report.getAllContainersCount(HealthState.MISSING), report.getAllContainersCount(HealthState.UNDER_REPLICATED), report.getAllContainersCount(HealthState.OVER_REPLICATED), - report.getAllContainersCount(HealthState.MIS_REPLICATED)); + report.getAllContainersCount(HealthState.MIS_REPLICATED), + report.getReplicaMismatchCount()); // Process MISSING containers List missingContainers = @@ -230,7 +281,7 @@ private void storeHealthStatesToDatabase( UnHealthyContainerStates.MISSING, currentTime, expected, 0, "No replicas available")); } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing MISSING state", cid); + LOG.warn("Container {} not found when processing MISSING state", cid, e); } } @@ -247,7 +298,7 @@ private void storeHealthStatesToDatabase( UnHealthyContainerStates.UNDER_REPLICATED, currentTime, expected, actual, "Insufficient replicas")); } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing UNDER_REPLICATED state", cid); + LOG.warn("Container {} not found when processing UNDER_REPLICATED state", cid, e); } } @@ -264,7 +315,7 @@ private void storeHealthStatesToDatabase( UnHealthyContainerStates.OVER_REPLICATED, currentTime, expected, actual, "Excess replicas")); } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing OVER_REPLICATED state", cid); + LOG.warn("Container {} not found when processing OVER_REPLICATED state", cid, e); } } @@ -285,6 +336,22 @@ private void storeHealthStatesToDatabase( } } + // Process REPLICA_MISMATCH containers (Recon-specific) + List replicaMismatchContainers = report.getReplicaMismatchContainers(); + for (ContainerID cid : replicaMismatchContainers) { + try { + ContainerInfo container = containerManager.getContainer(cid); + Set replicas = containerManager.getContainerReplicas(cid); + int expected = container.getReplicationConfig().getRequiredNodes(); + int actual = replicas.size(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.REPLICA_MISMATCH, currentTime, expected, actual, + "Data checksum mismatch across replicas")); + } catch (ContainerNotFoundException e) { + LOG.warn("Container {} not found when processing REPLICA_MISMATCH state", cid, e); + } + } + // Collect all container IDs for SCM state deletion for (ContainerInfo container : allContainers) { containerIdsToDelete.add(container.getContainerID()); @@ -297,9 +364,11 @@ private void storeHealthStatesToDatabase( LOG.info("Inserting {} unhealthy container records", recordsToInsert.size()); healthSchemaManager.insertUnhealthyContainerRecords(recordsToInsert); - LOG.info("Stored {} MISSING, {} UNDER_REPLICATED, {} OVER_REPLICATED, {} MIS_REPLICATED", + LOG.info("Stored {} MISSING, {} UNDER_REPLICATED, {} OVER_REPLICATED, " + + "{} MIS_REPLICATED, {} REPLICA_MISMATCH", missingContainers.size(), underRepContainers.size(), - overRepContainers.size(), misRepContainers.size()); + overRepContainers.size(), misRepContainers.size(), + replicaMismatchContainers.size()); } /** diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java index 99aea5ae15b3..17798d1b4d85 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java @@ -38,6 +38,10 @@ * the sampling limitation while maintaining backward compatibility by still * calling the parent's incrementAndSample() method.

      * + *

      REPLICA_MISMATCH Handling: Since SCM's HealthState enum doesn't include + * REPLICA_MISMATCH (it's a Recon-specific check for data checksum mismatches), + * we track it separately in replicaMismatchContainers.

      + * *

      Memory Impact: For a cluster with 100K containers and 5% unhealthy rate, * this adds approximately 620KB of memory during report generation (5K containers * × 124 bytes per container). Even in worst case (100% unhealthy), memory usage @@ -49,6 +53,9 @@ public class ReconReplicationManagerReport extends ReplicationManagerReport { private final Map> allContainersByState = new HashMap<>(); + // Captures containers with REPLICA_MISMATCH (Recon-specific, not in SCM's HealthState) + private final List replicaMismatchContainers = new ArrayList<>(); + /** * Override to capture ALL containers, not just first 100 samples. * Still calls parent method to maintain aggregate counts and samples @@ -109,5 +116,34 @@ public int getAllContainersCount(HealthState stat) { */ public void clearAllContainers() { allContainersByState.clear(); + replicaMismatchContainers.clear(); + } + + /** + * Add a container to the REPLICA_MISMATCH list. + * This is a Recon-specific health state not tracked by SCM. + * + * @param container The container ID with replica checksum mismatch + */ + public void addReplicaMismatchContainer(ContainerID container) { + replicaMismatchContainers.add(container); + } + + /** + * Get all containers with REPLICA_MISMATCH state. + * + * @return List of container IDs with data checksum mismatches, or empty list + */ + public List getReplicaMismatchContainers() { + return Collections.unmodifiableList(replicaMismatchContainers); + } + + /** + * Get count of containers with REPLICA_MISMATCH. + * + * @return Number of containers with replica checksum mismatches + */ + public int getReplicaMismatchCount() { + return replicaMismatchContainers.size(); } } 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 0f842420de81..3cb62c3c24e0 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 @@ -305,9 +305,9 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; this.dataSource = dataSource; - // Initialize Recon's ReplicationManager for Option 4 (local health checks) + // Initialize Recon's ReplicationManager for local health checks try { - LOG.info("Creating ReconReplicationManager (Option 4)"); + LOG.info("Creating ReconReplicationManager"); this.reconReplicationManager = new ReconReplicationManager( conf.getObject(ReplicationManager.ReplicationManagerConfiguration.class), conf, 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 index f05bd2a4d507..e72789aca3a9 100644 --- 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 @@ -40,7 +40,7 @@ import org.junit.jupiter.api.Test; /** - * Smoke tests for ReconReplicationManager (Option 4 - Local ReplicationManager). + * Smoke tests for ReconReplicationManager Local ReplicationManager. * * These tests verify that: * 1. ReconReplicationManager can be instantiated properly From eca50cbb0db408b95b0da814cb9f2a6aa480078d Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 24 Nov 2025 18:11:38 +0530 Subject: [PATCH 20/43] HDDS-13891. Remove unused old code. --- .../src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java index da2c85adbe96..95e13146deed 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java @@ -52,7 +52,6 @@ public enum SCMAction implements AuditAction { GET_CONTAINER_WITH_PIPELINE_BATCH, ADD_SCM, GET_REPLICATION_MANAGER_REPORT, - CHECK_CONTAINER_STATUS, TRANSFER_LEADERSHIP, GET_CONTAINER_REPLICAS, GET_CONTAINERS_ON_DECOM_NODE, From d2ff60c2624d513a98e7a9a21595c18538b32181 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 20 Feb 2026 11:16:17 +0530 Subject: [PATCH 21/43] HDDS-13891. Removed container health task old V1 code and made sampling llimit in recon to 0 and some other review comments. --- .../hadoop/hdds/recon/ReconConfigKeys.java | 12 - ...nQueue.java => NoOpsReplicationQueue.java} | 4 +- .../replication/ReplicationManager.java | 6 +- .../recon/TestReconAndAdminContainerCLI.java | 2 +- .../recon/TestReconContainerEndpoint.java | 9 +- .../ozone/recon/TestReconEndpointUtil.java | 4 +- .../hadoop/ozone/recon/TestReconTasks.java | 463 ---------- .../schema/ContainerSchemaDefinition.java | 110 --- .../schema/ContainerSchemaDefinitionV2.java | 4 +- .../schema/ReconSchemaGenerationModule.java | 1 - .../ozone/recon/ReconControllerModule.java | 4 - .../ozone/recon/api/ClusterStateEndpoint.java | 21 +- .../ozone/recon/api/ContainerEndpoint.java | 117 +-- .../api/types/UnhealthyContainerMetadata.java | 22 +- .../types/UnhealthyContainersResponse.java | 2 +- .../ozone/recon/fsck/ContainerHealthTask.java | 768 ----------------- .../recon/fsck/ContainerHealthTaskV2.java | 15 +- ...a => NoOpsContainerReplicaPendingOps.java} | 19 +- .../recon/fsck/ReconReplicationManager.java | 337 +++++--- .../fsck/ReconReplicationManagerReport.java | 41 +- .../recon/metrics/ContainerHealthMetrics.java | 86 -- .../ContainerHealthSchemaManager.java | 231 ----- .../ContainerHealthSchemaManagerV2.java | 110 +-- .../recon/scm/ReconContainerManager.java | 9 - .../ReconStorageContainerManagerFacade.java | 50 +- .../InitialConstraintUpgradeAction.java | 97 --- ...healthyContainerReplicaMismatchAction.java | 90 -- .../recon/api/TestClusterStateEndpoint.java | 20 +- .../recon/api/TestContainerEndpoint.java | 54 +- .../api/TestDeletedKeysSearchEndpoint.java | 2 - .../hadoop/ozone/recon/api/TestEndpoints.java | 10 +- .../recon/api/TestOmDBInsightEndPoint.java | 2 - .../recon/api/TestOpenContainerCount.java | 2 - .../recon/api/TestOpenKeysSearchEndpoint.java | 2 - .../recon/api/TestTriggerDBSyncEndpoint.java | 2 - .../recon/fsck/TestContainerHealthTask.java | 804 ------------------ ...estContainerHealthTaskRecordGenerator.java | 710 ---------------- .../fsck/TestReconReplicationManager.java | 144 +++- .../ozone/recon/heatmap/TestHeatMapInfo.java | 2 - .../TestSchemaVersionTableDefinition.java | 6 +- .../AbstractReconContainerManagerTest.java | 2 - .../TestInitialConstraintUpgradeAction.java | 188 ---- 42 files changed, 518 insertions(+), 4066 deletions(-) rename hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/{NullReplicationQueue.java => NoOpsReplicationQueue.java} (91%) delete mode 100644 hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java delete mode 100644 hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java delete mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java rename hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/{NullContainerReplicaPendingOps.java => NoOpsContainerReplicaPendingOps.java} (88%) delete mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthMetrics.java delete mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java delete mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java delete mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java delete mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java delete mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTaskRecordGenerator.java delete mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java index 5a01e37ea42b..f73b06ebb130 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/recon/ReconConfigKeys.java @@ -77,18 +77,6 @@ public final class ReconConfigKeys { public static final String OZONE_RECON_TASK_SAFEMODE_WAIT_THRESHOLD = "ozone.recon.task.safemode.wait.threshold"; - /** - * Configuration key to enable SCM-based container health reporting. - * When true, Recon uses ContainerHealthTaskV2 which leverages SCM's - * ReplicationManager.checkContainerStatus() as the single source of truth. - * When false (default), Recon uses the legacy ContainerHealthTask - * implementation. - */ - public static final String OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT = - "ozone.recon.container.health.use.scm.report"; - public static final boolean - OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT_DEFAULT = false; - /** * This class contains constants for Recon related configuration keys used in * SCM and Datanode. diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NullReplicationQueue.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NoOpsReplicationQueue.java similarity index 91% rename from hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NullReplicationQueue.java rename to hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NoOpsReplicationQueue.java index dd7e8dc9b899..375540d69d98 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NullReplicationQueue.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NoOpsReplicationQueue.java @@ -18,11 +18,11 @@ package org.apache.hadoop.hdds.scm.container.replication; /** - * A class which extents ReplicationQueue and does nothing. This is used when + * A class which extends ReplicationQueue and does nothing. This is used when * checking containers in a read-only mode, where we don't want to queue them * for replication. */ -public class NullReplicationQueue extends ReplicationQueue { +public class NoOpsReplicationQueue extends ReplicationQueue { @Override public void enqueue(ContainerHealthResult.UnderReplicatedHealthResult diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java index 83d3825b66c0..cb8cea4cd1b7 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java @@ -189,8 +189,8 @@ public class ReplicationManager implements SCMService, ContainerReplicaPendingOp private final UnderReplicatedProcessor underReplicatedProcessor; private final OverReplicatedProcessor overReplicatedProcessor; private final HealthCheck containerCheckChain; - private final ReplicationQueue nullReplicationQueue = - new NullReplicationQueue(); + private final ReplicationQueue noOpsReplicationQueue = + new NoOpsReplicationQueue(); /** * Constructs ReplicationManager instance with the given configuration. @@ -1006,7 +1006,7 @@ public ContainerHealthResult getContainerReplicationHealth( public boolean checkContainerStatus(ContainerInfo containerInfo, ReplicationManagerReport report) throws ContainerNotFoundException { report.increment(containerInfo.getState()); - return processContainer(containerInfo, nullReplicationQueue, report, true); + return processContainer(containerInfo, noOpsReplicationQueue, report, true); } /** diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java index 16b1769bb768..015c948b6a6d 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java @@ -82,7 +82,7 @@ import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; import org.apache.ozone.test.GenericTestUtils; import org.apache.ozone.test.LambdaTestUtils; import org.apache.ozone.test.tag.Flaky; diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java index f5055059ac11..8b4703ec1f1c 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java @@ -39,9 +39,7 @@ import org.apache.hadoop.ozone.recon.api.ContainerEndpoint; import org.apache.hadoop.ozone.recon.api.types.KeyMetadata; import org.apache.hadoop.ozone.recon.api.types.KeysResponse; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; -import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.spi.impl.OzoneManagerServiceProviderImpl; import org.apache.hadoop.ozone.recon.tasks.ReconTaskControllerImpl; import org.apache.ozone.test.GenericTestUtils; @@ -215,18 +213,13 @@ public void testContainerEndpointForOBSBucket() throws Exception { private Response getContainerEndpointResponse(long containerId) { OzoneStorageContainerManager reconSCM = recon.getReconServer().getReconStorageContainerManager(); - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconSCM.getContainerManager(); - ContainerHealthSchemaManager containerHealthSchemaManager = - reconContainerManager.getContainerSchemaManager(); ReconOMMetadataManager omMetadataManagerInstance = (ReconOMMetadataManager) recon.getReconServer().getOzoneManagerServiceProvider() .getOMMetadataManagerInstance(); ContainerEndpoint containerEndpoint = - new ContainerEndpoint(reconSCM, containerHealthSchemaManager, + new ContainerEndpoint(reconSCM, null, // ContainerHealthSchemaManagerV2 - not needed for this test - new OzoneConfiguration(), // OzoneConfiguration recon.getReconServer().getReconNamespaceSummaryManager(), recon.getReconServer().getReconContainerMetadataManager(), omMetadataManagerInstance); diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java index 4acafc105817..ffba62dc0035 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java @@ -42,7 +42,7 @@ import org.apache.hadoop.hdds.server.http.HttpConfig; import org.apache.hadoop.hdfs.web.URLConnectionFactory; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,7 +81,7 @@ public static void triggerReconDbSyncWithOm( } public static UnhealthyContainersResponse getUnhealthyContainersFromRecon( - OzoneConfiguration conf, ContainerSchemaDefinition.UnHealthyContainerStates containerState) + OzoneConfiguration conf, ContainerSchemaDefinitionV2.UnHealthyContainerStates containerState) throws JsonProcessingException { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append(getReconWebAddress(conf)) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java deleted file mode 100644 index 485624ab8b71..000000000000 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ /dev/null @@ -1,463 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hadoop.ozone.recon; - -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; -import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; -import static org.apache.hadoop.ozone.container.ozoneimpl.TestOzoneContainer.runTestOzoneContainerViaDataNode; -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.assertNotEquals; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.apache.hadoop.hdds.client.RatisReplicationConfig; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; -import org.apache.hadoop.hdds.protocol.proto.HddsProtos; -import org.apache.hadoop.hdds.scm.XceiverClientGrpc; -import org.apache.hadoop.hdds.scm.container.ContainerInfo; -import org.apache.hadoop.hdds.scm.container.ContainerManager; -import org.apache.hadoop.hdds.scm.pipeline.Pipeline; -import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; -import org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher; -import org.apache.hadoop.hdds.scm.server.StorageContainerManager; -import org.apache.hadoop.hdds.utils.IOUtils; -import org.apache.hadoop.hdds.utils.db.RDBBatchOperation; -import org.apache.hadoop.ozone.MiniOzoneCluster; -import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTask; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; -import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; -import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; -import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; -import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; -import org.apache.ozone.test.GenericTestUtils; -import org.apache.ozone.test.LambdaTestUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.event.Level; - -/** - * Integration Tests for Recon's tasks. - */ -public class TestReconTasks { - private MiniOzoneCluster cluster = null; - private OzoneConfiguration conf; - private ReconService recon; - - @BeforeEach - public void init() throws Exception { - conf = new OzoneConfiguration(); - conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); - conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); - - ReconTaskConfig taskConfig = conf.getObject(ReconTaskConfig.class); - taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); - conf.setFromObject(taskConfig); - - conf.set("ozone.scm.stale.node.interval", "6s"); - conf.set("ozone.scm.dead.node.interval", "8s"); - recon = new ReconService(conf); - cluster = MiniOzoneCluster.newBuilder(conf).setNumDatanodes(1) - .addService(recon) - .build(); - cluster.waitForClusterToBeReady(); - cluster.waitForPipelineTobeReady(ONE, 30000); - GenericTestUtils.setLogLevel(SCMDatanodeHeartbeatDispatcher.class, - Level.DEBUG); - } - - @AfterEach - public void shutdown() { - if (cluster != null) { - cluster.shutdown(); - } - } - - @Test - public void testSyncSCMContainerInfo() throws Exception { - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - StorageContainerManager scm = cluster.getStorageContainerManager(); - ContainerManager scmContainerManager = scm.getContainerManager(); - ContainerManager reconContainerManager = reconScm.getContainerManager(); - final ContainerInfo container1 = scmContainerManager.allocateContainer( - RatisReplicationConfig.getInstance( - HddsProtos.ReplicationFactor.ONE), "admin"); - final ContainerInfo container2 = scmContainerManager.allocateContainer( - RatisReplicationConfig.getInstance( - HddsProtos.ReplicationFactor.ONE), "admin"); - scmContainerManager.updateContainerState(container1.containerID(), - HddsProtos.LifeCycleEvent.FINALIZE); - scmContainerManager.updateContainerState(container2.containerID(), - HddsProtos.LifeCycleEvent.FINALIZE); - scmContainerManager.updateContainerState(container1.containerID(), - HddsProtos.LifeCycleEvent.CLOSE); - scmContainerManager.updateContainerState(container2.containerID(), - HddsProtos.LifeCycleEvent.CLOSE); - int scmContainersCount = scmContainerManager.getContainers().size(); - int reconContainersCount = reconContainerManager - .getContainers().size(); - assertNotEquals(scmContainersCount, reconContainersCount); - reconScm.syncWithSCMContainerInfo(); - reconContainersCount = reconContainerManager - .getContainers().size(); - assertEquals(scmContainersCount, reconContainersCount); - } - - @Test - public void testMissingContainerDownNode() throws Exception { - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - ReconContainerMetadataManager reconContainerMetadataManager = - recon.getReconServer().getReconContainerMetadataManager(); - - StorageContainerManager scm = cluster.getStorageContainerManager(); - PipelineManager reconPipelineManager = reconScm.getPipelineManager(); - PipelineManager scmPipelineManager = scm.getPipelineManager(); - - // Make sure Recon's pipeline state is initialized. - LambdaTestUtils.await(60000, 5000, - () -> (!reconPipelineManager.getPipelines().isEmpty())); - - ContainerManager scmContainerManager = scm.getContainerManager(); - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); - ContainerInfo containerInfo = - scmContainerManager - .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); - long containerID = containerInfo.getContainerID(); - - try (RDBBatchOperation rdbBatchOperation = RDBBatchOperation.newAtomicOperation()) { - reconContainerMetadataManager - .batchStoreContainerKeyCounts(rdbBatchOperation, containerID, 2L); - reconContainerMetadataManager.commitBatchOperation(rdbBatchOperation); - } - - Pipeline pipeline = - scmPipelineManager.getPipeline(containerInfo.getPipelineID()); - XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); - runTestOzoneContainerViaDataNode(containerID, client); - - // Make sure Recon got the container report with new container. - assertEquals(scmContainerManager.getContainers(), - reconContainerManager.getContainers()); - - // Bring down the Datanode that had the container replica. - cluster.shutdownHddsDatanode(pipeline.getFirstNode()); - - LambdaTestUtils.await(120000, 6000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, - Optional.empty(), 1000); - return (allMissingContainers.size() == 1); - }); - - // Restart the Datanode to make sure we remove the missing container. - cluster.restartHddsDatanode(pipeline.getFirstNode(), true); - LambdaTestUtils.await(120000, 10000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, - 0L, Optional.empty(), 1000); - return (allMissingContainers.isEmpty()); - }); - IOUtils.closeQuietly(client); - } - - /** - * This test verifies the count of MISSING and EMPTY_MISSING containers. - * Following steps being followed in a single DN cluster. - * --- Allocate a container in SCM. - * --- Client writes the chunk and put block to only DN successfully. - * --- Shuts down the only DN. - * --- Since container to key mapping doesn't have any key mapped to - * container, missing container will be marked EMPTY_MISSING. - * --- Add a key mapping entry to key container mapping table for the - * container added. - * --- Now container will no longer be marked as EMPTY_MISSING and just - * as MISSING. - * --- Restart the only DN in cluster. - * --- Now container no longer will be marked as MISSING. - * - * @throws Exception - */ - @Test - public void testEmptyMissingContainerDownNode() throws Exception { - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - ReconContainerMetadataManager reconContainerMetadataManager = - recon.getReconServer().getReconContainerMetadataManager(); - StorageContainerManager scm = cluster.getStorageContainerManager(); - PipelineManager reconPipelineManager = reconScm.getPipelineManager(); - PipelineManager scmPipelineManager = scm.getPipelineManager(); - - // Make sure Recon's pipeline state is initialized. - LambdaTestUtils.await(60000, 1000, - () -> (!reconPipelineManager.getPipelines().isEmpty())); - - ContainerManager scmContainerManager = scm.getContainerManager(); - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); - ContainerInfo containerInfo = - scmContainerManager - .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); - long containerID = containerInfo.getContainerID(); - - Pipeline pipeline = - scmPipelineManager.getPipeline(containerInfo.getPipelineID()); - XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); - runTestOzoneContainerViaDataNode(containerID, client); - - // Make sure Recon got the container report with new container. - assertEquals(scmContainerManager.getContainers(), - reconContainerManager.getContainers()); - - // Bring down the Datanode that had the container replica. - cluster.shutdownHddsDatanode(pipeline.getFirstNode()); - - // Since we no longer add EMPTY_MISSING containers to the table, we should - // have zero EMPTY_MISSING containers in the DB but their information will be logged. - LambdaTestUtils.await(25000, 1000, () -> { - List allEmptyMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates. - EMPTY_MISSING, - 0L, Optional.empty(), 1000); - - // Check if EMPTY_MISSING containers are not added to the DB and their count is logged - Map> - unhealthyContainerStateStatsMap = ((ContainerHealthTask) reconScm.getContainerHealthTask()) - .getUnhealthyContainerStateStatsMap(); - - // Return true if the size of the fetched containers is 0 and the log shows 1 for EMPTY_MISSING state - return allEmptyMissingContainers.isEmpty() && - unhealthyContainerStateStatsMap.get( - ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING) - .getOrDefault(CONTAINER_COUNT, 0L) == 1; - }); - - // Now add a container to key mapping count as 3. This data is used to - // identify if container is empty in terms of keys mapped to container. - try (RDBBatchOperation rdbBatchOperation = RDBBatchOperation.newAtomicOperation()) { - reconContainerMetadataManager - .batchStoreContainerKeyCounts(rdbBatchOperation, containerID, 3L); - reconContainerMetadataManager.commitBatchOperation(rdbBatchOperation); - } - - // Verify again and now container is not empty missing but just missing. - LambdaTestUtils.await(25000, 1000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, - 0L, Optional.empty(), 1000); - return (allMissingContainers.size() == 1); - }); - - LambdaTestUtils.await(25000, 1000, () -> { - List allEmptyMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates. - EMPTY_MISSING, - 0L, Optional.empty(), 1000); - - - Map> - unhealthyContainerStateStatsMap = ((ContainerHealthTask) reconScm.getContainerHealthTask()) - .getUnhealthyContainerStateStatsMap(); - - // Return true if the size of the fetched containers is 0 and the log shows 0 for EMPTY_MISSING state - return allEmptyMissingContainers.isEmpty() && - unhealthyContainerStateStatsMap.get( - ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING) - .getOrDefault(CONTAINER_COUNT, 0L) == 0; - }); - - // Now remove keys from container. This data is used to - // identify if container is empty in terms of keys mapped to container. - try (RDBBatchOperation rdbBatchOperation = RDBBatchOperation.newAtomicOperation()) { - reconContainerMetadataManager - .batchStoreContainerKeyCounts(rdbBatchOperation, containerID, 0L); - reconContainerMetadataManager.commitBatchOperation(rdbBatchOperation); - } - - // Since we no longer add EMPTY_MISSING containers to the table, we should - // have zero EMPTY_MISSING containers in the DB but their information will be logged. - LambdaTestUtils.await(25000, 1000, () -> { - List allEmptyMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates. - EMPTY_MISSING, - 0L, Optional.empty(), 1000); - - Map> - unhealthyContainerStateStatsMap = ((ContainerHealthTask) reconScm.getContainerHealthTask()) - .getUnhealthyContainerStateStatsMap(); - - // Return true if the size of the fetched containers is 0 and the log shows 1 for EMPTY_MISSING state - return allEmptyMissingContainers.isEmpty() && - unhealthyContainerStateStatsMap.get( - ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING) - .getOrDefault(CONTAINER_COUNT, 0L) == 1; - }); - - // Now restart the cluster and verify the container is no longer missing. - cluster.restartHddsDatanode(pipeline.getFirstNode(), true); - LambdaTestUtils.await(25000, 1000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManager() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, - 0L, Optional.empty(), 1000); - return (allMissingContainers.isEmpty()); - }); - - IOUtils.closeQuietly(client); - } - - @Test - public void testContainerHealthTaskV2WithSCMSync() throws Exception { - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - - StorageContainerManager scm = cluster.getStorageContainerManager(); - PipelineManager reconPipelineManager = reconScm.getPipelineManager(); - PipelineManager scmPipelineManager = scm.getPipelineManager(); - - // Make sure Recon's pipeline state is initialized. - LambdaTestUtils.await(60000, 5000, - () -> (!reconPipelineManager.getPipelines().isEmpty())); - - ContainerManager scmContainerManager = scm.getContainerManager(); - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); - - // Create a container in SCM - ContainerInfo containerInfo = - scmContainerManager.allocateContainer( - RatisReplicationConfig.getInstance(ONE), "test"); - long containerID = containerInfo.getContainerID(); - - Pipeline pipeline = - scmPipelineManager.getPipeline(containerInfo.getPipelineID()); - XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); - runTestOzoneContainerViaDataNode(containerID, client); - - // Make sure Recon got the container report with new container. - assertEquals(scmContainerManager.getContainers(), - reconContainerManager.getContainers()); - - // Bring down the Datanode that had the container replica. - cluster.shutdownHddsDatanode(pipeline.getFirstNode()); - - // V2 task should detect MISSING container from SCM - LambdaTestUtils.await(120000, 6000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, - 0L, 0L, 1000); - return (allMissingContainers.size() == 1); - }); - - // Restart the Datanode to make sure we remove the missing container. - cluster.restartHddsDatanode(pipeline.getFirstNode(), true); - - LambdaTestUtils.await(120000, 10000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, - 0L, 0L, 1000); - return (allMissingContainers.isEmpty()); - }); - - IOUtils.closeQuietly(client); - } - - /** - * Test that ContainerHealthTaskV2 can query MIS_REPLICATED containers. - * Steps: - * 1. Create a cluster - * 2. Verify the query mechanism for MIS_REPLICATED state works - * - * Note: Creating mis-replication scenarios (placement policy violations) - * is very complex in integration tests as it requires: - * - Multi-rack cluster setup with rack-aware placement policy - * - Or manipulating replica placement to violate policy constraints - * - * This test verifies the detection infrastructure exists. In production, - * mis-replication occurs when: - * 1. Replicas don't satisfy rack/node placement requirements - * 2. Replicas are on nodes that violate upgrade domain constraints - * 3. EC containers have incorrect replica placement - * - * Note: Tests for UNDER_REPLICATED and OVER_REPLICATED are in - * TestReconTasksV2MultiNode as they require multi-node clusters. - */ - @Test - public void testContainerHealthTaskV2MisReplicated() throws Exception { - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); - - // Verify the query mechanism for MIS_REPLICATED state works - List misReplicatedContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.MIS_REPLICATED, - 0L, 0L, 1000); - - // Should be empty in normal single-node cluster operation - // (mis-replication requires multi-node/rack setup with placement policy) - assertEquals(0, misReplicatedContainers.size()); - - // Note: Creating actual mis-replication scenarios requires: - // 1. Multi-rack cluster configuration - // 2. Rack-aware placement policy configuration - // 3. Manually placing replicas to violate placement rules - // - // Example scenario that would create MIS_REPLICATED: - // - 3-replica container with rack-aware policy (each replica on different rack) - // - All 3 replicas end up on same rack (violates placement policy) - // - ContainerHealthTaskV2 would detect this as MIS_REPLICATED - // - // The detection logic in SCM's ReplicationManager handles this, - // and our Recon's RM logic implementation reuses that logic. - } -} diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java deleted file mode 100644 index 05bf4f158d8e..000000000000 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java +++ /dev/null @@ -1,110 +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.ozone.recon.schema; - -import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.name; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import java.sql.Connection; -import java.sql.SQLException; -import javax.sql.DataSource; -import org.jooq.DSLContext; -import org.jooq.impl.DSL; -import org.jooq.impl.SQLDataType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Class used to create tables that are required for tracking containers. - */ -@Singleton -public class ContainerSchemaDefinition implements ReconSchemaDefinition { - private static final Logger LOG = LoggerFactory.getLogger(ContainerSchemaDefinition.class); - - public static final String UNHEALTHY_CONTAINERS_TABLE_NAME = - "UNHEALTHY_CONTAINERS"; - - private static final String CONTAINER_ID = "container_id"; - private static final String CONTAINER_STATE = "container_state"; - private final DataSource dataSource; - private DSLContext dslContext; - - @Inject - ContainerSchemaDefinition(DataSource dataSource) { - this.dataSource = dataSource; - } - - @Override - public void initializeSchema() throws SQLException { - Connection conn = dataSource.getConnection(); - dslContext = DSL.using(conn); - if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { - LOG.info("UNHEALTHY_CONTAINERS is missing creating new one."); - createUnhealthyContainersTable(); - } - } - - /** - * Create the Missing Containers table. - */ - private void createUnhealthyContainersTable() { - dslContext.createTableIfNotExists(UNHEALTHY_CONTAINERS_TABLE_NAME) - .column(CONTAINER_ID, SQLDataType.BIGINT.nullable(false)) - .column(CONTAINER_STATE, SQLDataType.VARCHAR(16).nullable(false)) - .column("in_state_since", SQLDataType.BIGINT.nullable(false)) - .column("expected_replica_count", SQLDataType.INTEGER.nullable(false)) - .column("actual_replica_count", SQLDataType.INTEGER.nullable(false)) - .column("replica_delta", SQLDataType.INTEGER.nullable(false)) - .column("reason", SQLDataType.VARCHAR(500).nullable(true)) - .constraint(DSL.constraint("pk_container_id") - .primaryKey(CONTAINER_ID, CONTAINER_STATE)) - .constraint(DSL.constraint(UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") - .check(field(name(CONTAINER_STATE)) - .in(UnHealthyContainerStates.values()))) - .execute(); - dslContext.createIndex("idx_container_state") - .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME), DSL.field(name(CONTAINER_STATE))) - .execute(); - } - - public DSLContext getDSLContext() { - return dslContext; - } - - public DataSource getDataSource() { - return dataSource; - } - - /** - * ENUM describing the allowed container states which can be stored in the - * unhealthy containers table. - */ - public enum UnHealthyContainerStates { - MISSING, - EMPTY_MISSING, - UNDER_REPLICATED, - OVER_REPLICATED, - MIS_REPLICATED, - ALL_REPLICAS_BAD, - NEGATIVE_SIZE, // Added new state to track containers with negative sizes - REPLICA_MISMATCH - } -} diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java index 3d9c5cc2b651..148295210843 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java @@ -104,6 +104,8 @@ public enum UnHealthyContainerStates { UNDER_REPLICATED, // From SCM ReplicationManager OVER_REPLICATED, // From SCM ReplicationManager MIS_REPLICATED, // From SCM ReplicationManager - REPLICA_MISMATCH // Computed locally by Recon (SCM doesn't track checksums) + REPLICA_MISMATCH, // Computed locally by Recon (SCM doesn't track checksums) + EMPTY_MISSING, // Kept for API compatibility with legacy clients + NEGATIVE_SIZE // Kept for API compatibility with legacy clients } } diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java index ae73f909221f..698a6ec8ee7b 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java @@ -32,7 +32,6 @@ protected void configure() { Multibinder schemaBinder = Multibinder.newSetBinder(binder(), ReconSchemaDefinition.class); schemaBinder.addBinding().to(UtilizationSchemaDefinition.class); - schemaBinder.addBinding().to(ContainerSchemaDefinition.class); schemaBinder.addBinding().to(ContainerSchemaDefinitionV2.class); schemaBinder.addBinding().to(ReconTaskSchemaDefinition.class); schemaBinder.addBinding().to(StatsSchemaDefinition.class); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java index 70e3a6b3d18e..45e4d0845606 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java @@ -42,7 +42,6 @@ import org.apache.hadoop.ozone.om.protocolPB.OmTransportFactory; import org.apache.hadoop.ozone.om.protocolPB.OzoneManagerProtocolClientSideTranslatorPB; import org.apache.hadoop.ozone.recon.heatmap.HeatMapServiceImpl; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.DataSourceConfiguration; import org.apache.hadoop.ozone.recon.persistence.JooqPersistenceModule; @@ -79,7 +78,6 @@ import org.apache.ozone.recon.schema.generated.tables.daos.FileCountBySizeDao; import org.apache.ozone.recon.schema.generated.tables.daos.GlobalStatsDao; 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.daos.UnhealthyContainersV2Dao; import org.apache.ratis.protocol.ClientId; import org.jooq.Configuration; @@ -104,7 +102,6 @@ protected void configure() { .to(ReconOmMetadataManagerImpl.class); bind(OMMetadataManager.class).to(ReconOmMetadataManagerImpl.class); - bind(ContainerHealthSchemaManager.class).in(Singleton.class); bind(ContainerHealthSchemaManagerV2.class).in(Singleton.class); bind(ReconContainerMetadataManager.class) .to(ReconContainerMetadataManagerImpl.class).in(Singleton.class); @@ -165,7 +162,6 @@ public static class ReconDaoBindingModule extends AbstractModule { ImmutableList.of( FileCountBySizeDao.class, ReconTaskStatusDao.class, - UnhealthyContainersDao.class, UnhealthyContainersV2Dao.class, GlobalStatsDao.class, ClusterGrowthDailyDao.class, diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java index 05037e3166f9..bcc0d68eb080 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java @@ -28,7 +28,6 @@ import java.io.IOException; import java.util.List; -import java.util.Optional; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -45,15 +44,14 @@ import org.apache.hadoop.ozone.recon.api.types.ClusterStateResponse; import org.apache.hadoop.ozone.recon.api.types.ClusterStorageReport; import org.apache.hadoop.ozone.recon.api.types.ContainerStateCounts; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconNodeManager; import org.apache.hadoop.ozone.recon.scm.ReconPipelineManager; import org.apache.hadoop.ozone.recon.spi.ReconGlobalStatsManager; import org.apache.hadoop.ozone.recon.tasks.GlobalStatsValue; import org.apache.hadoop.ozone.recon.tasks.OmTableInsightTask; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,13 +71,13 @@ public class ClusterStateEndpoint { private final ReconContainerManager containerManager; private final ReconGlobalStatsManager reconGlobalStatsManager; private final OzoneConfiguration ozoneConfiguration; - private final ContainerHealthSchemaManager containerHealthSchemaManager; + private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; @Inject ClusterStateEndpoint(OzoneStorageContainerManager reconSCM, ReconGlobalStatsManager reconGlobalStatsManager, - ContainerHealthSchemaManager - containerHealthSchemaManager, + ContainerHealthSchemaManagerV2 + containerHealthSchemaManagerV2, OzoneConfiguration ozoneConfiguration) { this.nodeManager = (ReconNodeManager) reconSCM.getScmNodeManager(); @@ -87,7 +85,7 @@ public class ClusterStateEndpoint { this.containerManager = (ReconContainerManager) reconSCM.getContainerManager(); this.reconGlobalStatsManager = reconGlobalStatsManager; - this.containerHealthSchemaManager = containerHealthSchemaManager; + this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; this.ozoneConfiguration = ozoneConfiguration; } @@ -100,10 +98,11 @@ public Response getClusterState() { ContainerStateCounts containerStateCounts = new ContainerStateCounts(); int pipelines = this.pipelineManager.getPipelines().size(); - List missingContainers = containerHealthSchemaManager + List missingContainers = + containerHealthSchemaManagerV2 .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, - 0L, Optional.empty(), MISSING_CONTAINER_COUNT_LIMIT); + ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, + 0L, 0L, MISSING_CONTAINER_COUNT_LIMIT); containerStateCounts.setMissingContainerCount( missingContainers.size() == MISSING_CONTAINER_COUNT_LIMIT ? diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 223ec860a7e5..61b7533c7f48 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -35,7 +35,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; import javax.inject.Inject; @@ -49,9 +48,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; -import org.apache.hadoop.hdds.recon.ReconConfigKeys; import org.apache.hadoop.hdds.scm.container.ContainerID; import org.apache.hadoop.hdds.scm.container.ContainerInfo; import org.apache.hadoop.hdds.scm.pipeline.Pipeline; @@ -77,7 +74,6 @@ import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; @@ -85,9 +81,7 @@ import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager; import org.apache.hadoop.ozone.util.SeekableIterator; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -105,11 +99,9 @@ public class ContainerEndpoint { private final ReconContainerManager containerManager; private final PipelineManager pipelineManager; - private final ContainerHealthSchemaManager containerHealthSchemaManager; private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; private final ReconNamespaceSummaryManager reconNamespaceSummaryManager; private final OzoneStorageContainerManager reconSCM; - private final OzoneConfiguration ozoneConfiguration; private static final Logger LOG = LoggerFactory.getLogger(ContainerEndpoint.class); private BucketLayout layout = BucketLayout.DEFAULT; @@ -148,18 +140,14 @@ public static DataFilter fromValue(String value) { @Inject public ContainerEndpoint(OzoneStorageContainerManager reconSCM, - ContainerHealthSchemaManager containerHealthSchemaManager, ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2, - OzoneConfiguration ozoneConfiguration, ReconNamespaceSummaryManager reconNamespaceSummaryManager, ReconContainerMetadataManager reconContainerMetadataManager, ReconOMMetadataManager omMetadataManager) { this.containerManager = (ReconContainerManager) reconSCM.getContainerManager(); this.pipelineManager = reconSCM.getPipelineManager(); - this.containerHealthSchemaManager = containerHealthSchemaManager; this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; - this.ozoneConfiguration = ozoneConfiguration; this.reconNamespaceSummaryManager = reconNamespaceSummaryManager; this.reconSCM = reconSCM; this.reconContainerMetadataManager = reconContainerMetadataManager; @@ -349,8 +337,9 @@ public Response getMissingContainers( int limit ) { List missingContainers = new ArrayList<>(); - containerHealthSchemaManager.getUnhealthyContainers( - UnHealthyContainerStates.MISSING, 0L, Optional.empty(), limit) + containerHealthSchemaManagerV2.getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, + 0L, 0L, limit) .forEach(container -> { long containerID = container.getContainerId(); try { @@ -402,84 +391,7 @@ public Response getUnhealthyContainers( @QueryParam(RECON_QUERY_MAX_CONTAINER_ID) long maxContainerId, @DefaultValue(PREV_CONTAINER_ID_DEFAULT_VALUE) @QueryParam(RECON_QUERY_MIN_CONTAINER_ID) long minContainerId) { - - // Check feature flag to determine which implementation to use - boolean useV2 = ozoneConfiguration.getBoolean( - ReconConfigKeys.OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT, - ReconConfigKeys.OZONE_RECON_CONTAINER_HEALTH_USE_SCM_REPORT_DEFAULT); - - if (useV2) { - return getUnhealthyContainersV2(state, limit, maxContainerId, minContainerId); - } else { - return getUnhealthyContainersV1(state, limit, maxContainerId, minContainerId); - } - } - - /** - * V1 implementation - reads from UNHEALTHY_CONTAINERS table. - */ - private Response getUnhealthyContainersV1( - String state, - int limit, - long maxContainerId, - long minContainerId) { - Optional maxContainerIdOpt = maxContainerId > 0 ? Optional.of(maxContainerId) : Optional.empty(); - List unhealthyMeta = new ArrayList<>(); - List summary; - try { - UnHealthyContainerStates internalState = null; - - if (state != null) { - // If an invalid state is passed in, this will throw - // illegalArgumentException and fail the request - internalState = UnHealthyContainerStates.valueOf(state); - } - - summary = containerHealthSchemaManager.getUnhealthyContainersSummary(); - List containers = containerHealthSchemaManager - .getUnhealthyContainers(internalState, minContainerId, maxContainerIdOpt, limit); - - // Filtering out EMPTY_MISSING and NEGATIVE_SIZE containers from the response. - // These container states are not being inserted into the database as they represent - // edge cases that are not critical to track as unhealthy containers. - List filteredContainers = containers.stream() - .filter(container -> !container.getContainerState() - .equals(UnHealthyContainerStates.EMPTY_MISSING.toString()) - && !container.getContainerState() - .equals(UnHealthyContainerStates.NEGATIVE_SIZE.toString())) - .collect(Collectors.toList()); - - for (UnhealthyContainers c : filteredContainers) { - long containerID = c.getContainerId(); - ContainerInfo containerInfo = - containerManager.getContainer(ContainerID.valueOf(containerID)); - long keyCount = containerInfo.getNumberOfKeys(); - UUID pipelineID = containerInfo.getPipelineID().getId(); - List datanodes = - containerManager.getLatestContainerHistory(containerID, - containerInfo.getReplicationConfig().getRequiredNodes()); - unhealthyMeta.add(new UnhealthyContainerMetadata( - c, datanodes, pipelineID, keyCount)); - } - } catch (IOException ex) { - throw new WebApplicationException(ex, - Response.Status.INTERNAL_SERVER_ERROR); - } catch (IllegalArgumentException e) { - throw new WebApplicationException(e, Response.Status.BAD_REQUEST); - } - - UnhealthyContainersResponse response = - new UnhealthyContainersResponse(unhealthyMeta); - if (!unhealthyMeta.isEmpty()) { - response.setFirstKey(unhealthyMeta.stream().map(UnhealthyContainerMetadata::getContainerID) - .min(Long::compareTo).orElse(0L)); - response.setLastKey(unhealthyMeta.stream().map(UnhealthyContainerMetadata::getContainerID) - .max(Long::compareTo).orElse(0L)); - } - for (UnhealthyContainersSummary s : summary) { - response.setSummaryCount(s.getContainerState(), s.getCount()); - } - return Response.ok(response).build(); + return getUnhealthyContainersV2(state, limit, maxContainerId, minContainerId); } /** @@ -523,18 +435,17 @@ private Response getUnhealthyContainersV2( containerManager.getLatestContainerHistory(containerID, containerInfo.getReplicationConfig().getRequiredNodes()); - // Create UnhealthyContainers POJO from V2 record for response - UnhealthyContainers v1Container = new UnhealthyContainers(); - v1Container.setContainerId(c.getContainerId()); - v1Container.setContainerState(c.getContainerState()); - v1Container.setInStateSince(c.getInStateSince()); - v1Container.setExpectedReplicaCount(c.getExpectedReplicaCount()); - v1Container.setActualReplicaCount(c.getActualReplicaCount()); - v1Container.setReplicaDelta(c.getReplicaDelta()); - v1Container.setReason(c.getReason()); - unhealthyMeta.add(new UnhealthyContainerMetadata( - v1Container, datanodes, pipelineID, keyCount)); + c.getContainerId(), + c.getContainerState(), + c.getInStateSince(), + c.getExpectedReplicaCount(), + c.getActualReplicaCount(), + c.getReplicaDelta(), + c.getReason(), + datanodes, + pipelineID, + keyCount)); } } catch (IOException ex) { throw new WebApplicationException(ex, diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java index bc6bb57b4a69..7418863fab10 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java @@ -23,7 +23,6 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; /** * Metadata object that represents an unhealthy Container. @@ -61,15 +60,18 @@ public class UnhealthyContainerMetadata { @XmlElement(name = "replicas") private List replicas; - public UnhealthyContainerMetadata(UnhealthyContainers rec, - List replicas, UUID pipelineID, long keyCount) { - this.containerID = rec.getContainerId(); - this.containerState = rec.getContainerState(); - this.unhealthySince = rec.getInStateSince(); - this.actualReplicaCount = rec.getActualReplicaCount(); - this.expectedReplicaCount = rec.getExpectedReplicaCount(); - this.replicaDeltaCount = rec.getReplicaDelta(); - this.reason = rec.getReason(); + @SuppressWarnings("checkstyle:ParameterNumber") + public UnhealthyContainerMetadata(long containerID, String containerState, + long unhealthySince, long expectedReplicaCount, long actualReplicaCount, + long replicaDeltaCount, String reason, List replicas, + UUID pipelineID, long keyCount) { + this.containerID = containerID; + this.containerState = containerState; + this.unhealthySince = unhealthySince; + this.actualReplicaCount = actualReplicaCount; + this.expectedReplicaCount = expectedReplicaCount; + this.replicaDeltaCount = replicaDeltaCount; + this.reason = reason; this.replicas = replicas; this.pipelineID = pipelineID; this.keys = keyCount; diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java index 350f9e8ceda1..eec52478d60f 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collection; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; /** * Class that represents the API Response structure of Unhealthy Containers. diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java deleted file mode 100644 index a6b6f3a8c30f..000000000000 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java +++ /dev/null @@ -1,768 +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.ozone.recon.ReconConstants.CONTAINER_COUNT; -import static org.apache.hadoop.ozone.recon.ReconConstants.DEFAULT_FETCH_COUNT; -import static org.apache.hadoop.ozone.recon.ReconConstants.TOTAL_KEYS; -import static org.apache.hadoop.ozone.recon.ReconConstants.TOTAL_USED_BYTES; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING; - -import com.google.common.annotations.VisibleForTesting; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Consumer; -import java.util.stream.Collectors; -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.ContainerID; -import org.apache.hadoop.hdds.scm.container.ContainerInfo; -import org.apache.hadoop.hdds.scm.container.ContainerManager; -import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; -import org.apache.hadoop.hdds.scm.container.ContainerReplica; -import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline; -import org.apache.hadoop.ozone.common.statemachine.InvalidStateTransitionException; -import org.apache.hadoop.ozone.recon.metrics.ContainerHealthMetrics; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; -import org.apache.hadoop.ozone.recon.scm.ReconScmTask; -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.hadoop.util.Time; -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.jooq.Cursor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Class that scans the list of containers and keeps track of containers with - * no replicas in a SQL table. - */ -public class ContainerHealthTask extends ReconScmTask { - - private static final Logger LOG = - LoggerFactory.getLogger(ContainerHealthTask.class); - public static final int FETCH_COUNT = Integer.parseInt(DEFAULT_FETCH_COUNT); - - private final ReadWriteLock lock = new ReentrantReadWriteLock(true); - - private final StorageContainerServiceProvider scmClient; - private final ContainerManager containerManager; - private final ContainerHealthSchemaManager containerHealthSchemaManager; - private final ReconContainerMetadataManager reconContainerMetadataManager; - private final PlacementPolicy placementPolicy; - private final long interval; - private Map> - unhealthyContainerStateStatsMapForTesting; - - private final Set processedContainers = new HashSet<>(); - - private final OzoneConfiguration conf; - - private final ReconTaskStatusUpdater taskStatusUpdater; - private final ContainerHealthMetrics containerHealthMetrics; - - @SuppressWarnings("checkstyle:ParameterNumber") - public ContainerHealthTask( - ContainerManager containerManager, - StorageContainerServiceProvider scmClient, - ContainerHealthSchemaManager containerHealthSchemaManager, - PlacementPolicy placementPolicy, - ReconTaskConfig reconTaskConfig, - ReconContainerMetadataManager reconContainerMetadataManager, - OzoneConfiguration conf, ReconTaskStatusUpdaterManager taskStatusUpdaterManager) { - super(taskStatusUpdaterManager); - this.scmClient = scmClient; - this.containerHealthSchemaManager = containerHealthSchemaManager; - this.reconContainerMetadataManager = reconContainerMetadataManager; - this.placementPolicy = placementPolicy; - this.containerManager = containerManager; - this.conf = conf; - interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); - this.taskStatusUpdater = getTaskStatusUpdater(); - this.containerHealthMetrics = ContainerHealthMetrics.create(); - } - - @Override - public void run() { - try { - while (canRun()) { - initializeAndRunTask(); - Thread.sleep(interval); - } - } catch (Throwable t) { - LOG.error("Exception in Container Health task thread.", t); - if (t instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - taskStatusUpdater.setLastTaskRunStatus(-1); - taskStatusUpdater.recordRunCompletion(); - } - } - - @Override - protected void runTask() throws Exception { - lock.writeLock().lock(); - // Map contains all UNHEALTHY STATES as keys and value is another map - // with 3 keys (CONTAINER_COUNT, TOTAL_KEYS, TOTAL_USED_BYTES) and value - // is count for each of these 3 stats. - // E.g. >, >, - // >, - // >, >, - // > - Map> - unhealthyContainerStateStatsMap; - try { - unhealthyContainerStateStatsMap = new HashMap<>(Collections.emptyMap()); - initializeUnhealthyContainerStateStatsMap( - unhealthyContainerStateStatsMap); - long start = Time.monotonicNow(); - long currentTime = System.currentTimeMillis(); - long existingCount = processExistingDBRecords(currentTime, - unhealthyContainerStateStatsMap); - LOG.debug("Container Health task thread took {} milliseconds to" + - " process {} existing database records.", - Time.monotonicNow() - start, existingCount); - - start = Time.monotonicNow(); - checkAndProcessContainers(unhealthyContainerStateStatsMap, currentTime); - LOG.debug("Container Health Task thread took {} milliseconds to process containers", - Time.monotonicNow() - start); - taskStatusUpdater.setLastTaskRunStatus(0); - processedContainers.clear(); - logUnhealthyContainerStats(unhealthyContainerStateStatsMap); - } finally { - lock.writeLock().unlock(); - } - } - - private void checkAndProcessContainers( - Map> - unhealthyContainerStateStatsMap, long currentTime) { - ContainerID startID = ContainerID.valueOf(1); - List containers = containerManager.getContainers(startID, - FETCH_COUNT); - long start; - long iterationCount = 0; - while (!containers.isEmpty()) { - start = Time.monotonicNow(); - containers.stream() - .filter(c -> !processedContainers.contains(c)) - .forEach(c -> processContainer(c, currentTime, - unhealthyContainerStateStatsMap)); - LOG.debug("Container Health task thread took {} milliseconds for" + - " processing {} containers.", Time.monotonicNow() - start, - containers.size()); - if (containers.size() >= FETCH_COUNT) { - startID = ContainerID.valueOf( - containers.get(containers.size() - 1).getContainerID() + 1); - containers = containerManager.getContainers(startID, FETCH_COUNT); - } else { - containers.clear(); - } - iterationCount++; - } - LOG.info( - "Container Health task thread took {} iterations to fetch all " + - "containers using batched approach with batch size of {}", - iterationCount, FETCH_COUNT); - } - - private void logUnhealthyContainerStats( - Map> unhealthyContainerStateStatsMap) { - - unhealthyContainerStateStatsMapForTesting = new HashMap<>(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 in unhealthy container statistics but do not add it to the table. - unhealthyContainerStateStatsMap.forEach((unhealthyContainerState, containerStateStatsMap) -> { - // Reset metrics to zero if the map is empty for MISSING or UNDER_REPLICATED - Optional.of(containerStateStatsMap) - .filter(Map::isEmpty) - .ifPresent(emptyMap -> resetContainerHealthMetrics(unhealthyContainerState)); - - // Process and log the container state statistics - String logMessage = containerStateStatsMap.entrySet().stream() - .peek(entry -> updateContainerHealthMetrics(unhealthyContainerState, entry)) - .map(entry -> entry.getKey() + " -> " + entry.getValue()) - .collect(Collectors.joining(" , ", unhealthyContainerState + " **Container State Stats:** \n\t", "")); - - if (!containerStateStatsMap.isEmpty()) { - LOG.info(logMessage); - } - }); - } - - /** - * Helper method to update container health metrics using functional approach. - */ - private void updateContainerHealthMetrics(UnHealthyContainerStates state, Map.Entry entry) { - Map> metricUpdaters = new HashMap<>(); - metricUpdaters.put(UnHealthyContainerStates.MISSING, containerHealthMetrics::setMissingContainerCount); - metricUpdaters.put(UnHealthyContainerStates.UNDER_REPLICATED, - containerHealthMetrics::setUnderReplicatedContainerCount); - - Optional.ofNullable(metricUpdaters.get(state)) - .filter(updater -> CONTAINER_COUNT.equals(entry.getKey())) - .ifPresent(updater -> updater.accept(entry.getValue())); - } - - /** - * Resets container health metrics to zero using a functional approach. - */ - private void resetContainerHealthMetrics(UnHealthyContainerStates state) { - Map> resetActions = new HashMap<>(); - resetActions.put(UnHealthyContainerStates.MISSING, containerHealthMetrics::setMissingContainerCount); - resetActions.put(UnHealthyContainerStates.UNDER_REPLICATED, - containerHealthMetrics::setUnderReplicatedContainerCount); - - Optional.ofNullable(resetActions.get(state)).ifPresent(action -> action.accept(0L)); - } - - 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 ContainerHealthStatus setCurrentContainer(long recordId) - throws ContainerNotFoundException { - ContainerInfo container = - containerManager.getContainer(ContainerID.valueOf(recordId)); - Set replicas = - containerManager.getContainerReplicas(container.containerID()); - return new ContainerHealthStatus(container, replicas, placementPolicy, - reconContainerMetadataManager, conf); - } - - private void completeProcessingContainer( - ContainerHealthStatus container, - Set existingRecords, - long currentTime, - Map> - unhealthyContainerStateCountMap) { - containerHealthSchemaManager.insertUnhealthyContainerRecords( - ContainerHealthRecords.generateUnhealthyRecords( - container, existingRecords, currentTime, - unhealthyContainerStateCountMap)); - processedContainers.add(container.getContainer()); - } - - /** - * This method reads all existing records in the UnhealthyContainers table. - * The container records are read sorted by Container ID, as there can be - * more than 1 record per container. - * Each record is checked to see if it should be retained or deleted, and if - * any of the replica counts have changed the record is updated. Each record - * for a container is collected into a Set and when the next container id - * changes, indicating the end of the records for the current container, - * completeProcessingContainer is called. This will check to see if any - * additional records need to be added to the database. - * - * If a container is identified as missing, empty-missing, under-replicated, - * over-replicated or mis-replicated, the method checks with SCM to determine - * if it has been deleted, using {@code containerDeletedInSCM}. If the container is - * deleted in SCM, the corresponding record is removed from Recon. - * - * @param currentTime Timestamp to place on all records generated by this run - * @param unhealthyContainerStateCountMap - * @return Count of records processed - */ - private long processExistingDBRecords(long currentTime, - Map> - unhealthyContainerStateCountMap) { - long recordCount = 0; - try (Cursor cursor = - containerHealthSchemaManager.getAllUnhealthyRecordsCursor()) { - ContainerHealthStatus currentContainer = null; - Set existingRecords = new HashSet<>(); - while (cursor.hasNext()) { - recordCount++; - UnhealthyContainersRecord rec = cursor.fetchNext(); - try { - // Set the current container if it's not already set - if (currentContainer == null) { - currentContainer = setCurrentContainer(rec.getContainerId()); - } - // If the container ID has changed, finish processing the previous one - if (currentContainer.getContainerID() != rec.getContainerId()) { - completeProcessingContainer( - currentContainer, existingRecords, currentTime, - unhealthyContainerStateCountMap); - existingRecords.clear(); - currentContainer = setCurrentContainer(rec.getContainerId()); - } - - // Unhealthy Containers such as MISSING, UNDER_REPLICATED, - // OVER_REPLICATED, MIS_REPLICATED can have their unhealthy states changed or retained. - if (!ContainerHealthRecords.retainOrUpdateRecord(currentContainer, rec)) { - rec.delete(); - LOG.info("DELETED existing unhealthy container record...for Container: {}", - currentContainer.getContainerID()); - } - - // If the container is marked as MISSING and it's deleted in SCM, remove the record - if (currentContainer.isMissing() && containerDeletedInSCM(currentContainer.getContainer())) { - rec.delete(); - LOG.info("DELETED existing MISSING unhealthy container record...as container deleted " + - "in SCM as well: {}", currentContainer.getContainerID()); - } - - existingRecords.add(rec.getContainerState()); - // If the record was changed, update it - if (rec.changed()) { - rec.update(); - } - } catch (ContainerNotFoundException cnf) { - // If the container is not found, delete the record and reset currentContainer - rec.delete(); - currentContainer = null; - } - } - // Remember to finish processing the last container - if (currentContainer != null) { - completeProcessingContainer( - currentContainer, existingRecords, currentTime, - unhealthyContainerStateCountMap); - } - } - return recordCount; - } - - private void processContainer(ContainerInfo container, long currentTime, - Map> - unhealthyContainerStateStatsMap) { - try { - Set containerReplicas = - containerManager.getContainerReplicas(container.containerID()); - ContainerHealthStatus h = new ContainerHealthStatus(container, - containerReplicas, placementPolicy, - reconContainerMetadataManager, conf); - - if ((h.isHealthilyReplicated() && !h.areChecksumsMismatched()) || h.isDeleted()) { - return; - } - // For containers deleted in SCM, we sync the container state here. - if (h.isMissing() && containerDeletedInSCM(container)) { - return; - } - containerHealthSchemaManager.insertUnhealthyContainerRecords( - ContainerHealthRecords.generateUnhealthyRecords(h, currentTime, - unhealthyContainerStateStatsMap)); - } catch (ContainerNotFoundException e) { - LOG.error("Container not found while processing container in Container " + - "Health task", e); - } - } - - /** - * Ensures the container's state in Recon is updated to match its state in SCM. - * - * If SCM reports the container as DELETED, this method attempts to transition - * the container's state in Recon from CLOSED to DELETING, or from DELETING to - * DELETED, based on the current state in Recon. It logs each transition attempt - * and handles any exceptions that may occur. - * - * @param containerInfo the container whose state is being checked and potentially updated. - * @return {@code true} if the container was found to be DELETED in SCM and the - * state transition was attempted in Recon; {@code false} otherwise. - */ - private boolean containerDeletedInSCM(ContainerInfo containerInfo) { - try { - ContainerWithPipeline containerWithPipeline = - scmClient.getContainerWithPipeline(containerInfo.getContainerID()); - if (containerWithPipeline.getContainerInfo().getState() == - HddsProtos.LifeCycleState.DELETED) { - if (containerInfo.getState() == HddsProtos.LifeCycleState.CLOSED) { - containerManager.updateContainerState(containerInfo.containerID(), - HddsProtos.LifeCycleEvent.DELETE); - LOG.debug("Successfully changed container {} state from CLOSED to DELETING.", - containerInfo.containerID()); - } - if (containerInfo.getState() == HddsProtos.LifeCycleState.DELETING && - containerManager.getContainerReplicas(containerInfo.containerID()).isEmpty() - ) { - containerManager.updateContainerState(containerInfo.containerID(), - HddsProtos.LifeCycleEvent.CLEANUP); - LOG.info("Successfully Deleted container {} from Recon.", containerInfo.containerID()); - } - return true; - } - } catch (InvalidStateTransitionException e) { - LOG.error("Failed to transition Container state while processing " + - "container in Container Health task", e); - } catch (IOException e) { - LOG.error("Got exception while processing container in" + - " Container Health task", e); - } - return false; - } - - /** - * This method is used to handle containers with negative sizes. It logs an - * error message. - * @param containerHealthStatus - * @param currentTime - * @param unhealthyContainerStateStatsMap - */ - private static void handleNegativeSizedContainers( - ContainerHealthStatus containerHealthStatus, long currentTime, - Map> - unhealthyContainerStateStatsMap) { - // NEGATIVE_SIZE containers are also not inserted into the database. - // This condition usually arises due to corrupted or invalid metadata, where - // the container's size is inaccurately recorded as negative. Since this does not - // represent a typical unhealthy scenario and may not have any meaningful - // impact on system health, such containers are logged for investigation but - // excluded from the UNHEALTHY_CONTAINERS table to maintain data integrity. - ContainerInfo container = containerHealthStatus.getContainer(); - LOG.error("Container {} has negative size.", container.getContainerID()); - populateContainerStats(containerHealthStatus, UnHealthyContainerStates.NEGATIVE_SIZE, - unhealthyContainerStateStatsMap); - } - - /** - * This method is used to handle containers that are empty and missing. It logs - * a debug message. - * @param containerHealthStatus - * @param currentTime - * @param unhealthyContainerStateStatsMap - */ - private static void handleEmptyMissingContainers( - ContainerHealthStatus containerHealthStatus, long currentTime, - Map> - unhealthyContainerStateStatsMap) { - // EMPTY_MISSING containers are not inserted into the database. - // These containers typically represent those that were never written to - // or remain in an incomplete state. Tracking such containers as unhealthy - // would not provide valuable insights since they don't pose a risk or issue - // to the system. Instead, they are logged for awareness, but not stored in - // the UNHEALTHY_CONTAINERS table to avoid unnecessary entries. - ContainerInfo container = containerHealthStatus.getContainer(); - LOG.debug("Empty container {} is missing. It will be logged in the " + - "unhealthy container statistics, but no record will be created in the " + - "UNHEALTHY_CONTAINERS table.", container.getContainerID()); - populateContainerStats(containerHealthStatus, EMPTY_MISSING, - unhealthyContainerStateStatsMap); - } - - /** - * Helper methods to generate and update the required database records for - * unhealthy containers. - */ - public static class ContainerHealthRecords { - - /** - * Given an existing database record and a ContainerHealthStatus object, - * this method will check if the database record should be retained or not. - * Eg, if a missing record exists, and the ContainerHealthStatus indicates - * the container is still missing, the method will return true, indicating - * the record should be retained. If the container is no longer missing, - * it will return false, indicating the record should be deleted. - * If the record is to be retained, the fields in the record for actual - * replica count, delta and reason will be updated if their counts have - * changed. - * - * @param container ContainerHealthStatus representing the - * health state of the container. - * @param rec Existing database record from the - * UnhealthyContainers table. - * @return returns true or false if need to retain or update the unhealthy - * container record - */ - public static boolean retainOrUpdateRecord( - ContainerHealthStatus container, UnhealthyContainersRecord rec) { - boolean returnValue; - switch (UnHealthyContainerStates.valueOf(rec.getContainerState())) { - case MISSING: - returnValue = container.isMissing() && !container.isEmpty(); - break; - case MIS_REPLICATED: - returnValue = keepMisReplicatedRecord(container, rec); - break; - case UNDER_REPLICATED: - returnValue = keepUnderReplicatedRecord(container, rec); - break; - case OVER_REPLICATED: - returnValue = keepOverReplicatedRecord(container, rec); - break; - case REPLICA_MISMATCH: - returnValue = keepReplicaMismatchRecord(container, rec); - break; - default: - returnValue = false; - } - return returnValue; - } - - public static List generateUnhealthyRecords( - ContainerHealthStatus container, long time, - Map> - unhealthyContainerStateStatsMap) { - return generateUnhealthyRecords(container, new HashSet<>(), time, - unhealthyContainerStateStatsMap); - } - - /** - * Check the status of the container and generate any database records that - * need to be recorded. This method also considers the records seen by the - * method retainOrUpdateRecord. If a record has been seen by that method - * then it will not be emitted here. Therefore this method returns only the - * missing records which have not been seen already. - * @return List of UnhealthyContainer records to be stored in the DB - */ - public static List generateUnhealthyRecords( - ContainerHealthStatus container, Set recordForStateExists, - long time, - Map> - unhealthyContainerStateStatsMap) { - List records = new ArrayList<>(); - if ((container.isHealthilyReplicated() && !container.areChecksumsMismatched()) || container.isDeleted()) { - return records; - } - - if (container.isMissing()) { - boolean shouldAddRecord = !recordForStateExists.contains(UnHealthyContainerStates.MISSING.toString()); - if (!container.isEmpty()) { - LOG.info("Non-empty container {} is missing. It has {} " + - "keys and {} bytes used according to SCM metadata. " + - "Please visit Recon's missing container page for a list of " + - "keys (and their metadata) mapped to this container.", - container.getContainerID(), container.getNumKeys(), - container.getContainer().getUsedBytes()); - - if (shouldAddRecord) { - records.add(recordForState(container, UnHealthyContainerStates.MISSING, time)); - } - populateContainerStats(container, UnHealthyContainerStates.MISSING, unhealthyContainerStateStatsMap); - } else { - handleEmptyMissingContainers(container, time, unhealthyContainerStateStatsMap); - } - // A container cannot have any other records if it is missing, so return - return records; - } - - // For Negative sized containers we only log but not insert into DB - if (container.getContainer().getUsedBytes() < 0) { - handleNegativeSizedContainers(container, time, - unhealthyContainerStateStatsMap); - } - - if (container.isUnderReplicated()) { - boolean shouldAddRecord = !recordForStateExists.contains(UnHealthyContainerStates.UNDER_REPLICATED.toString()); - if (shouldAddRecord) { - records.add(recordForState(container, UnHealthyContainerStates.UNDER_REPLICATED, time)); - } - populateContainerStats(container, UnHealthyContainerStates.UNDER_REPLICATED, unhealthyContainerStateStatsMap); - } - - if (container.isOverReplicated()) { - boolean shouldAddRecord = !recordForStateExists.contains(UnHealthyContainerStates.OVER_REPLICATED.toString()); - if (shouldAddRecord) { - records.add(recordForState(container, UnHealthyContainerStates.OVER_REPLICATED, time)); - } - populateContainerStats(container, UnHealthyContainerStates.OVER_REPLICATED, unhealthyContainerStateStatsMap); - } - - if (container.areChecksumsMismatched() - && !recordForStateExists.contains( - UnHealthyContainerStates.REPLICA_MISMATCH.toString())) { - records.add(recordForState( - container, UnHealthyContainerStates.REPLICA_MISMATCH, time)); - populateContainerStats(container, - UnHealthyContainerStates.REPLICA_MISMATCH, - unhealthyContainerStateStatsMap); - } - - if (container.isMisReplicated()) { - boolean shouldAddRecord = !recordForStateExists.contains(UnHealthyContainerStates.MIS_REPLICATED.toString()); - if (shouldAddRecord) { - records.add(recordForState(container, UnHealthyContainerStates.MIS_REPLICATED, time)); - } - populateContainerStats(container, UnHealthyContainerStates.MIS_REPLICATED, unhealthyContainerStateStatsMap); - } - return records; - } - - private static UnhealthyContainers recordForState( - ContainerHealthStatus container, UnHealthyContainerStates state, - long time) { - UnhealthyContainers rec = new UnhealthyContainers(); - rec.setContainerId(container.getContainerID()); - if (state == UnHealthyContainerStates.MIS_REPLICATED) { - rec.setExpectedReplicaCount(container.expectedPlacementCount()); - rec.setActualReplicaCount(container.actualPlacementCount()); - rec.setReplicaDelta(container.misReplicatedDelta()); - rec.setReason(container.misReplicatedReason()); - } else { - rec.setExpectedReplicaCount(container.getReplicationFactor()); - rec.setActualReplicaCount(container.getReplicaCount()); - rec.setReplicaDelta(container.replicaDelta()); - } - rec.setContainerState(state.toString()); - rec.setInStateSince(time); - return rec; - } - - private static boolean keepOverReplicatedRecord( - ContainerHealthStatus container, UnhealthyContainersRecord rec) { - if (container.isOverReplicated()) { - updateExpectedReplicaCount(rec, container.getReplicationFactor()); - updateActualReplicaCount(rec, container.getReplicaCount()); - updateReplicaDelta(rec, container.replicaDelta()); - return true; - } - return false; - } - - private static boolean keepUnderReplicatedRecord( - ContainerHealthStatus container, UnhealthyContainersRecord rec) { - if (container.isUnderReplicated()) { - updateExpectedReplicaCount(rec, container.getReplicationFactor()); - updateActualReplicaCount(rec, container.getReplicaCount()); - updateReplicaDelta(rec, container.replicaDelta()); - return true; - } - return false; - } - - private static boolean keepMisReplicatedRecord( - ContainerHealthStatus container, UnhealthyContainersRecord rec) { - if (container.isMisReplicated()) { - updateExpectedReplicaCount(rec, container.expectedPlacementCount()); - updateActualReplicaCount(rec, container.actualPlacementCount()); - updateReplicaDelta(rec, container.misReplicatedDelta()); - updateReason(rec, container.misReplicatedReason()); - return true; - } - return false; - } - - private static boolean keepReplicaMismatchRecord( - ContainerHealthStatus container, UnhealthyContainersRecord rec) { - if (container.areChecksumsMismatched()) { - updateExpectedReplicaCount(rec, container.getReplicationFactor()); - updateActualReplicaCount(rec, container.getReplicaCount()); - updateReplicaDelta(rec, container.replicaDelta()); - return true; - } - return false; - } - - /** - * With a Jooq record, if you update any field in the record, the record - * is marked as changed, even if you updated it to the same value as it is - * already set to. We only need to run a DB update statement if the record - * has really changed. The methods below ensure we do not update the Jooq - * record unless the values have changed and hence save a DB execution - */ - private static void updateExpectedReplicaCount( - UnhealthyContainersRecord rec, int expectedCount) { - if (rec.getExpectedReplicaCount() != expectedCount) { - rec.setExpectedReplicaCount(expectedCount); - } - } - - private static void updateActualReplicaCount( - UnhealthyContainersRecord rec, int actualCount) { - if (rec.getActualReplicaCount() != actualCount) { - rec.setActualReplicaCount(actualCount); - } - } - - private static void updateReplicaDelta( - UnhealthyContainersRecord rec, int delta) { - if (rec.getReplicaDelta() != delta) { - rec.setReplicaDelta(delta); - } - } - - private static void updateReason( - UnhealthyContainersRecord rec, String reason) { - if (!rec.getReason().equals(reason)) { - rec.setReason(reason); - } - } - } - - private static void populateContainerStats( - ContainerHealthStatus container, - UnHealthyContainerStates unhealthyState, - Map> - unhealthyContainerStateStatsMap) { - if (unhealthyContainerStateStatsMap.containsKey(unhealthyState)) { - Map containerStatsMap = - unhealthyContainerStateStatsMap.get(unhealthyState); - containerStatsMap.compute(CONTAINER_COUNT, - (containerCount, value) -> (value == null) ? 1 : (value + 1)); - containerStatsMap.compute(TOTAL_KEYS, - (totalKeyCount, value) -> (value == null) ? container.getNumKeys() : - (value + container.getNumKeys())); - containerStatsMap.compute(TOTAL_USED_BYTES, - (totalUsedBytes, value) -> (value == null) ? - container.getContainer().getUsedBytes() : - (value + container.getContainer().getUsedBytes())); - } - } - - @Override - public synchronized void stop() { - super.stop(); - this.containerHealthMetrics.unRegister(); - } - - /** - * Expose the unhealthyContainerStateStatsMap for testing purposes. - */ - @VisibleForTesting - public Map> getUnhealthyContainerStateStatsMap() { - return unhealthyContainerStateStatsMapForTesting; - } - -} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index a052c6b68cec..4305a0aa6925 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -18,13 +18,8 @@ package org.apache.hadoop.ozone.recon.fsck; import javax.inject.Inject; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; -import org.apache.hadoop.hdds.scm.PlacementPolicy; -import org.apache.hadoop.hdds.scm.container.ContainerManager; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.scm.ReconScmTask; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; -import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; import org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdaterManager; import org.slf4j.Logger; @@ -37,7 +32,7 @@ *

        *
      • Uses Recon's local ReplicationManager (not RPC to SCM)
      • *
      • Calls processAll() once to check all containers in batch
      • - *
      • ReplicationManager uses stub PendingOps (NullContainerReplicaPendingOps)
      • + *
      • ReplicationManager uses stub PendingOps (NoOpsContainerReplicaPendingOps)
      • *
      • No false positives despite stub - health determination ignores pending ops
      • *
      • All database operations handled inside ReconReplicationManager
      • *
      @@ -52,7 +47,7 @@ *
    * * @see ReconReplicationManager - * @see NullContainerReplicaPendingOps + * @see NoOpsContainerReplicaPendingOps */ public class ContainerHealthTaskV2 extends ReconScmTask { @@ -63,13 +58,7 @@ public class ContainerHealthTaskV2 extends ReconScmTask { private final long interval; @Inject - @SuppressWarnings("checkstyle:ParameterNumber") public ContainerHealthTaskV2( - ContainerManager containerManager, - ContainerHealthSchemaManagerV2 schemaManagerV2, - PlacementPolicy placementPolicy, - ReconContainerMetadataManager reconContainerMetadataManager, - OzoneConfiguration conf, ReconTaskConfig reconTaskConfig, ReconTaskStatusUpdaterManager taskStatusUpdaterManager, ReconStorageContainerManagerFacade reconScm) { diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NoOpsContainerReplicaPendingOps.java similarity index 88% rename from hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java rename to hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NoOpsContainerReplicaPendingOps.java index 2b346b512a82..4ad24533a692 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NullContainerReplicaPendingOps.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/NoOpsContainerReplicaPendingOps.java @@ -28,7 +28,7 @@ import org.apache.hadoop.ozone.protocol.commands.SCMCommand; /** - * Null implementation of ContainerReplicaPendingOps for Recon's + * No-op implementation of ContainerReplicaPendingOps for Recon's * local ReplicationManager. * *

    This stub always returns empty pending operations because Recon does not @@ -40,9 +40,9 @@ * deduplication (Phase 2), which Recon doesn't need since it doesn't enqueue * commands.

    */ -public class NullContainerReplicaPendingOps extends ContainerReplicaPendingOps { +public class NoOpsContainerReplicaPendingOps extends ContainerReplicaPendingOps { - public NullContainerReplicaPendingOps(Clock clock, + public NoOpsContainerReplicaPendingOps(Clock clock, ReplicationManager.ReplicationManagerConfiguration rmConf) { super(clock, rmConf); } @@ -63,16 +63,19 @@ public List getPendingOps(ContainerID id) { /** * No-op since Recon doesn't add pending operations. */ + @Override public void scheduleAddReplica(ContainerID containerID, DatanodeDetails target, - SCMCommand command, int replicaIndex, long containerSize) { + int replicaIndex, SCMCommand command, long deadlineEpochMillis, + long containerSize, long scheduledEpochMillis) { // No-op - Recon doesn't send commands } /** * No-op since Recon doesn't add pending operations. */ + @Override public void scheduleDeleteReplica(ContainerID containerID, DatanodeDetails target, - SCMCommand command, int replicaIndex) { + int replicaIndex, SCMCommand command, long deadlineEpochMillis) { // No-op - Recon doesn't send commands } @@ -106,10 +109,4 @@ public long getPendingOpCount(ContainerReplicaOp.PendingOpType opType) { return 0L; } - /** - * Always returns 0 since Recon has no pending operations. - */ - public long getTotalScheduledBytes(DatanodeDetails datanode) { - return 0L; - } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 336304b8ea33..43e3d9fa37af 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -20,9 +20,8 @@ import java.io.IOException; import java.time.Clock; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.hadoop.hdds.conf.ConfigurationSource; @@ -32,8 +31,7 @@ import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; import org.apache.hadoop.hdds.scm.container.ContainerReplica; -import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport.HealthState; -import org.apache.hadoop.hdds.scm.container.replication.NullReplicationQueue; +import org.apache.hadoop.hdds.scm.container.replication.NoOpsReplicationQueue; 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; @@ -50,7 +48,7 @@ * *

    Key Differences from SCM:

    *
      - *
    1. Uses NullContainerReplicaPendingOps stub (no pending operations tracking)
    2. + *
    3. Uses NoOpsContainerReplicaPendingOps stub (no pending operations tracking)
    4. *
    5. Overrides processAll() to capture ALL container health states (no 100-sample limit)
    6. *
    7. Stores results in Recon's UNHEALTHY_CONTAINERS_V2 table
    8. *
    9. Does not issue replication commands (read-only monitoring)
    10. @@ -68,7 +66,7 @@ * Since Recon only needs Phase 1 (health determination) and doesn't issue commands, * the stub PendingOps does not cause false positives.

      * - * @see NullContainerReplicaPendingOps + * @see NoOpsContainerReplicaPendingOps * @see ReconReplicationManagerReport */ public class ReconReplicationManager extends ReplicationManager { @@ -79,34 +77,123 @@ public class ReconReplicationManager extends ReplicationManager { private final ContainerHealthSchemaManagerV2 healthSchemaManager; private final ContainerManager containerManager; - @SuppressWarnings("checkstyle:ParameterNumber") + /** + * Immutable wiring context for ReconReplicationManager initialization. + */ + public static final class InitContext { + private final ReplicationManagerConfiguration rmConf; + private final ConfigurationSource conf; + private final ContainerManager containerManager; + private final PlacementPolicy ratisContainerPlacement; + private final PlacementPolicy ecContainerPlacement; + private final EventPublisher eventPublisher; + private final SCMContext scmContext; + private final NodeManager nodeManager; + private final Clock clock; + + private InitContext(Builder builder) { + this.rmConf = builder.rmConf; + this.conf = builder.conf; + this.containerManager = builder.containerManager; + this.ratisContainerPlacement = builder.ratisContainerPlacement; + this.ecContainerPlacement = builder.ecContainerPlacement; + this.eventPublisher = builder.eventPublisher; + this.scmContext = builder.scmContext; + this.nodeManager = builder.nodeManager; + this.clock = builder.clock; + } + + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder for creating {@link InitContext} instances. + */ + public static final class Builder { + private ReplicationManagerConfiguration rmConf; + private ConfigurationSource conf; + private ContainerManager containerManager; + private PlacementPolicy ratisContainerPlacement; + private PlacementPolicy ecContainerPlacement; + private EventPublisher eventPublisher; + private SCMContext scmContext; + private NodeManager nodeManager; + private Clock clock; + + private Builder() { + } + + public Builder setRmConf(ReplicationManagerConfiguration rmConf) { + this.rmConf = rmConf; + return this; + } + + public Builder setConf(ConfigurationSource conf) { + this.conf = conf; + return this; + } + + public Builder setContainerManager(ContainerManager containerManager) { + this.containerManager = containerManager; + return this; + } + + public Builder setRatisContainerPlacement(PlacementPolicy ratisContainerPlacement) { + this.ratisContainerPlacement = ratisContainerPlacement; + return this; + } + + public Builder setEcContainerPlacement(PlacementPolicy ecContainerPlacement) { + this.ecContainerPlacement = ecContainerPlacement; + return this; + } + + public Builder setEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + return this; + } + + public Builder setScmContext(SCMContext scmContext) { + this.scmContext = scmContext; + return this; + } + + public Builder setNodeManager(NodeManager nodeManager) { + this.nodeManager = nodeManager; + return this; + } + + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + public InitContext build() { + return new InitContext(this); + } + } + } + public ReconReplicationManager( - ReplicationManagerConfiguration rmConf, - ConfigurationSource conf, - ContainerManager containerManager, - PlacementPolicy ratisContainerPlacement, - PlacementPolicy ecContainerPlacement, - EventPublisher eventPublisher, - SCMContext scmContext, - NodeManager nodeManager, - Clock clock, + InitContext initContext, ContainerHealthSchemaManagerV2 healthSchemaManager) throws IOException { // Call parent with stub PendingOps (proven to not cause false positives) super( - rmConf, - conf, - containerManager, - ratisContainerPlacement, - ecContainerPlacement, - eventPublisher, - scmContext, - nodeManager, - clock, - new NullContainerReplicaPendingOps(clock, rmConf) + initContext.rmConf, + initContext.conf, + initContext.containerManager, + initContext.ratisContainerPlacement, + initContext.ecContainerPlacement, + initContext.eventPublisher, + initContext.scmContext, + initContext.nodeManager, + initContext.clock, + new NoOpsContainerReplicaPendingOps(initContext.clock, initContext.rmConf) ); - this.containerManager = containerManager; + this.containerManager = initContext.containerManager; this.healthSchemaManager = healthSchemaManager; } @@ -145,7 +232,7 @@ public synchronized void start() { *
*

* - *

This matches the legacy ContainerHealthTask logic: + *

This uses checksum mismatch logic: * {@code replicas.stream().map(ContainerReplica::getDataChecksum).distinct().count() != 1} *

* @@ -185,7 +272,7 @@ private boolean hasDataChecksumMismatch(Set replicas) { *

Differences from SCM's processAll():

*
    *
  • Uses ReconReplicationManagerReport (captures all containers)
  • - *
  • Uses NullReplicationQueue (doesn't enqueue commands)
  • + *
  • Uses NoOpsReplicationQueue (doesn't enqueue commands)
  • *
  • Adds REPLICA_MISMATCH detection (not done by SCM)
  • *
  • Stores results in database instead of just keeping in-memory report
  • *
@@ -198,7 +285,7 @@ public synchronized void processAll() { // Use extended report that captures ALL containers, not just 100 samples final ReconReplicationManagerReport report = new ReconReplicationManagerReport(); - final ReplicationQueue nullQueue = new NullReplicationQueue(); + final ReplicationQueue nullQueue = new NoOpsReplicationQueue(); // Get all containers (same as parent) final List containers = containerManager.getContainers(); @@ -257,84 +344,90 @@ private void storeHealthStatesToDatabase( long currentTime = System.currentTimeMillis(); List recordsToInsert = new ArrayList<>(); List containerIdsToDelete = new ArrayList<>(); - - // Get all containers per health state (not just 100 samples) - Map> containersByState = - report.getAllContainersByState(); - - LOG.info("Processing health states: MISSING={}, UNDER_REPLICATED={}, " + - "OVER_REPLICATED={}, MIS_REPLICATED={}, REPLICA_MISMATCH={}", - report.getAllContainersCount(HealthState.MISSING), - report.getAllContainersCount(HealthState.UNDER_REPLICATED), - report.getAllContainersCount(HealthState.OVER_REPLICATED), - report.getAllContainersCount(HealthState.MIS_REPLICATED), - report.getReplicaMismatchCount()); - - // Process MISSING containers - List missingContainers = - containersByState.getOrDefault(HealthState.MISSING, Collections.emptyList()); - for (ContainerID cid : missingContainers) { + final int[] missingCount = {0}; + final int[] underRepCount = {0}; + final int[] overRepCount = {0}; + final int[] misRepCount = {0}; + final int[] emptyMissingCount = {0}; + final int[] negativeSizeCount = {0}; + Set negativeSizeRecorded = new HashSet<>(); + + report.forEachContainerByState((state, cid) -> { try { - ContainerInfo container = containerManager.getContainer(cid); - int expected = container.getReplicationConfig().getRequiredNodes(); - recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.MISSING, currentTime, expected, 0, - "No replicas available")); - } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing MISSING state", cid, e); - } - } - - // Process UNDER_REPLICATED containers - List underRepContainers = - containersByState.getOrDefault(HealthState.UNDER_REPLICATED, Collections.emptyList()); - for (ContainerID cid : underRepContainers) { - try { - ContainerInfo container = containerManager.getContainer(cid); - Set replicas = containerManager.getContainerReplicas(cid); - int expected = container.getReplicationConfig().getRequiredNodes(); - int actual = replicas.size(); - recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.UNDER_REPLICATED, currentTime, expected, actual, - "Insufficient replicas")); - } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing UNDER_REPLICATED state", cid, e); - } - } - - // Process OVER_REPLICATED containers - List overRepContainers = - containersByState.getOrDefault(HealthState.OVER_REPLICATED, Collections.emptyList()); - for (ContainerID cid : overRepContainers) { - try { - ContainerInfo container = containerManager.getContainer(cid); - Set replicas = containerManager.getContainerReplicas(cid); - int expected = container.getReplicationConfig().getRequiredNodes(); - int actual = replicas.size(); - recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.OVER_REPLICATED, currentTime, expected, actual, - "Excess replicas")); - } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing OVER_REPLICATED state", cid, e); - } - } - - // Process MIS_REPLICATED containers - List misRepContainers = - containersByState.getOrDefault(HealthState.MIS_REPLICATED, Collections.emptyList()); - for (ContainerID cid : misRepContainers) { - try { - ContainerInfo container = containerManager.getContainer(cid); - Set replicas = containerManager.getContainerReplicas(cid); - int expected = container.getReplicationConfig().getRequiredNodes(); - int actual = replicas.size(); - recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.MIS_REPLICATED, currentTime, expected, actual, - "Placement policy violated")); + switch (state) { + case MISSING: + ContainerInfo missingContainer = containerManager.getContainer(cid); + if (isEmptyMissing(missingContainer)) { + emptyMissingCount[0]++; + int missingExpected = missingContainer.getReplicationConfig().getRequiredNodes(); + recordsToInsert.add(createRecord(missingContainer, + UnHealthyContainerStates.EMPTY_MISSING, currentTime, missingExpected, 0, + "Container has no replicas and no keys")); + break; + } + missingCount[0]++; + int nonEmptyMissingExpected = + missingContainer.getReplicationConfig().getRequiredNodes(); + recordsToInsert.add(createRecord(missingContainer, + UnHealthyContainerStates.MISSING, currentTime, nonEmptyMissingExpected, 0, + "No replicas available")); + break; + case UNDER_REPLICATED: + underRepCount[0]++; + ContainerInfo underRepContainer = containerManager.getContainer(cid); + Set underReplicas = containerManager.getContainerReplicas(cid); + int underRepExpected = underRepContainer.getReplicationConfig().getRequiredNodes(); + int underRepActual = underReplicas.size(); + recordsToInsert.add(createRecord(underRepContainer, + UnHealthyContainerStates.UNDER_REPLICATED, currentTime, + underRepExpected, underRepActual, + "Insufficient replicas")); + addNegativeSizeRecordIfNeeded(underRepContainer, currentTime, underRepActual, recordsToInsert, + negativeSizeRecorded, negativeSizeCount); + break; + case OVER_REPLICATED: + overRepCount[0]++; + ContainerInfo overRepContainer = containerManager.getContainer(cid); + Set overReplicas = containerManager.getContainerReplicas(cid); + int overRepExpected = overRepContainer.getReplicationConfig().getRequiredNodes(); + int overRepActual = overReplicas.size(); + recordsToInsert.add(createRecord(overRepContainer, + UnHealthyContainerStates.OVER_REPLICATED, currentTime, + overRepExpected, overRepActual, + "Excess replicas")); + addNegativeSizeRecordIfNeeded(overRepContainer, currentTime, overRepActual, recordsToInsert, + negativeSizeRecorded, negativeSizeCount); + break; + case MIS_REPLICATED: + misRepCount[0]++; + ContainerInfo misRepContainer = containerManager.getContainer(cid); + Set misReplicas = containerManager.getContainerReplicas(cid); + int misRepExpected = misRepContainer.getReplicationConfig().getRequiredNodes(); + int misRepActual = misReplicas.size(); + recordsToInsert.add(createRecord(misRepContainer, + UnHealthyContainerStates.MIS_REPLICATED, currentTime, misRepExpected, misRepActual, + "Placement policy violated")); + addNegativeSizeRecordIfNeeded(misRepContainer, currentTime, misRepActual, recordsToInsert, + negativeSizeRecorded, negativeSizeCount); + break; + default: + break; + } } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing MIS_REPLICATED state", cid); + LOG.warn("Container {} not found when processing {} state", cid, state, e); } - } + }); + + LOG.info("Processing health states: MISSING={}, EMPTY_MISSING={}, " + + "UNDER_REPLICATED={}, OVER_REPLICATED={}, MIS_REPLICATED={}, " + + "NEGATIVE_SIZE={}, REPLICA_MISMATCH={}", + missingCount[0], + emptyMissingCount[0], + underRepCount[0], + overRepCount[0], + misRepCount[0], + negativeSizeCount[0], + report.getReplicaMismatchCount()); // Process REPLICA_MISMATCH containers (Recon-specific) List replicaMismatchContainers = report.getReplicaMismatchContainers(); @@ -364,13 +457,39 @@ private void storeHealthStatesToDatabase( LOG.info("Inserting {} unhealthy container records", recordsToInsert.size()); healthSchemaManager.insertUnhealthyContainerRecords(recordsToInsert); - LOG.info("Stored {} MISSING, {} UNDER_REPLICATED, {} OVER_REPLICATED, " + - "{} MIS_REPLICATED, {} REPLICA_MISMATCH", - missingContainers.size(), underRepContainers.size(), - overRepContainers.size(), misRepContainers.size(), + LOG.info("Stored {} MISSING, {} EMPTY_MISSING, {} UNDER_REPLICATED, " + + "{} OVER_REPLICATED, {} MIS_REPLICATED, {} NEGATIVE_SIZE, " + + "{} REPLICA_MISMATCH", + missingCount[0], emptyMissingCount[0], underRepCount[0], + overRepCount[0], misRepCount[0], negativeSizeCount[0], replicaMismatchContainers.size()); } + private boolean isEmptyMissing(ContainerInfo container) { + return container.getNumberOfKeys() == 0; + } + + private boolean isNegativeSize(ContainerInfo container) { + return container.getUsedBytes() < 0; + } + + private void addNegativeSizeRecordIfNeeded( + ContainerInfo container, + long currentTime, + int actualReplicaCount, + List recordsToInsert, + Set negativeSizeRecorded, + int[] negativeSizeCount) { + if (isNegativeSize(container) + && negativeSizeRecorded.add(container.getContainerID())) { + int expected = container.getReplicationConfig().getRequiredNodes(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.NEGATIVE_SIZE, currentTime, expected, actualReplicaCount, + "Container reports negative usedBytes")); + negativeSizeCount[0]++; + } + } + /** * Create an unhealthy container record for database insertion. * diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java index 17798d1b4d85..c602a75e3ff8 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java @@ -22,7 +22,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; +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.ReplicationManagerReport; /** @@ -50,12 +53,18 @@ public class ReconReplicationManagerReport extends ReplicationManagerReport { // Captures ALL containers per health state (no SAMPLE_LIMIT restriction) - private final Map> allContainersByState = + private final Map> allContainersByState = new HashMap<>(); // Captures containers with REPLICA_MISMATCH (Recon-specific, not in SCM's HealthState) private final List replicaMismatchContainers = new ArrayList<>(); + public ReconReplicationManagerReport() { + // Recon keeps a full per-state list in allContainersByState below. + // Disable base sampling map to avoid duplicate tracking. + super(0); + } + /** * Override to capture ALL containers, not just first 100 samples. * Still calls parent method to maintain aggregate counts and samples @@ -65,14 +74,14 @@ public class ReconReplicationManagerReport extends ReplicationManagerReport { * @param container The container ID to record */ @Override - public void incrementAndSample(HealthState stat, ContainerID container) { - // Call parent to maintain aggregate counts and samples (limited to 100) + public void incrementAndSample(ContainerHealthState stat, ContainerInfo container) { + // Call parent to maintain aggregate counts. super.incrementAndSample(stat, container); // Capture ALL containers for Recon (no SAMPLE_LIMIT restriction) allContainersByState .computeIfAbsent(stat, k -> new ArrayList<>()) - .add(container); + .add(container.containerID()); } /** @@ -84,7 +93,7 @@ public void incrementAndSample(HealthState stat, ContainerID container) { * @return List of all container IDs with the specified health state, * or empty list if none */ - public List getAllContainers(HealthState stat) { + public List getAllContainers(ContainerHealthState stat) { return allContainersByState.getOrDefault(stat, Collections.emptyList()); } @@ -94,7 +103,7 @@ public List getAllContainers(HealthState stat) { * * @return Immutable map of HealthState to list of container IDs */ - public Map> getAllContainersByState() { + public Map> getAllContainersByState() { return Collections.unmodifiableMap(allContainersByState); } @@ -106,19 +115,10 @@ public Map> getAllContainersByState() { * @param stat The health state to query * @return Number of containers captured for this state */ - public int getAllContainersCount(HealthState stat) { + public int getAllContainersCount(ContainerHealthState stat) { return allContainersByState.getOrDefault(stat, Collections.emptyList()).size(); } - /** - * Clear all captured containers. Useful for resetting the report - * for a new processing cycle. - */ - public void clearAllContainers() { - allContainersByState.clear(); - replicaMismatchContainers.clear(); - } - /** * Add a container to the REPLICA_MISMATCH list. * This is a Recon-specific health state not tracked by SCM. @@ -146,4 +146,13 @@ public List getReplicaMismatchContainers() { public int getReplicaMismatchCount() { return replicaMismatchContainers.size(); } + + /** + * Iterate through all unhealthy containers captured from SCM health states. + */ + public void forEachContainerByState( + BiConsumer consumer) { + allContainersByState.forEach( + (state, containers) -> containers.forEach(container -> consumer.accept(state, container))); + } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthMetrics.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthMetrics.java deleted file mode 100644 index f013f8670afe..000000000000 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthMetrics.java +++ /dev/null @@ -1,86 +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.metrics; - -import org.apache.hadoop.hdds.annotation.InterfaceAudience; -import org.apache.hadoop.metrics2.MetricsSystem; -import org.apache.hadoop.metrics2.annotation.Metric; -import org.apache.hadoop.metrics2.annotation.Metrics; -import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem; -import org.apache.hadoop.metrics2.lib.MutableGaugeLong; -import org.apache.hadoop.ozone.OzoneConsts; - -/** - * Class for tracking metrics related to container health task in Recon. - */ -@InterfaceAudience.Private -@Metrics(about = "Recon ContainerHealthTask Metrics", context = OzoneConsts.OZONE) -public final class ContainerHealthMetrics { - - private static final String SOURCE_NAME = - ContainerHealthMetrics.class.getSimpleName(); - - @Metric(about = "Number of missing containers detected in Recon.") - private MutableGaugeLong missingContainerCount; - - @Metric(about = "Number of under replicated containers detected in Recon.") - private MutableGaugeLong underReplicatedContainerCount; - - @Metric(about = "Number of replica mismatch containers detected in Recon.") - private MutableGaugeLong replicaMisMatchContainerCount; - - private ContainerHealthMetrics() { - } - - public void unRegister() { - MetricsSystem ms = DefaultMetricsSystem.instance(); - ms.unregisterSource(SOURCE_NAME); - } - - public static ContainerHealthMetrics create() { - MetricsSystem ms = DefaultMetricsSystem.instance(); - return ms.register(SOURCE_NAME, - "Recon Container Health Task Metrics", - new ContainerHealthMetrics()); - } - - public void setMissingContainerCount(long missingContainerCount) { - this.missingContainerCount.set(missingContainerCount); - } - - public void setUnderReplicatedContainerCount(long underReplicatedContainerCount) { - this.underReplicatedContainerCount.set(underReplicatedContainerCount); - } - - public void setReplicaMisMatchContainerCount(long replicaMisMatchContainerCount) { - this.replicaMisMatchContainerCount.set(replicaMisMatchContainerCount); - } - - public long getMissingContainerCount() { - return missingContainerCount.value(); - } - - public long getUnderReplicatedContainerCount() { - return underReplicatedContainerCount.value(); - } - - public long getReplicaMisMatchContainerCount() { - return replicaMisMatchContainerCount.value(); - } - -} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java deleted file mode 100644 index bbafeaa4b83c..000000000000 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java +++ /dev/null @@ -1,231 +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.persistence; - -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates.ALL_REPLICAS_BAD; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED; -import static org.apache.ozone.recon.schema.generated.tables.UnhealthyContainersTable.UNHEALTHY_CONTAINERS; -import static org.jooq.impl.DSL.count; - -import com.google.common.annotations.VisibleForTesting; -import com.google.inject.Inject; -import com.google.inject.Singleton; -import java.sql.Connection; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; -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.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; -import org.apache.ozone.recon.schema.generated.tables.records.UnhealthyContainersRecord; -import org.jooq.Condition; -import org.jooq.Cursor; -import org.jooq.DSLContext; -import org.jooq.OrderField; -import org.jooq.Record; -import org.jooq.SelectQuery; -import org.jooq.exception.DataAccessException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provide a high level API to access the Container Schema. - */ -@Singleton -public class ContainerHealthSchemaManager { - private static final Logger LOG = - LoggerFactory.getLogger(ContainerHealthSchemaManager.class); - - private final UnhealthyContainersDao unhealthyContainersDao; - private final ContainerSchemaDefinition containerSchemaDefinition; - - @Inject - public ContainerHealthSchemaManager( - ContainerSchemaDefinition containerSchemaDefinition, - UnhealthyContainersDao unhealthyContainersDao) { - this.unhealthyContainersDao = unhealthyContainersDao; - this.containerSchemaDefinition = containerSchemaDefinition; - } - - /** - * Get a batch of unhealthy containers, starting at offset and returning - * limit records. If a null value is passed for state, then unhealthy - * containers in all states will be returned. Otherwise, only containers - * matching the given state will be returned. - * @param state Return only containers in this state, or all containers if - * null - * @param minContainerId minimum containerId for filter - * @param maxContainerId maximum containerId for filter - * @param limit The total records to return - * @return List of unhealthy containers. - */ - public List getUnhealthyContainers( - UnHealthyContainerStates state, Long minContainerId, Optional 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()}; - } else { - containerCondition = UNHEALTHY_CONTAINERS.CONTAINER_ID.greaterThan(minContainerId); - 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()))); - } - } 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()); - } - - /** - * 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 - */ - public List getUnhealthyContainersSummary() { - 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); - } - - public Cursor getAllUnhealthyRecordsCursor() { - DSLContext dslContext = containerSchemaDefinition.getDSLContext(); - return dslContext - .selectFrom(UNHEALTHY_CONTAINERS) - .orderBy(UNHEALTHY_CONTAINERS.CONTAINER_ID.asc()) - .fetchLazy(); - } - - public void insertUnhealthyContainerRecords(List recs) { - if (LOG.isDebugEnabled()) { - recs.forEach(rec -> LOG.debug("rec.getContainerId() : {}, rec.getContainerState(): {}", - rec.getContainerId(), rec.getContainerState())); - } - - 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 - } - } 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); - } - } - - /** - * Delete a specific unhealthy container state record. - * - * @param containerId Container ID - * @param state Container state to delete - */ - public void deleteUnhealthyContainer(long containerId, Object state) { - DSLContext dslContext = containerSchemaDefinition.getDSLContext(); - try { - if (state instanceof UnHealthyContainerStates) { - dslContext.deleteFrom(UNHEALTHY_CONTAINERS) - .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.eq(containerId)) - .and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(((UnHealthyContainerStates) state).toString())) - .execute(); - } else { - dslContext.deleteFrom(UNHEALTHY_CONTAINERS) - .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.eq(containerId)) - .and(UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString())) - .execute(); - } - } catch (Exception e) { - LOG.error("Failed to delete unhealthy container {} state {}", containerId, state, e); - } - } - - /** - * Delete all unhealthy states for a container. - * - * @param containerId Container ID - */ - public void deleteAllStatesForContainer(long containerId) { - DSLContext dslContext = containerSchemaDefinition.getDSLContext(); - try { - dslContext.deleteFrom(UNHEALTHY_CONTAINERS) - .where(UNHEALTHY_CONTAINERS.CONTAINER_ID.eq(containerId)) - .execute(); - } catch (Exception e) { - LOG.error("Failed to delete all states for container {}", containerId, e); - } - } - - /** - * Clear all unhealthy container records. This is primarily used for testing - * to ensure clean state between tests. - */ - @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); - } - } - -} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index 1289773b090a..b1ed458adafd 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -21,6 +21,7 @@ import static org.apache.ozone.recon.schema.generated.tables.UnhealthyContainersV2Table.UNHEALTHY_CONTAINERS_V2; import static org.jooq.impl.DSL.count; +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; import java.sql.Connection; @@ -44,13 +45,12 @@ /** * Manager for UNHEALTHY_CONTAINERS_V2 table used by ContainerHealthTaskV2. - * This is independent from ContainerHealthSchemaManager to allow both - * implementations to run in parallel. */ @Singleton public class ContainerHealthSchemaManagerV2 { private static final Logger LOG = LoggerFactory.getLogger(ContainerHealthSchemaManagerV2.class); + private static final int BATCH_INSERT_CHUNK_SIZE = 1000; private final UnhealthyContainersV2Dao unhealthyContainersV2Dao; private final ContainerSchemaDefinitionV2 containerSchemaDefinitionV2; @@ -81,26 +81,27 @@ public void insertUnhealthyContainerRecords(List rec DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); try { - // Try batch insert first (optimal path - single SQL statement) + // Try batch insert first in chunks to keep memory/transaction pressure bounded. dslContext.transaction(configuration -> { DSLContext txContext = configuration.dsl(); - // Build batch insert using VALUES clause - List records = new ArrayList<>(); - for (UnhealthyContainerRecordV2 rec : recs) { - UnhealthyContainersV2Record record = txContext.newRecord(UNHEALTHY_CONTAINERS_V2); - 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()); - records.add(record); + for (int from = 0; from < recs.size(); from += BATCH_INSERT_CHUNK_SIZE) { + int to = Math.min(from + BATCH_INSERT_CHUNK_SIZE, recs.size()); + List records = new ArrayList<>(to - from); + for (int i = from; i < to; i++) { + UnhealthyContainerRecordV2 rec = recs.get(i); + UnhealthyContainersV2Record record = txContext.newRecord(UNHEALTHY_CONTAINERS_V2); + 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()); + records.add(record); + } + txContext.batchInsert(records).execute(); } - - // Execute true batch insert (single INSERT statement with multiple VALUES) - txContext.batchInsert(records).execute(); }); LOG.debug("Batch inserted {} unhealthy container records", recs.size()); @@ -150,44 +151,12 @@ public void insertUnhealthyContainerRecords(List rec } } - /** - * Delete a specific unhealthy container record from V2 table. - */ - public void deleteUnhealthyContainer(long containerId, Object state) { - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); - try { - String stateStr = (state instanceof UnHealthyContainerStates) - ? ((UnHealthyContainerStates) state).toString() - : state.toString(); - dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) - .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.eq(containerId)) - .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.eq(stateStr)) - .execute(); - LOG.debug("Deleted container {} with state {} from V2 table", containerId, state); - } catch (Exception e) { - LOG.error("Failed to delete container {} from V2 table", containerId, e); - } - } - - /** - * Delete all records for a specific container (all states). - */ - public void deleteAllStatesForContainer(long containerId) { - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); - try { - int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) - .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.eq(containerId)) - .execute(); - LOG.debug("Deleted {} records for container {} from V2 table", deleted, containerId); - } catch (Exception e) { - LOG.error("Failed to delete all states for container {} from V2 table", containerId, e); - } - } - /** * Batch delete SCM-tracked states for multiple containers. - * This deletes MISSING, UNDER_REPLICATED, OVER_REPLICATED, MIS_REPLICATED - * for all containers in the list in a single transaction. + * This deletes all states generated from SCM/Recon health scans: + * MISSING, EMPTY_MISSING, UNDER_REPLICATED, OVER_REPLICATED, + * MIS_REPLICATED and NEGATIVE_SIZE for all containers in the list in a + * single transaction. * REPLICA_MISMATCH is NOT deleted as it's tracked locally by Recon. * * @param containerIds List of container IDs to delete states for @@ -203,9 +172,11 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.in(containerIds)) .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.in( UnHealthyContainerStates.MISSING.toString(), + UnHealthyContainerStates.EMPTY_MISSING.toString(), UnHealthyContainerStates.UNDER_REPLICATED.toString(), UnHealthyContainerStates.OVER_REPLICATED.toString(), - UnHealthyContainerStates.MIS_REPLICATED.toString())) + UnHealthyContainerStates.MIS_REPLICATED.toString(), + UnHealthyContainerStates.NEGATIVE_SIZE.toString())) .execute(); LOG.debug("Batch deleted {} SCM-tracked state records for {} containers", deleted, containerIds.size()); @@ -215,34 +186,6 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { } } - /** - * Batch delete REPLICA_MISMATCH state for multiple containers. - * This is separate from batchDeleteSCMStatesForContainers because - * REPLICA_MISMATCH is tracked locally by Recon, not by SCM. - * - * @param containerIds List of container IDs to delete REPLICA_MISMATCH for - */ - public void batchDeleteReplicaMismatchForContainers(List containerIds) { - if (containerIds == null || containerIds.isEmpty()) { - return; - } - - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); - try { - int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) - .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.in(containerIds)) - .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.eq( - UnHealthyContainerStates.REPLICA_MISMATCH.toString())) - .execute(); - LOG.debug("Batch deleted {} REPLICA_MISMATCH records for {} containers", - deleted, containerIds.size()); - } catch (Exception e) { - LOG.error("Failed to batch delete REPLICA_MISMATCH for {} containers", - containerIds.size(), e); - throw new RuntimeException("Failed to batch delete REPLICA_MISMATCH", e); - } - } - /** * Get summary of unhealthy containers grouped by state from V2 table. */ @@ -321,6 +264,7 @@ public List getUnhealthyContainers( /** * Clear all records from V2 table (for testing). */ + @VisibleForTesting public void clearAllUnhealthyContainerRecords() { DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); try { diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java index 844875b721d0..1290c5962abd 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java @@ -50,7 +50,6 @@ import org.apache.hadoop.hdds.utils.db.DBStore; import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.ozone.common.statemachine.InvalidStateTransitionException; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; @@ -67,7 +66,6 @@ public class ReconContainerManager extends ContainerManagerImpl { LoggerFactory.getLogger(ReconContainerManager.class); private final StorageContainerServiceProvider scmClient; private final PipelineManager pipelineManager; - private final ContainerHealthSchemaManager containerHealthSchemaManager; private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; private final ReconContainerMetadataManager cdbServiceProvider; private final Table nodeDB; @@ -83,7 +81,6 @@ public ReconContainerManager( Table containerStore, PipelineManager pipelineManager, StorageContainerServiceProvider scm, - ContainerHealthSchemaManager containerHealthSchemaManager, ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2, ReconContainerMetadataManager reconContainerMetadataManager, SCMHAManager scmhaManager, @@ -94,7 +91,6 @@ public ReconContainerManager( pendingOps); this.scmClient = scm; this.pipelineManager = pipelineManager; - this.containerHealthSchemaManager = containerHealthSchemaManager; this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; this.cdbServiceProvider = reconContainerMetadataManager; this.nodeDB = ReconSCMDBDefinition.NODES.getTable(store); @@ -344,11 +340,6 @@ public void removeContainerReplica(ContainerID containerID, } } - @VisibleForTesting - public ContainerHealthSchemaManager getContainerSchemaManager() { - return containerHealthSchemaManager; - } - @VisibleForTesting public ContainerHealthSchemaManagerV2 getContainerSchemaManagerV2() { return containerHealthSchemaManagerV2; 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 eb524d040eac..2015b841c08f 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 @@ -114,11 +114,9 @@ import org.apache.hadoop.ozone.recon.ReconContext; 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.ContainerHealthTaskV2; 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.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; @@ -157,7 +155,6 @@ public class ReconStorageContainerManagerFacade private final SCMNodeDetails reconNodeDetails; private final SCMHAManager scmhaManager; private final SequenceIdGenerator sequenceIdGen; - private final ReconScmTask containerHealthTask; private final ReconScmTask containerHealthTaskV2; private final DataSource dataSource; private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; @@ -184,7 +181,6 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, StorageContainerServiceProvider scmServiceProvider, ContainerCountBySizeDao containerCountBySizeDao, UtilizationSchemaDefinition utilizationSchemaDefinition, - ContainerHealthSchemaManager containerHealthSchemaManager, ReconContainerMetadataManager reconContainerMetadataManager, ReconUtils reconUtils, ReconSafeModeManager safeModeManager, @@ -253,7 +249,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, dbStore, ReconSCMDBDefinition.CONTAINERS.getTable(dbStore), pipelineManager, scmServiceProvider, - containerHealthSchemaManager, containerHealthSchemaManagerV2, + containerHealthSchemaManagerV2, reconContainerMetadataManager, scmhaManager, sequenceIdGen, pendingOps); this.scmServiceProvider = scmServiceProvider; @@ -274,27 +270,9 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, PipelineSyncTask pipelineSyncTask = new PipelineSyncTask(pipelineManager, nodeManager, scmServiceProvider, reconTaskConfig, taskStatusUpdaterManager); - // Create legacy ContainerHealthTask (always runs, writes to UNHEALTHY_CONTAINERS) - LOG.info("Creating ContainerHealthTask (legacy)"); - containerHealthTask = new ContainerHealthTask( - containerManager, - scmServiceProvider, - containerHealthSchemaManager, - containerPlacementPolicy, - reconTaskConfig, - reconContainerMetadataManager, - conf, - taskStatusUpdaterManager - ); - // Create ContainerHealthTaskV2 (always runs, writes to UNHEALTHY_CONTAINERS_V2) LOG.info("Creating ContainerHealthTaskV2"); containerHealthTaskV2 = new ContainerHealthTaskV2( - containerManager, - containerHealthSchemaManagerV2, - containerPlacementPolicy, - reconContainerMetadataManager, - conf, reconTaskConfig, taskStatusUpdaterManager, this // ReconStorageContainerManagerFacade - provides access to ReconReplicationManager @@ -310,15 +288,18 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, try { LOG.info("Creating ReconReplicationManager"); this.reconReplicationManager = new ReconReplicationManager( - conf.getObject(ReplicationManager.ReplicationManagerConfiguration.class), - conf, - containerManager, - containerPlacementPolicy, // Use for both Ratis and EC - containerPlacementPolicy, - eventQueue, - scmContext, - nodeManager, - Clock.system(ZoneId.systemDefault()), + 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(), containerHealthSchemaManagerV2 ); LOG.info("Successfully created ReconReplicationManager"); @@ -331,7 +312,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, new ReconStaleNodeHandler(nodeManager, pipelineManager, pipelineSyncTask); DeadNodeHandler deadNodeHandler = new ReconDeadNodeHandler(nodeManager, pipelineManager, containerManager, scmServiceProvider, - containerHealthTask, pipelineSyncTask); + containerHealthTaskV2, pipelineSyncTask); ContainerReportHandler containerReportHandler = new ReconContainerReportHandler(nodeManager, containerManager); @@ -399,7 +380,6 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, eventQueue.addHandler(SCMEvents.CLOSE_CONTAINER, closeContainerHandler); eventQueue.addHandler(SCMEvents.NEW_NODE, newNodeHandler); reconScmTasks.add(pipelineSyncTask); - reconScmTasks.add(containerHealthTask); reconScmTasks.add(containerHealthTaskV2); reconScmTasks.add(containerSizeCountTask); reconSafeModeMgrTask = new ReconSafeModeMgrTask( @@ -781,7 +761,7 @@ public ContainerSizeCountTask getContainerSizeCountTask() { @VisibleForTesting public ReconScmTask getContainerHealthTask() { - return containerHealthTask; + return containerHealthTaskV2; } @VisibleForTesting diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java deleted file mode 100644 index ea8af99d96e6..000000000000 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hadoop.ozone.recon.upgrade; - -import static org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature.INITIAL_VERSION; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; -import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.name; - -import com.google.common.annotations.VisibleForTesting; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Arrays; -import javax.sql.DataSource; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; -import org.jooq.DSLContext; -import org.jooq.impl.DSL; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Upgrade action for the INITIAL schema version, which manages constraints - * for the UNHEALTHY_CONTAINERS table. - */ -@UpgradeActionRecon(feature = INITIAL_VERSION) -public class InitialConstraintUpgradeAction implements ReconUpgradeAction { - - private static final Logger LOG = LoggerFactory.getLogger(InitialConstraintUpgradeAction.class); - private DSLContext dslContext; - - @Override - public void execute(DataSource source) throws SQLException { - try (Connection conn = source.getConnection()) { - if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { - return; - } - dslContext = DSL.using(conn); - // Drop the existing constraint - dropConstraint(); - // Add the updated constraint with all enum states - addUpdatedConstraint(); - } catch (SQLException e) { - throw new SQLException("Failed to execute InitialConstraintUpgradeAction", e); - } - } - - /** - * Drops the existing constraint from the UNHEALTHY_CONTAINERS table. - */ - private void dropConstraint() { - String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1"; - dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME) - .dropConstraint(constraintName) - .execute(); - LOG.debug("Dropped the existing constraint: {}", constraintName); - } - - /** - * Adds the updated constraint directly within this class. - */ - private void addUpdatedConstraint() { - String[] enumStates = Arrays - .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) - .map(Enum::name) - .toArray(String[]::new); - - dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME) - .add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") - .check(field(name("container_state")) - .in(enumStates))) - .execute(); - - LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table for enum state values: {}", - Arrays.toString(enumStates)); - } - - @VisibleForTesting - public void setDslContext(DSLContext dslContext) { - this.dslContext = dslContext; - } -} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java deleted file mode 100644 index ebf8556f5c49..000000000000 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hadoop.ozone.recon.upgrade; - -import static org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature.UNHEALTHY_CONTAINER_REPLICA_MISMATCH; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; -import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.name; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Arrays; -import javax.sql.DataSource; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; -import org.jooq.DSLContext; -import org.jooq.impl.DSL; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Upgrade action for handling the addition of a new unhealthy container state in Recon, which will be for containers, - * that have replicas with different data checksums. - */ -@UpgradeActionRecon(feature = UNHEALTHY_CONTAINER_REPLICA_MISMATCH) -public class UnhealthyContainerReplicaMismatchAction implements ReconUpgradeAction { - private static final Logger LOG = LoggerFactory.getLogger(UnhealthyContainerReplicaMismatchAction.class); - private DSLContext dslContext; - - @Override - public void execute(DataSource source) throws Exception { - try (Connection conn = source.getConnection()) { - if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { - return; - } - dslContext = DSL.using(conn); - // Drop the existing constraint - dropConstraint(); - // Add the updated constraint with all enum states - addUpdatedConstraint(); - } catch (SQLException e) { - throw new SQLException("Failed to execute UnhealthyContainerReplicaMismatchAction", e); - } - } - - /** - * Drops the existing constraint from the UNHEALTHY_CONTAINERS table. - */ - private void dropConstraint() { - String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1"; - dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME) - .dropConstraint(constraintName) - .execute(); - LOG.debug("Dropped the existing constraint: {}", constraintName); - } - - /** - * Adds the updated constraint directly within this class. - */ - private void addUpdatedConstraint() { - String[] enumStates = Arrays - .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) - .map(Enum::name) - .toArray(String[]::new); - - dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME) - .add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") - .check(field(name("container_state")) - .in(enumStates))) - .execute(); - - LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table for enum state values: {}", - Arrays.toString(enumStates)); - } -} 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..92adcf931fd1 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 @@ -54,7 +54,7 @@ import org.apache.hadoop.ozone.recon.api.types.ClusterStateResponse; import org.apache.hadoop.ozone.recon.api.types.ClusterStorageReport; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconNodeManager; @@ -110,7 +110,7 @@ public void setUp() throws Exception { .addBinding(StorageContainerServiceProvider.class, mock(StorageContainerServiceProviderImpl.class)) .addBinding(ClusterStateEndpoint.class) - .addBinding(ContainerHealthSchemaManager.class) + .addBinding(ContainerHealthSchemaManagerV2.class) .build(); OzoneStorageContainerManager ozoneStorageContainerManager = reconTestInjector.getInstance(OzoneStorageContainerManager.class); @@ -118,14 +118,14 @@ public void setUp() throws Exception { ozoneStorageContainerManager.getContainerManager(); ReconPipelineManager reconPipelineManager = (ReconPipelineManager) ozoneStorageContainerManager.getPipelineManager(); - ContainerHealthSchemaManager containerHealthSchemaManager = - reconTestInjector.getInstance(ContainerHealthSchemaManager.class); + ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2 = + reconTestInjector.getInstance(ContainerHealthSchemaManagerV2.class); ReconGlobalStatsManager reconGlobalStatsManager = reconTestInjector.getInstance(ReconGlobalStatsManager.class); conf = mock(OzoneConfiguration.class); clusterStateEndpoint = new ClusterStateEndpoint(ozoneStorageContainerManager, - reconGlobalStatsManager, containerHealthSchemaManager, conf); + reconGlobalStatsManager, containerHealthSchemaManagerV2, conf); pipeline = getRandomPipeline(); pipelineID = pipeline.getId(); reconPipelineManager.addPipeline(pipeline); @@ -177,8 +177,8 @@ public void testStorageReportIsClusterStorageReport() { ReconNodeManager mockNodeManager = mock(ReconNodeManager.class); ReconPipelineManager mockPipelineManager = mock(ReconPipelineManager.class); ReconContainerManager mockContainerManager = mock(ReconContainerManager.class); - ContainerHealthSchemaManager mockContainerHealthSchemaManager = - mock(ContainerHealthSchemaManager.class); + ContainerHealthSchemaManagerV2 mockContainerHealthSchemaManagerV2 = + mock(ContainerHealthSchemaManagerV2.class); ReconGlobalStatsManager mockGlobalStatsManager = mock(ReconGlobalStatsManager.class); OzoneConfiguration mockConf = mock(OzoneConfiguration.class); @@ -194,8 +194,8 @@ public void testStorageReportIsClusterStorageReport() { .thenReturn(0); when(mockContainerManager.getContainerStateCount(HddsProtos.LifeCycleState.DELETED)) .thenReturn(0); - when(mockContainerHealthSchemaManager.getUnhealthyContainers( - any(), anyLong(), any(), anyInt())).thenReturn(Collections.emptyList()); + when(mockContainerHealthSchemaManagerV2.getUnhealthyContainers( + any(), anyLong(), anyLong(), anyInt())).thenReturn(Collections.emptyList()); SCMNodeStat scmNodeStat = new SCMNodeStat( 1000L, 400L, 600L, 300L, 50L, 20L); @@ -209,7 +209,7 @@ public void testStorageReportIsClusterStorageReport() { .thenReturn(new SpaceUsageSource.Fixed(2000L, 1500L, 500L)); ClusterStateEndpoint endpoint = new ClusterStateEndpoint( - mockScm, mockGlobalStatsManager, mockContainerHealthSchemaManager, mockConf); + mockScm, mockGlobalStatsManager, mockContainerHealthSchemaManagerV2, mockConf); ClusterStateResponse response = (ClusterStateResponse) endpoint.getClusterState().getEntity(); 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..55d3ec037170 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 @@ -97,7 +97,7 @@ import org.apache.hadoop.ozone.recon.api.types.MissingContainersResponse; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; @@ -113,8 +113,7 @@ import org.apache.hadoop.ozone.recon.tasks.ContainerKeyMapperTaskOBS; 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.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; import org.apache.ozone.test.tag.Flaky; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -141,7 +140,7 @@ public class TestContainerEndpoint { private ReconContainerMetadataManager reconContainerMetadataManager; private ContainerEndpoint containerEndpoint; private boolean isSetupDone = false; - private ContainerHealthSchemaManager containerHealthSchemaManager; + private ContainerHealthSchemaManagerV2 containerHealthSchemaManager; private ReconOMMetadataManager reconOMMetadataManager; private OzoneConfiguration omConfiguration; @@ -198,7 +197,7 @@ private void initializeInjector() throws Exception { .addBinding(StorageContainerServiceProvider.class, mock(StorageContainerServiceProviderImpl.class)) .addBinding(ContainerEndpoint.class) - .addBinding(ContainerHealthSchemaManager.class) + .addBinding(ContainerHealthSchemaManagerV2.class) .build(); OzoneStorageContainerManager ozoneStorageContainerManager = @@ -211,7 +210,7 @@ private void initializeInjector() throws Exception { reconTestInjector.getInstance(ReconContainerMetadataManager.class); containerEndpoint = reconTestInjector.getInstance(ContainerEndpoint.class); containerHealthSchemaManager = - reconTestInjector.getInstance(ContainerHealthSchemaManager.class); + reconTestInjector.getInstance(ContainerHealthSchemaManagerV2.class); this.reconNamespaceSummaryManager = reconTestInjector.getInstance(ReconNamespaceSummaryManager.class); @@ -950,8 +949,8 @@ public void testUnhealthyContainers() throws IOException, TimeoutException { 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 emptyMissing = "EMPTY_MISSING"; + String negativeSize = "NEGATIVE_SIZE"; // 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 ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2( + 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/TestEndpoints.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java index 07dd11d8021e..a543e076085f 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java @@ -123,7 +123,7 @@ import org.apache.hadoop.ozone.recon.api.types.VolumesResponse; import org.apache.hadoop.ozone.recon.common.ReconTestUtils; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconPipelineManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; @@ -280,7 +280,7 @@ private void initializeInjector() throws Exception { .addBinding(VolumeEndpoint.class) .addBinding(BucketEndpoint.class) .addBinding(MetricsServiceProviderFactory.class) - .addBinding(ContainerHealthSchemaManager.class) + .addBinding(ContainerHealthSchemaManagerV2.class) .addBinding(UtilizationEndpoint.class) .addBinding(ReconUtils.class, reconUtilsMock) .addBinding(StorageContainerLocationProtocol.class, mockScmClient) @@ -309,11 +309,11 @@ private void initializeInjector() throws Exception { omTableInsightTask = new OmTableInsightTask(reconGlobalStatsManager, reconOMMetadataManager); - ContainerHealthSchemaManager containerHealthSchemaManager = - reconTestInjector.getInstance(ContainerHealthSchemaManager.class); + ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2 = + reconTestInjector.getInstance(ContainerHealthSchemaManagerV2.class); clusterStateEndpoint = new ClusterStateEndpoint(reconScm, reconGlobalStatsManager, - containerHealthSchemaManager, mock(OzoneConfiguration.class)); + containerHealthSchemaManagerV2, mock(OzoneConfiguration.class)); containerSizeCountTask = reconScm.getContainerSizeCountTask(); MetricsServiceProviderFactory metricsServiceProviderFactory = reconTestInjector.getInstance(MetricsServiceProviderFactory.class); 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 deleted file mode 100644 index 4210756d1cd5..000000000000 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java +++ /dev/null @@ -1,804 +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.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.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. - */ -public class TestContainerHealthTask extends AbstractReconSqlDBTest { - - 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 = - 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()); - - 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; - } - - // 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); - } - - @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 = - 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; - }); - } - - 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 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 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 index e72789aca3a9..518fe3f0c192 100644 --- 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 @@ -25,16 +25,29 @@ import java.time.Clock; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +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.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.ContainerHealthSchemaManagerV2; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -80,18 +93,67 @@ public void setUp() throws Exception { when(scmContext.isInSafeMode()).thenReturn(false); // Create ReconReplicationManager - reconRM = new ReconReplicationManager( - new ReplicationManager.ReplicationManagerConfiguration(), - new OzoneConfiguration(), - containerManager, - placementPolicy, - placementPolicy, - new EventQueue(), - scmContext, - nodeManager, - Clock.system(ZoneId.systemDefault()), - schemaManagerV2 - ); + 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 @@ -170,4 +232,62 @@ public void testSchemaManagerIntegration() { // 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); + + 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(replicationConfig.getRequiredNodes()).thenReturn(requiredNodes); + return containerInfo; + } + + private ReconReplicationManager createStateInjectingReconRM( + long emptyMissingContainerId, + long negativeSizeContainerId) 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, + ReplicationQueue repQueue, ReplicationManagerReport report, + boolean readOnly) { + ReconReplicationManagerReport reconReport = + (ReconReplicationManagerReport) report; + if (containerInfo.getContainerID() == emptyMissingContainerId) { + reconReport.incrementAndSample(ContainerHealthState.MISSING, containerInfo); + return true; + } + if (containerInfo.getContainerID() == negativeSizeContainerId) { + reconReport.incrementAndSample( + ContainerHealthState.UNDER_REPLICATED, containerInfo); + return true; + } + return false; + } + }; + } } 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/TestSchemaVersionTableDefinition.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java index 3c01312c43bf..c7063da1d2c4 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java @@ -17,7 +17,7 @@ package org.apache.hadoop.ozone.recon.persistence; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; +import static org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UNHEALTHY_CONTAINERS_V2_TABLE_NAME; import static org.apache.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME; import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; import static org.apache.ozone.recon.schema.SqlDbUtils.listAllTables; @@ -189,7 +189,7 @@ public void testPreUpgradedClusterScenario() throws Exception { dropTable(connection, SCHEMA_VERSION_TABLE_NAME); if (listAllTables(connection).isEmpty()) { createTable(connection, GLOBAL_STATS_TABLE_NAME); - createTable(connection, UNHEALTHY_CONTAINERS_TABLE_NAME); + createTable(connection, UNHEALTHY_CONTAINERS_V2_TABLE_NAME); } // Initialize the schema @@ -231,7 +231,7 @@ public void testUpgradedClusterScenario() throws Exception { if (listAllTables(connection).isEmpty()) { // Create necessary tables to simulate the cluster state createTable(connection, GLOBAL_STATS_TABLE_NAME); - createTable(connection, UNHEALTHY_CONTAINERS_TABLE_NAME); + createTable(connection, UNHEALTHY_CONTAINERS_V2_TABLE_NAME); // Create the schema version table createSchemaVersionTable(connection); } diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java index 06be5d3fbae7..b7ebdcba1af2 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java @@ -58,7 +58,6 @@ import org.apache.hadoop.hdds.utils.db.DBStoreBuilder; import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.ozone.recon.ReconUtils; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; @@ -113,7 +112,6 @@ public void setUp(@TempDir File tempDir) throws Exception { ReconSCMDBDefinition.CONTAINERS.getTable(store), pipelineManager, getScmServiceProvider(), - mock(ContainerHealthSchemaManager.class), mock(ContainerHealthSchemaManagerV2.class), mock(ReconContainerMetadataManager.class), scmhaManager, diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java deleted file mode 100644 index 7c4c8e551229..000000000000 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hadoop.ozone.recon.upgrade; - -import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.name; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -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.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; -import org.jooq.DSLContext; -import org.jooq.impl.DSL; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Test class for InitialConstraintUpgradeAction. - */ -public class TestInitialConstraintUpgradeAction extends AbstractReconSqlDBTest { - - private InitialConstraintUpgradeAction upgradeAction; - private DSLContext dslContext; - private ReconStorageContainerManagerFacade mockScmFacade; - - @BeforeEach - public void setUp() throws SQLException { - // Initialize the DSLContext - dslContext = getDslContext(); - - // Initialize the upgrade action - upgradeAction = new InitialConstraintUpgradeAction(); - - // Mock the SCM facade to provide the DataSource - mockScmFacade = mock(ReconStorageContainerManagerFacade.class); - DataSource dataSource = getInjector().getInstance(DataSource.class); - when(mockScmFacade.getDataSource()).thenReturn(dataSource); - - // Set the DataSource and DSLContext directly - upgradeAction.setDslContext(dslContext); - - // Check if the table already exists - try (Connection conn = dataSource.getConnection()) { - DatabaseMetaData dbMetaData = conn.getMetaData(); - ResultSet tables = dbMetaData.getTables(null, null, UNHEALTHY_CONTAINERS_TABLE_NAME, null); - if (!tables.next()) { - // Create the initial table if it does not exist - dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME) - .column("container_id", org.jooq.impl.SQLDataType.BIGINT - .nullable(false)) - .column("container_state", org.jooq.impl.SQLDataType.VARCHAR(16) - .nullable(false)) - .constraint(DSL.constraint("pk_container_id") - .primaryKey("container_id", "container_state")) - .execute(); - } - } - } - - @Test - public void testUpgradeAppliesConstraintModificationForAllStates() throws SQLException { - // Run the upgrade action - upgradeAction.execute(mockScmFacade.getDataSource()); - - // Iterate over all valid states and insert records - for (ContainerSchemaDefinition.UnHealthyContainerStates state : - ContainerSchemaDefinition.UnHealthyContainerStates.values()) { - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values( - System.currentTimeMillis(), // Unique container_id for each record - state.name(), System.currentTimeMillis(), 3, 2, 1, "Replica count mismatch" - ) - .execute(); - } - - // Verify that the number of inserted records matches the number of enum values - int count = dslContext.fetchCount(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)); - assertEquals(ContainerSchemaDefinition.UnHealthyContainerStates.values().length, - count, "Expected one record for each valid state"); - - // Try inserting an invalid state (should fail due to constraint) - assertThrows(org.jooq.exception.DataAccessException.class, () -> - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values(999L, "INVALID_STATE", System.currentTimeMillis(), 3, 2, 1, - "Invalid state test").execute(), - "Inserting an invalid container_state should fail due to the constraint"); - } - - @Test - public void testInsertionWithNullContainerState() { - assertThrows(org.jooq.exception.DataAccessException.class, () -> { - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values( - 100L, // container_id - null, // container_state is NULL - System.currentTimeMillis(), 3, 2, 1, "Testing NULL state" - ) - .execute(); - }, "Inserting a NULL container_state should fail due to the NOT NULL constraint"); - } - - @Test - public void testDuplicatePrimaryKeyInsertion() throws SQLException { - // Insert the first record - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values(200L, "MISSING", System.currentTimeMillis(), 3, 2, 1, "First insertion" - ) - .execute(); - - // Try inserting a duplicate record with the same primary key - assertThrows(org.jooq.exception.DataAccessException.class, () -> { - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values(200L, "MISSING", System.currentTimeMillis(), 3, 2, 1, "Duplicate insertion" - ) - .execute(); - }, "Inserting a duplicate primary key should fail due to the primary key constraint"); - } - -} From 40e0ed918afec326ac0cf621f3506da59b8b3db7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 20 Feb 2026 12:18:26 +0530 Subject: [PATCH 22/43] HDDS-13891. Fixed robot test failure. --- .../dist/src/main/smoketest/recon/recon-taskstatus.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot b/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot index d3473b9aff0e..49a0df2ffe6b 100644 --- a/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot +++ b/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot @@ -28,7 +28,7 @@ Suite Setup Get Security Enabled From Config ${BASE_URL} http://recon:9888 ${TASK_STATUS_ENDPOINT} ${BASE_URL}/api/v1/task/status ${TRIGGER_SYNC_ENDPOINT} ${BASE_URL}/api/v1/triggerdbsync/om -${TASK_NAME_1} ContainerHealthTask +${TASK_NAME_1} ContainerHealthTaskV2 ${TASK_NAME_2} OmDeltaRequest ${BUCKET} testbucket ${VOLUME} testvolume From 1ae5f2f5b8aceb2d4250d5965da448dca9fc5d6e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 20 Feb 2026 16:32:21 +0530 Subject: [PATCH 23/43] HDDS-13891. Fixed test failure. --- .../common/src/main/resources/ozone-default.xml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml index 76ae41404ad1..e9c3f2a6237a 100644 --- a/hadoop-hdds/common/src/main/resources/ozone-default.xml +++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml @@ -4506,19 +4506,6 @@
- - ozone.recon.container.health.use.scm.report - false - OZONE, MANAGEMENT, RECON - - Feature flag to enable ContainerHealthTaskV2 which uses SCM's ReplicationManager - as the single source of truth for container health states. When enabled, V2 task - will sync container health states directly from SCM instead of computing them locally. - This provides more accurate and consistent container health reporting. - Default is false (uses legacy implementation). - - - ozone.om.snapshot.compaction.dag.max.time.allowed 30d From 62bdef00c265ed9f78108461240692246e48a6b8 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sat, 21 Feb 2026 20:32:32 +0530 Subject: [PATCH 24/43] HDDS-13891. Fixed review comments for refactoring and test code. --- .../recon/TestReconTasksV2MultiNode.java | 206 ++++++--------- .../ozone/recon/api/ContainerEndpoint.java | 58 +++-- .../recon/fsck/ContainerHealthTaskV2.java | 35 ++- .../recon/fsck/ReconReplicationManager.java | 245 +++++++++++------- .../metrics/ContainerHealthTaskV2Metrics.java | 75 ++++++ .../ContainerHealthSchemaManagerV2.java | 126 +++++---- 6 files changed, 444 insertions(+), 301 deletions(-) create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java index 4466a78c0150..fb77b894a7c7 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -33,6 +33,9 @@ import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; import org.apache.ozone.test.LambdaTestUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** @@ -44,6 +47,55 @@ */ public class TestReconTasksV2MultiNode { + private static MiniOzoneCluster cluster; + private static ReconService reconService; + private static ReconStorageContainerManagerFacade reconScm; + private static ReconContainerManager reconContainerManager; + private static PipelineManager reconPipelineManager; + + @BeforeAll + public static void setupCluster() throws Exception { + OzoneConfiguration testConf = new OzoneConfiguration(); + testConf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); + testConf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); + + ReconTaskConfig taskConfig = testConf.getObject(ReconTaskConfig.class); + taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); + testConf.setFromObject(taskConfig); + + testConf.set("ozone.scm.stale.node.interval", "6s"); + testConf.set("ozone.scm.dead.node.interval", "8s"); + + reconService = new ReconService(testConf); + cluster = MiniOzoneCluster.newBuilder(testConf) + .setNumDatanodes(3) + .addService(reconService) + .build(); + + cluster.waitForClusterToBeReady(); + + reconScm = (ReconStorageContainerManagerFacade) + reconService.getReconServer().getReconStorageContainerManager(); + reconPipelineManager = reconScm.getPipelineManager(); + reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); + } + + @BeforeEach + public void cleanupBeforeEach() throws Exception { + // Ensure each test starts from a clean unhealthy-container table. + reconContainerManager.getContainerSchemaManagerV2().clearAllUnhealthyContainerRecords(); + // Ensure Recon has initialized pipeline state before assertions. + LambdaTestUtils.await(60000, 5000, + () -> (!reconPipelineManager.getPipelines().isEmpty())); + } + + @AfterAll + public static void shutdownCluster() { + if (cluster != null) { + cluster.shutdown(); + } + } + /** * Test that ContainerHealthTaskV2 can query UNDER_REPLICATED containers. * Steps: @@ -75,57 +127,17 @@ public class TestReconTasksV2MultiNode { */ @Test public void testContainerHealthTaskV2UnderReplicated() throws Exception { - // Create a cluster with 3 datanodes - OzoneConfiguration testConf = new OzoneConfiguration(); - testConf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); - testConf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); - - ReconTaskConfig taskConfig = testConf.getObject(ReconTaskConfig.class); - taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); - testConf.setFromObject(taskConfig); - - testConf.set("ozone.scm.stale.node.interval", "6s"); - testConf.set("ozone.scm.dead.node.interval", "8s"); - - ReconService testRecon = new ReconService(testConf); - MiniOzoneCluster testCluster = MiniOzoneCluster.newBuilder(testConf) - .setNumDatanodes(3) - .addService(testRecon) - .build(); - - try { - testCluster.waitForClusterToBeReady(); - testCluster.waitForPipelineTobeReady( - HddsProtos.ReplicationFactor.THREE, 60000); + cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.THREE, 60000); - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - testRecon.getReconServer().getReconStorageContainerManager(); + // Verify the query mechanism for UNDER_REPLICATED state works + List underReplicatedContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.UNDER_REPLICATED, + 0L, 0L, 1000); - PipelineManager reconPipelineManager = reconScm.getPipelineManager(); - - // Make sure Recon's pipeline state is initialized - LambdaTestUtils.await(60000, 5000, - () -> (!reconPipelineManager.getPipelines().isEmpty())); - - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); - - // Verify the query mechanism for UNDER_REPLICATED state works - List underReplicatedContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.UNDER_REPLICATED, - 0L, 0L, 1000); - - // Should be empty in normal operation (all replicas healthy) - assertEquals(0, underReplicatedContainers.size()); - - } finally { - if (testCluster != null) { - testCluster.shutdown(); - } - } + // Should be empty in normal operation (all replicas healthy) + assertEquals(0, underReplicatedContainers.size()); } /** @@ -143,80 +155,26 @@ public void testContainerHealthTaskV2UnderReplicated() throws Exception { */ @Test public void testContainerHealthTaskV2OverReplicated() throws Exception { - // Create a cluster with 3 datanodes - OzoneConfiguration testConf = new OzoneConfiguration(); - testConf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); - testConf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); - - ReconTaskConfig taskConfig = testConf.getObject(ReconTaskConfig.class); - taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); - testConf.setFromObject(taskConfig); - - testConf.set("ozone.scm.stale.node.interval", "6s"); - testConf.set("ozone.scm.dead.node.interval", "8s"); - - ReconService testRecon = new ReconService(testConf); - MiniOzoneCluster testCluster = MiniOzoneCluster.newBuilder(testConf) - .setNumDatanodes(3) - .addService(testRecon) - .build(); - - try { - testCluster.waitForClusterToBeReady(); - testCluster.waitForPipelineTobeReady( - HddsProtos.ReplicationFactor.ONE, 60000); - - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - testRecon.getReconServer().getReconStorageContainerManager(); - - PipelineManager reconPipelineManager = reconScm.getPipelineManager(); - - // Make sure Recon's pipeline state is initialized - LambdaTestUtils.await(60000, 5000, - () -> (!reconPipelineManager.getPipelines().isEmpty())); - - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); - - // Note: Creating over-replication in integration tests is challenging - // as it requires artificially adding extra replicas. In production, - // over-replication can occur when: - // 1. A dead datanode comes back online with old replicas - // 2. Replication commands create extra replicas before cleanup - // 3. Manual intervention or bugs cause duplicate replicas - // - // For now, this test verifies the detection mechanism exists. - // If over-replication is detected in the future, the V2 table - // should contain the record with proper replica counts. - - // The actual over-replication detection would look like this: - // LambdaTestUtils.await(120000, 6000, () -> { - // List overReplicatedContainers = - // reconContainerManager.getContainerSchemaManagerV2() - // .getUnhealthyContainers( - // ContainerSchemaDefinitionV2.UnHealthyContainerStates.OVER_REPLICATED, - // 0L, 0L, 1000); - // if (!overReplicatedContainers.isEmpty()) { - // UnhealthyContainerRecordV2 record = overReplicatedContainers.get(0); - // return record.getActualReplicaCount() > record.getExpectedReplicaCount(); - // } - // return false; - // }); - - // For now, just verify that the query mechanism works - List overReplicatedContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.OVER_REPLICATED, - 0L, 0L, 1000); - // Should be empty in normal operation - assertEquals(0, overReplicatedContainers.size()); - - } finally { - if (testCluster != null) { - testCluster.shutdown(); - } - } + cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.ONE, 60000); + + // Note: Creating over-replication in integration tests is challenging + // as it requires artificially adding extra replicas. In production, + // over-replication can occur when: + // 1. A dead datanode comes back online with old replicas + // 2. Replication commands create extra replicas before cleanup + // 3. Manual intervention or bugs cause duplicate replicas + // + // For now, this test verifies the detection mechanism exists. + // If over-replication is detected in the future, the V2 table + // should contain the record with proper replica counts. + + // For now, just verify that the query mechanism works + List overReplicatedContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinitionV2.UnHealthyContainerStates.OVER_REPLICATED, + 0L, 0L, 1000); + // Should be empty in normal operation + assertEquals(0, overReplicatedContainers.size()); } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 61b7533c7f48..9d5ab31ed6f2 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -27,6 +27,7 @@ import static org.apache.hadoop.ozone.recon.ReconConstants.RECON_QUERY_PREVKEY; import java.io.IOException; +import java.io.UncheckedIOException; import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; @@ -424,31 +425,11 @@ private Response getUnhealthyContainersV2( List v2Containers = containerHealthSchemaManagerV2.getUnhealthyContainers(v2State, minContainerId, maxContainerId, limit); - // Convert V2 records to response format - for (ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2 c : v2Containers) { - long containerID = c.getContainerId(); - ContainerInfo containerInfo = - containerManager.getContainer(ContainerID.valueOf(containerID)); - long keyCount = containerInfo.getNumberOfKeys(); - UUID pipelineID = containerInfo.getPipelineID().getId(); - List datanodes = - containerManager.getLatestContainerHistory(containerID, - containerInfo.getReplicationConfig().getRequiredNodes()); - - unhealthyMeta.add(new UnhealthyContainerMetadata( - c.getContainerId(), - c.getContainerState(), - c.getInStateSince(), - c.getExpectedReplicaCount(), - c.getActualReplicaCount(), - c.getReplicaDelta(), - c.getReason(), - datanodes, - pipelineID, - keyCount)); - } - } catch (IOException ex) { - throw new WebApplicationException(ex, + unhealthyMeta = v2Containers.stream() + .map(this::toUnhealthyMetadata) + .collect(Collectors.toList()); + } catch (UncheckedIOException ex) { + throw new WebApplicationException(ex.getCause(), Response.Status.INTERNAL_SERVER_ERROR); } catch (IllegalArgumentException e) { throw new WebApplicationException(e, Response.Status.BAD_REQUEST); @@ -468,6 +449,33 @@ private Response getUnhealthyContainersV2( return Response.ok(response).build(); } + private UnhealthyContainerMetadata toUnhealthyMetadata( + ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2 record) { + try { + long containerID = record.getContainerId(); + ContainerInfo containerInfo = + containerManager.getContainer(ContainerID.valueOf(containerID)); + long keyCount = containerInfo.getNumberOfKeys(); + UUID pipelineID = containerInfo.getPipelineID().getId(); + List datanodes = + containerManager.getLatestContainerHistory(containerID, + containerInfo.getReplicationConfig().getRequiredNodes()); + return new UnhealthyContainerMetadata( + record.getContainerId(), + record.getContainerState(), + record.getInStateSince(), + record.getExpectedReplicaCount(), + record.getActualReplicaCount(), + record.getReplicaDelta(), + record.getReason(), + datanodes, + pipelineID, + keyCount); + } catch (IOException ioEx) { + throw new UncheckedIOException(ioEx); + } + } + /** * Return * {@link org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 4305a0aa6925..331745aa813d 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -18,10 +18,12 @@ package org.apache.hadoop.ozone.recon.fsck; import javax.inject.Inject; +import org.apache.hadoop.ozone.recon.metrics.ContainerHealthTaskV2Metrics; import org.apache.hadoop.ozone.recon.scm.ReconScmTask; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; import org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdaterManager; +import org.apache.hadoop.util.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +58,7 @@ public class ContainerHealthTaskV2 extends ReconScmTask { private final ReconStorageContainerManagerFacade reconScm; private final long interval; + private final ContainerHealthTaskV2Metrics metrics; @Inject public ContainerHealthTaskV2( @@ -65,6 +68,7 @@ public ContainerHealthTaskV2( super(taskStatusUpdaterManager); this.reconScm = reconScm; this.interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); + this.metrics = ContainerHealthTaskV2Metrics.create(); LOG.info("Initialized ContainerHealthTaskV2 with Local ReplicationManager, interval={}ms", interval); } @@ -72,12 +76,13 @@ public ContainerHealthTaskV2( @Override protected void run() { while (canRun()) { + long cycleStart = Time.monotonicNow(); try { initializeAndRunTask(); - - // Wait before next run using configured interval - synchronized (this) { - wait(interval); + long elapsed = Time.monotonicNow() - cycleStart; + long sleepMs = Math.max(0, interval - elapsed); + if (sleepMs > 0) { + Thread.sleep(sleepMs); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -101,6 +106,7 @@ protected void run() { */ @Override protected void runTask() throws Exception { + long start = Time.monotonicNow(); LOG.info("ContainerHealthTaskV2 starting - using local ReplicationManager"); // Get Recon's ReplicationManager (actually a ReconReplicationManager instance) @@ -112,8 +118,25 @@ protected void runTask() throws Exception { // 1. Runs health checks on all containers using inherited SCM logic // 2. Captures ALL unhealthy containers (no sampling) // 3. Stores all health states in database - reconRM.processAll(); + boolean succeeded = false; + try { + reconRM.processAll(); + metrics.incrSuccess(); + succeeded = true; + } catch (Exception e) { + metrics.incrFailure(); + throw e; + } finally { + long durationMs = Time.monotonicNow() - start; + metrics.addRunTime(durationMs); + LOG.info("ContainerHealthTaskV2 completed with status={} in {} ms", + succeeded ? "success" : "failure", durationMs); + } + } - LOG.info("ContainerHealthTaskV2 completed successfully"); + @Override + public synchronized void stop() { + super.stop(); + metrics.unRegister(); } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 43e3d9fa37af..67a9deced15b 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -27,6 +27,7 @@ import org.apache.hadoop.hdds.conf.ConfigurationSource; import org.apache.hadoop.hdds.scm.PlacementPolicy; import org.apache.hadoop.hdds.scm.container.ContainerID; +import org.apache.hadoop.hdds.scm.container.ContainerHealthState; import org.apache.hadoop.hdds.scm.container.ContainerInfo; import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; @@ -39,6 +40,7 @@ import org.apache.hadoop.hdds.server.events.EventPublisher; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.util.Time; import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -281,7 +283,7 @@ private boolean hasDataChecksumMismatch(Set replicas) { public synchronized void processAll() { LOG.info("ReconReplicationManager starting container health check"); - final long startTime = System.currentTimeMillis(); + final long startTime = Time.monotonicNow(); // Use extended report that captures ALL containers, not just 100 samples final ReconReplicationManagerReport report = new ReconReplicationManagerReport(); @@ -325,7 +327,7 @@ public synchronized void processAll() { // Store ALL per-container health states to database storeHealthStatesToDatabase(report, containers); - long duration = System.currentTimeMillis() - startTime; + long duration = Time.monotonicNow() - startTime; LOG.info("ReconReplicationManager completed in {}ms for {} containers", duration, containers.size()); } @@ -343,93 +345,108 @@ private void storeHealthStatesToDatabase( long currentTime = System.currentTimeMillis(); List recordsToInsert = new ArrayList<>(); - List containerIdsToDelete = new ArrayList<>(); - final int[] missingCount = {0}; - final int[] underRepCount = {0}; - final int[] overRepCount = {0}; - final int[] misRepCount = {0}; - final int[] emptyMissingCount = {0}; - final int[] negativeSizeCount = {0}; + List containerIdsToDelete = collectContainerIds(allContainers); + ProcessingStats stats = new ProcessingStats(); Set negativeSizeRecorded = new HashSet<>(); report.forEachContainerByState((state, cid) -> { try { - switch (state) { - case MISSING: - ContainerInfo missingContainer = containerManager.getContainer(cid); - if (isEmptyMissing(missingContainer)) { - emptyMissingCount[0]++; - int missingExpected = missingContainer.getReplicationConfig().getRequiredNodes(); - recordsToInsert.add(createRecord(missingContainer, - UnHealthyContainerStates.EMPTY_MISSING, currentTime, missingExpected, 0, - "Container has no replicas and no keys")); - break; - } - missingCount[0]++; - int nonEmptyMissingExpected = - missingContainer.getReplicationConfig().getRequiredNodes(); - recordsToInsert.add(createRecord(missingContainer, - UnHealthyContainerStates.MISSING, currentTime, nonEmptyMissingExpected, 0, - "No replicas available")); - break; - case UNDER_REPLICATED: - underRepCount[0]++; - ContainerInfo underRepContainer = containerManager.getContainer(cid); - Set underReplicas = containerManager.getContainerReplicas(cid); - int underRepExpected = underRepContainer.getReplicationConfig().getRequiredNodes(); - int underRepActual = underReplicas.size(); - recordsToInsert.add(createRecord(underRepContainer, - UnHealthyContainerStates.UNDER_REPLICATED, currentTime, - underRepExpected, underRepActual, - "Insufficient replicas")); - addNegativeSizeRecordIfNeeded(underRepContainer, currentTime, underRepActual, recordsToInsert, - negativeSizeRecorded, negativeSizeCount); - break; - case OVER_REPLICATED: - overRepCount[0]++; - ContainerInfo overRepContainer = containerManager.getContainer(cid); - Set overReplicas = containerManager.getContainerReplicas(cid); - int overRepExpected = overRepContainer.getReplicationConfig().getRequiredNodes(); - int overRepActual = overReplicas.size(); - recordsToInsert.add(createRecord(overRepContainer, - UnHealthyContainerStates.OVER_REPLICATED, currentTime, - overRepExpected, overRepActual, - "Excess replicas")); - addNegativeSizeRecordIfNeeded(overRepContainer, currentTime, overRepActual, recordsToInsert, - negativeSizeRecorded, negativeSizeCount); - break; - case MIS_REPLICATED: - misRepCount[0]++; - ContainerInfo misRepContainer = containerManager.getContainer(cid); - Set misReplicas = containerManager.getContainerReplicas(cid); - int misRepExpected = misRepContainer.getReplicationConfig().getRequiredNodes(); - int misRepActual = misReplicas.size(); - recordsToInsert.add(createRecord(misRepContainer, - UnHealthyContainerStates.MIS_REPLICATED, currentTime, misRepExpected, misRepActual, - "Placement policy violated")); - addNegativeSizeRecordIfNeeded(misRepContainer, currentTime, misRepActual, recordsToInsert, - negativeSizeRecorded, negativeSizeCount); - break; - default: - break; - } + handleScmStateContainer(state, cid, currentTime, recordsToInsert, + negativeSizeRecorded, stats); } catch (ContainerNotFoundException e) { LOG.warn("Container {} not found when processing {} state", cid, state, e); } }); - LOG.info("Processing health states: MISSING={}, EMPTY_MISSING={}, " + - "UNDER_REPLICATED={}, OVER_REPLICATED={}, MIS_REPLICATED={}, " + - "NEGATIVE_SIZE={}, REPLICA_MISMATCH={}", - missingCount[0], - emptyMissingCount[0], - underRepCount[0], - overRepCount[0], - misRepCount[0], - negativeSizeCount[0], - report.getReplicaMismatchCount()); - - // Process REPLICA_MISMATCH containers (Recon-specific) + logProcessingStats(stats, report.getReplicaMismatchCount()); + + int replicaMismatchCount = processReplicaMismatchContainers( + report, currentTime, recordsToInsert); + persistUnhealthyRecords(containerIdsToDelete, recordsToInsert); + + LOG.info("Stored {} MISSING, {} EMPTY_MISSING, {} UNDER_REPLICATED, " + + "{} OVER_REPLICATED, {} MIS_REPLICATED, {} NEGATIVE_SIZE, " + + "{} REPLICA_MISMATCH", + stats.missingCount, stats.emptyMissingCount, stats.underRepCount, + stats.overRepCount, stats.misRepCount, stats.negativeSizeCount, + replicaMismatchCount); + } + + private void handleScmStateContainer( + ContainerHealthState state, + ContainerID containerId, + long currentTime, + List recordsToInsert, + Set negativeSizeRecorded, + ProcessingStats stats) throws ContainerNotFoundException { + switch (state) { + case MISSING: + handleMissingContainer(containerId, currentTime, recordsToInsert, stats); + break; + case UNDER_REPLICATED: + handleReplicaStateContainer(containerId, currentTime, + UnHealthyContainerStates.UNDER_REPLICATED, "Insufficient replicas", + recordsToInsert, negativeSizeRecorded, stats::incrementUnderRepCount, stats); + break; + case OVER_REPLICATED: + handleReplicaStateContainer(containerId, currentTime, + UnHealthyContainerStates.OVER_REPLICATED, "Excess replicas", + recordsToInsert, negativeSizeRecorded, stats::incrementOverRepCount, stats); + break; + case MIS_REPLICATED: + handleReplicaStateContainer(containerId, currentTime, + UnHealthyContainerStates.MIS_REPLICATED, "Placement policy violated", + recordsToInsert, negativeSizeRecorded, stats::incrementMisRepCount, stats); + break; + default: + break; + } + } + + private void handleMissingContainer( + ContainerID containerId, + long currentTime, + List recordsToInsert, + ProcessingStats stats) throws ContainerNotFoundException { + ContainerInfo container = containerManager.getContainer(containerId); + int expected = container.getReplicationConfig().getRequiredNodes(); + if (isEmptyMissing(container)) { + stats.incrementEmptyMissingCount(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.EMPTY_MISSING, currentTime, expected, 0, + "Container has no replicas and no keys")); + return; + } + + stats.incrementMissingCount(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.MISSING, currentTime, expected, 0, + "No replicas available")); + } + + private void handleReplicaStateContainer( + ContainerID containerId, + long currentTime, + UnHealthyContainerStates targetState, + String reason, + List recordsToInsert, + Set negativeSizeRecorded, + Runnable counterIncrementer, + ProcessingStats stats) throws ContainerNotFoundException { + counterIncrementer.run(); + ContainerInfo container = containerManager.getContainer(containerId); + Set replicas = containerManager.getContainerReplicas(containerId); + int expected = container.getReplicationConfig().getRequiredNodes(); + int actual = replicas.size(); + recordsToInsert.add(createRecord(container, targetState, currentTime, expected, actual, reason)); + addNegativeSizeRecordIfNeeded(container, currentTime, actual, recordsToInsert, + negativeSizeRecorded, stats); + } + + private int processReplicaMismatchContainers( + ReconReplicationManagerReport report, + long currentTime, + List recordsToInsert) { List replicaMismatchContainers = report.getReplicaMismatchContainers(); for (ContainerID cid : replicaMismatchContainers) { try { @@ -444,25 +461,38 @@ private void storeHealthStatesToDatabase( LOG.warn("Container {} not found when processing REPLICA_MISMATCH state", cid, e); } } + return replicaMismatchContainers.size(); + } - // Collect all container IDs for SCM state deletion + private List collectContainerIds(List allContainers) { + List containerIds = new ArrayList<>(allContainers.size()); for (ContainerInfo container : allContainers) { - containerIdsToDelete.add(container.getContainerID()); + containerIds.add(container.getContainerID()); } + return containerIds; + } - // Batch delete old states, then batch insert new states + private void persistUnhealthyRecords( + List containerIdsToDelete, + List recordsToInsert) { LOG.info("Deleting SCM states for {} containers", containerIdsToDelete.size()); healthSchemaManager.batchDeleteSCMStatesForContainers(containerIdsToDelete); LOG.info("Inserting {} unhealthy container records", recordsToInsert.size()); healthSchemaManager.insertUnhealthyContainerRecords(recordsToInsert); + } - LOG.info("Stored {} MISSING, {} EMPTY_MISSING, {} UNDER_REPLICATED, " + - "{} OVER_REPLICATED, {} MIS_REPLICATED, {} NEGATIVE_SIZE, " + - "{} REPLICA_MISMATCH", - missingCount[0], emptyMissingCount[0], underRepCount[0], - overRepCount[0], misRepCount[0], negativeSizeCount[0], - replicaMismatchContainers.size()); + private void logProcessingStats(ProcessingStats stats, int replicaMismatchCount) { + LOG.info("Processing health states: MISSING={}, EMPTY_MISSING={}, " + + "UNDER_REPLICATED={}, OVER_REPLICATED={}, MIS_REPLICATED={}, " + + "NEGATIVE_SIZE={}, REPLICA_MISMATCH={}", + stats.missingCount, + stats.emptyMissingCount, + stats.underRepCount, + stats.overRepCount, + stats.misRepCount, + stats.negativeSizeCount, + replicaMismatchCount); } private boolean isEmptyMissing(ContainerInfo container) { @@ -479,14 +509,47 @@ private void addNegativeSizeRecordIfNeeded( int actualReplicaCount, List recordsToInsert, Set negativeSizeRecorded, - int[] negativeSizeCount) { + ProcessingStats stats) { if (isNegativeSize(container) && negativeSizeRecorded.add(container.getContainerID())) { int expected = container.getReplicationConfig().getRequiredNodes(); recordsToInsert.add(createRecord(container, UnHealthyContainerStates.NEGATIVE_SIZE, currentTime, expected, actualReplicaCount, "Container reports negative usedBytes")); - negativeSizeCount[0]++; + stats.incrementNegativeSizeCount(); + } + } + + private static final class ProcessingStats { + private int missingCount; + private int underRepCount; + private int overRepCount; + private int misRepCount; + private int emptyMissingCount; + private int negativeSizeCount; + + void incrementMissingCount() { + missingCount++; + } + + void incrementUnderRepCount() { + underRepCount++; + } + + void incrementOverRepCount() { + overRepCount++; + } + + void incrementMisRepCount() { + misRepCount++; + } + + void incrementEmptyMissingCount() { + emptyMissingCount++; + } + + void incrementNegativeSizeCount() { + negativeSizeCount++; } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java new file mode 100644 index 000000000000..2449f21f8604 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java @@ -0,0 +1,75 @@ +/* + * 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.metrics; + +import org.apache.hadoop.hdds.annotation.InterfaceAudience; +import org.apache.hadoop.metrics2.MetricsSystem; +import org.apache.hadoop.metrics2.annotation.Metric; +import org.apache.hadoop.metrics2.annotation.Metrics; +import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem; +import org.apache.hadoop.metrics2.lib.MutableCounterLong; +import org.apache.hadoop.metrics2.lib.MutableRate; +import org.apache.hadoop.ozone.OzoneConsts; + +/** + * Runtime metrics for ContainerHealthTaskV2 execution. + */ +@InterfaceAudience.Private +@Metrics(about = "ContainerHealthTaskV2 Metrics", context = OzoneConsts.OZONE) +public final class ContainerHealthTaskV2Metrics { + + private static final String SOURCE_NAME = + ContainerHealthTaskV2Metrics.class.getSimpleName(); + + @Metric(about = "ContainerHealthTaskV2 runtime in milliseconds") + private MutableRate runTimeMs; + + @Metric(about = "ContainerHealthTaskV2 successful runs") + private MutableCounterLong runSuccessCount; + + @Metric(about = "ContainerHealthTaskV2 failed runs") + private MutableCounterLong runFailureCount; + + private ContainerHealthTaskV2Metrics() { + } + + public static ContainerHealthTaskV2Metrics create() { + MetricsSystem ms = DefaultMetricsSystem.instance(); + return ms.register( + SOURCE_NAME, + "ContainerHealthTaskV2 Metrics", + new ContainerHealthTaskV2Metrics()); + } + + public void unRegister() { + MetricsSystem ms = DefaultMetricsSystem.instance(); + ms.unregisterSource(SOURCE_NAME); + } + + public void addRunTime(long runtimeMs) { + runTimeMs.add(runtimeMs); + } + + public void incrSuccess() { + runSuccessCount.incr(); + } + + public void incrFailure() { + runFailureCount.incr(); + } +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index b1ed458adafd..e2453205cde4 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -81,28 +81,7 @@ public void insertUnhealthyContainerRecords(List rec DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); try { - // Try batch insert first in chunks to keep memory/transaction pressure bounded. - dslContext.transaction(configuration -> { - DSLContext txContext = configuration.dsl(); - - for (int from = 0; from < recs.size(); from += BATCH_INSERT_CHUNK_SIZE) { - int to = Math.min(from + BATCH_INSERT_CHUNK_SIZE, recs.size()); - List records = new ArrayList<>(to - from); - for (int i = from; i < to; i++) { - UnhealthyContainerRecordV2 rec = recs.get(i); - UnhealthyContainersV2Record record = txContext.newRecord(UNHEALTHY_CONTAINERS_V2); - 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()); - records.add(record); - } - txContext.batchInsert(records).execute(); - } - }); + batchInsertInChunks(dslContext, recs); LOG.debug("Batch inserted {} unhealthy container records", recs.size()); @@ -110,47 +89,84 @@ public void insertUnhealthyContainerRecords(List rec // Batch insert failed (likely duplicate key) - fall back to insert-or-update per record LOG.warn("Batch insert failed, falling back to individual insert-or-update for {} records", recs.size(), e); + fallbackInsertOrUpdate(recs); + } catch (Exception e) { + LOG.error("Failed to batch insert records into {}", UNHEALTHY_CONTAINERS_V2_TABLE_NAME, e); + throw new RuntimeException("Recon failed to insert " + recs.size() + + " unhealthy container records.", e); + } + } - try (Connection connection = containerSchemaDefinitionV2.getDataSource().getConnection()) { - connection.setAutoCommit(false); - try { - for (UnhealthyContainerRecordV2 rec : recs) { - UnhealthyContainersV2 jooqRec = new UnhealthyContainersV2( - rec.getContainerId(), - rec.getContainerState(), - rec.getInStateSince(), - rec.getExpectedReplicaCount(), - rec.getActualReplicaCount(), - rec.getReplicaDelta(), - rec.getReason()); - - try { - unhealthyContainersV2Dao.insert(jooqRec); - } catch (DataAccessException insertEx) { - // Duplicate key - update existing record - unhealthyContainersV2Dao.update(jooqRec); - } + private void batchInsertInChunks(DSLContext dslContext, + List recs) { + dslContext.transaction(configuration -> { + DSLContext txContext = configuration.dsl(); + List 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(txContext, recs.get(i))); + } + txContext.batchInsert(records).execute(); + } + }); + } + + private void fallbackInsertOrUpdate(List recs) { + try (Connection connection = containerSchemaDefinitionV2.getDataSource().getConnection()) { + connection.setAutoCommit(false); + try { + for (UnhealthyContainerRecordV2 rec : recs) { + UnhealthyContainersV2 jooqRec = toJooqPojo(rec); + try { + unhealthyContainersV2Dao.insert(jooqRec); + } catch (DataAccessException insertEx) { + // Duplicate key - update existing record + unhealthyContainersV2Dao.update(jooqRec); } - connection.commit(); - } catch (Exception innerEx) { - connection.rollback(); - LOG.error("Transaction rolled back during fallback insert", innerEx); - throw innerEx; - } finally { - connection.setAutoCommit(true); } - } catch (Exception fallbackEx) { - LOG.error("Failed to insert {} records even with fallback", recs.size(), fallbackEx); - throw new RuntimeException("Recon failed to insert " + recs.size() + - " unhealthy container records.", fallbackEx); + connection.commit(); + } catch (Exception innerEx) { + connection.rollback(); + LOG.error("Transaction rolled back during fallback insert", innerEx); + throw innerEx; + } finally { + connection.setAutoCommit(true); } - } catch (Exception e) { - LOG.error("Failed to batch insert records into {}", UNHEALTHY_CONTAINERS_V2_TABLE_NAME, e); + } catch (Exception fallbackEx) { + LOG.error("Failed to insert {} records even with fallback", recs.size(), fallbackEx); throw new RuntimeException("Recon failed to insert " + recs.size() + - " unhealthy container records.", e); + " unhealthy container records.", fallbackEx); } } + private UnhealthyContainersV2Record toJooqRecord(DSLContext txContext, + UnhealthyContainerRecordV2 rec) { + UnhealthyContainersV2Record record = txContext.newRecord(UNHEALTHY_CONTAINERS_V2); + 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; + } + + private UnhealthyContainersV2 toJooqPojo(UnhealthyContainerRecordV2 rec) { + return new UnhealthyContainersV2( + rec.getContainerId(), + rec.getContainerState(), + rec.getInStateSince(), + rec.getExpectedReplicaCount(), + rec.getActualReplicaCount(), + rec.getReplicaDelta(), + rec.getReason()); + } + /** * Batch delete SCM-tracked states for multiple containers. * This deletes all states generated from SCM/Recon health scans: From 14d1c3897ee61b7a66ffdec5d15de9994e5a3c82 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sat, 21 Feb 2026 20:53:59 +0530 Subject: [PATCH 25/43] HDDS-13891. Fixed checkstyle issues. --- .../ozone/recon/TestReconTasksV2MultiNode.java | 6 ++---- .../hadoop/ozone/recon/api/ContainerEndpoint.java | 2 +- .../ozone/recon/fsck/ReconReplicationManager.java | 13 +++++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java index fb77b894a7c7..21315f965a1a 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -48,8 +48,6 @@ public class TestReconTasksV2MultiNode { private static MiniOzoneCluster cluster; - private static ReconService reconService; - private static ReconStorageContainerManagerFacade reconScm; private static ReconContainerManager reconContainerManager; private static PipelineManager reconPipelineManager; @@ -66,7 +64,7 @@ public static void setupCluster() throws Exception { testConf.set("ozone.scm.stale.node.interval", "6s"); testConf.set("ozone.scm.dead.node.interval", "8s"); - reconService = new ReconService(testConf); + ReconService reconService = new ReconService(testConf); cluster = MiniOzoneCluster.newBuilder(testConf) .setNumDatanodes(3) .addService(reconService) @@ -74,7 +72,7 @@ public static void setupCluster() throws Exception { cluster.waitForClusterToBeReady(); - reconScm = (ReconStorageContainerManagerFacade) + ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) reconService.getReconServer().getReconStorageContainerManager(); reconPipelineManager = reconScm.getPipelineManager(); reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 9d5ab31ed6f2..afdcd5591886 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -403,7 +403,7 @@ private Response getUnhealthyContainersV2( int limit, long maxContainerId, long minContainerId) { - List unhealthyMeta = new ArrayList<>(); + List unhealthyMeta; List summary = new ArrayList<>(); try { diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 67a9deced15b..8050b1b039cb 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -26,8 +26,8 @@ import java.util.Set; import org.apache.hadoop.hdds.conf.ConfigurationSource; import org.apache.hadoop.hdds.scm.PlacementPolicy; -import org.apache.hadoop.hdds.scm.container.ContainerID; 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.ContainerNotFoundException; @@ -384,19 +384,22 @@ private void handleScmStateContainer( handleMissingContainer(containerId, currentTime, recordsToInsert, stats); break; case UNDER_REPLICATED: + stats.incrementUnderRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.UNDER_REPLICATED, "Insufficient replicas", - recordsToInsert, negativeSizeRecorded, stats::incrementUnderRepCount, stats); + recordsToInsert, negativeSizeRecorded, stats); break; case OVER_REPLICATED: + stats.incrementOverRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.OVER_REPLICATED, "Excess replicas", - recordsToInsert, negativeSizeRecorded, stats::incrementOverRepCount, stats); + recordsToInsert, negativeSizeRecorded, stats); break; case MIS_REPLICATED: + stats.incrementMisRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.MIS_REPLICATED, "Placement policy violated", - recordsToInsert, negativeSizeRecorded, stats::incrementMisRepCount, stats); + recordsToInsert, negativeSizeRecorded, stats); break; default: break; @@ -431,9 +434,7 @@ private void handleReplicaStateContainer( String reason, List recordsToInsert, Set negativeSizeRecorded, - Runnable counterIncrementer, ProcessingStats stats) throws ContainerNotFoundException { - counterIncrementer.run(); ContainerInfo container = containerManager.getContainer(containerId); Set replicas = containerManager.getContainerReplicas(containerId); int expected = container.getReplicationConfig().getRequiredNodes(); From c689a13c5491d6ccb6cdefb3358e6ea78f470969 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 27 Feb 2026 13:01:57 +0530 Subject: [PATCH 26/43] HDDS-13891. Added back the upgrade action claseses deleted by mistake. --- .../recon/TestReconAndAdminContainerCLI.java | 2 +- .../ozone/recon/TestReconEndpointUtil.java | 4 +- .../recon/TestReconTasksV2MultiNode.java | 8 +- ...V2.java => ContainerSchemaDefinition.java} | 56 +++++---- .../schema/ReconSchemaGenerationModule.java | 2 +- .../ozone/recon/ReconControllerModule.java | 4 +- .../ozone/recon/api/ClusterStateEndpoint.java | 4 +- .../ozone/recon/api/ContainerEndpoint.java | 18 +-- .../types/UnhealthyContainersResponse.java | 2 +- .../recon/fsck/ContainerHealthTaskV2.java | 2 +- .../recon/fsck/ReconReplicationManager.java | 6 +- .../fsck/ReconReplicationManagerReport.java | 2 +- .../ContainerHealthSchemaManagerV2.java | 74 ++++++------ .../ReconStorageContainerManagerFacade.java | 2 +- .../InitialConstraintUpgradeAction.java | 102 ++++++++++++++++ ...healthyContainerReplicaMismatchAction.java | 103 ++++++++++++++++ .../recon/api/TestContainerEndpoint.java | 2 +- .../fsck/TestReconReplicationManager.java | 112 +++++++++++++++--- .../TestSchemaVersionTableDefinition.java | 6 +- .../TestInitialConstraintUpgradeAction.java | 112 ++++++++++++++++++ 20 files changed, 509 insertions(+), 114 deletions(-) rename hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/{ContainerSchemaDefinitionV2.java => ContainerSchemaDefinition.java} (59%) create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java create mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java index 015c948b6a6d..16b1769bb768 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconAndAdminContainerCLI.java @@ -82,7 +82,7 @@ import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; import org.apache.ozone.test.GenericTestUtils; import org.apache.ozone.test.LambdaTestUtils; import org.apache.ozone.test.tag.Flaky; diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java index ffba62dc0035..4acafc105817 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconEndpointUtil.java @@ -42,7 +42,7 @@ import org.apache.hadoop.hdds.server.http.HttpConfig; import org.apache.hadoop.hdfs.web.URLConnectionFactory; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,7 +81,7 @@ public static void triggerReconDbSyncWithOm( } public static UnhealthyContainersResponse getUnhealthyContainersFromRecon( - OzoneConfiguration conf, ContainerSchemaDefinitionV2.UnHealthyContainerStates containerState) + OzoneConfiguration conf, ContainerSchemaDefinition.UnHealthyContainerStates containerState) throws JsonProcessingException { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append(getReconWebAddress(conf)) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java index 21315f965a1a..1344aedf53c6 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -31,7 +31,7 @@ import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.apache.ozone.test.LambdaTestUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -83,7 +83,7 @@ public void cleanupBeforeEach() throws Exception { // Ensure each test starts from a clean unhealthy-container table. reconContainerManager.getContainerSchemaManagerV2().clearAllUnhealthyContainerRecords(); // Ensure Recon has initialized pipeline state before assertions. - LambdaTestUtils.await(60000, 5000, + LambdaTestUtils.await(60000, 300, () -> (!reconPipelineManager.getPipelines().isEmpty())); } @@ -131,7 +131,7 @@ public void testContainerHealthTaskV2UnderReplicated() throws Exception { List underReplicatedContainers = reconContainerManager.getContainerSchemaManagerV2() .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.UNDER_REPLICATED, + ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); // Should be empty in normal operation (all replicas healthy) @@ -170,7 +170,7 @@ public void testContainerHealthTaskV2OverReplicated() throws Exception { List overReplicatedContainers = reconContainerManager.getContainerSchemaManagerV2() .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.OVER_REPLICATED, + ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, 0L, 0L, 1000); // Should be empty in normal operation assertEquals(0, overReplicatedContainers.size()); diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java similarity index 59% rename from hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java rename to hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java index 148295210843..c99ac911855a 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinitionV2.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java @@ -33,16 +33,15 @@ import org.slf4j.LoggerFactory; /** - * Schema definition for ContainerHealthTaskV2 - uses SCM as source of truth. - * This is independent from the legacy ContainerSchemaDefinition to allow - * both implementations to run in parallel during migration. + * Schema definition for unhealthy containers. */ @Singleton -public class ContainerSchemaDefinitionV2 implements ReconSchemaDefinition { - private static final Logger LOG = LoggerFactory.getLogger(ContainerSchemaDefinitionV2.class); +public class ContainerSchemaDefinition implements ReconSchemaDefinition { + private static final Logger LOG = + LoggerFactory.getLogger(ContainerSchemaDefinition.class); - public static final String UNHEALTHY_CONTAINERS_V2_TABLE_NAME = - "UNHEALTHY_CONTAINERS_V2"; + public static final String UNHEALTHY_CONTAINERS_TABLE_NAME = + "UNHEALTHY_CONTAINERS"; private static final String CONTAINER_ID = "container_id"; private static final String CONTAINER_STATE = "container_state"; @@ -50,7 +49,7 @@ public class ContainerSchemaDefinitionV2 implements ReconSchemaDefinition { private DSLContext dslContext; @Inject - ContainerSchemaDefinitionV2(DataSource dataSource) { + ContainerSchemaDefinition(DataSource dataSource) { this.dataSource = dataSource; } @@ -58,17 +57,14 @@ public class ContainerSchemaDefinitionV2 implements ReconSchemaDefinition { public void initializeSchema() throws SQLException { Connection conn = dataSource.getConnection(); dslContext = DSL.using(conn); - if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_V2_TABLE_NAME)) { - LOG.info("UNHEALTHY_CONTAINERS_V2 is missing, creating new one."); - createUnhealthyContainersV2Table(); + if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { + LOG.info("UNHEALTHY_CONTAINERS is missing, creating new one."); + createUnhealthyContainersTable(); } } - /** - * Create the UNHEALTHY_CONTAINERS_V2 table for V2 task. - */ - private void createUnhealthyContainersV2Table() { - dslContext.createTableIfNotExists(UNHEALTHY_CONTAINERS_V2_TABLE_NAME) + private void createUnhealthyContainersTable() { + dslContext.createTableIfNotExists(UNHEALTHY_CONTAINERS_TABLE_NAME) .column(CONTAINER_ID, SQLDataType.BIGINT.nullable(false)) .column(CONTAINER_STATE, SQLDataType.VARCHAR(16).nullable(false)) .column("in_state_since", SQLDataType.BIGINT.nullable(false)) @@ -76,14 +72,15 @@ private void createUnhealthyContainersV2Table() { .column("actual_replica_count", SQLDataType.INTEGER.nullable(false)) .column("replica_delta", SQLDataType.INTEGER.nullable(false)) .column("reason", SQLDataType.VARCHAR(500).nullable(true)) - .constraint(DSL.constraint("pk_container_id_v2") + .constraint(DSL.constraint("pk_container_id") .primaryKey(CONTAINER_ID, CONTAINER_STATE)) - .constraint(DSL.constraint(UNHEALTHY_CONTAINERS_V2_TABLE_NAME + "_ck1") + .constraint(DSL.constraint(UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") .check(field(name(CONTAINER_STATE)) .in(UnHealthyContainerStates.values()))) .execute(); - dslContext.createIndex("idx_container_state_v2") - .on(DSL.table(UNHEALTHY_CONTAINERS_V2_TABLE_NAME), DSL.field(name(CONTAINER_STATE))) + dslContext.createIndex("idx_container_state") + .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME), + DSL.field(name(CONTAINER_STATE))) .execute(); } @@ -96,16 +93,17 @@ public DataSource getDataSource() { } /** - * ENUM describing the allowed container states in V2 table. - * V2 uses SCM's ReplicationManager as the source of truth. + * ENUM describing the allowed container states in the unhealthy containers + * table. */ public enum UnHealthyContainerStates { - MISSING, // From SCM ReplicationManager - UNDER_REPLICATED, // From SCM ReplicationManager - OVER_REPLICATED, // From SCM ReplicationManager - MIS_REPLICATED, // From SCM ReplicationManager - REPLICA_MISMATCH, // Computed locally by Recon (SCM doesn't track checksums) - EMPTY_MISSING, // Kept for API compatibility with legacy clients - NEGATIVE_SIZE // Kept for API compatibility with legacy clients + MISSING, + UNDER_REPLICATED, + OVER_REPLICATED, + MIS_REPLICATED, + REPLICA_MISMATCH, + EMPTY_MISSING, + NEGATIVE_SIZE, + ALL_REPLICAS_BAD } } diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java index 698a6ec8ee7b..8ab570a0ab6c 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ReconSchemaGenerationModule.java @@ -32,7 +32,7 @@ protected void configure() { Multibinder schemaBinder = Multibinder.newSetBinder(binder(), ReconSchemaDefinition.class); schemaBinder.addBinding().to(UtilizationSchemaDefinition.class); - schemaBinder.addBinding().to(ContainerSchemaDefinitionV2.class); + schemaBinder.addBinding().to(ContainerSchemaDefinition.class); schemaBinder.addBinding().to(ReconTaskSchemaDefinition.class); schemaBinder.addBinding().to(StatsSchemaDefinition.class); schemaBinder.addBinding().to(SchemaVersionTableDefinition.class); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java index 45e4d0845606..99abbca14267 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java @@ -78,7 +78,7 @@ import org.apache.ozone.recon.schema.generated.tables.daos.FileCountBySizeDao; import org.apache.ozone.recon.schema.generated.tables.daos.GlobalStatsDao; import org.apache.ozone.recon.schema.generated.tables.daos.ReconTaskStatusDao; -import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; +import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersDao; import org.apache.ratis.protocol.ClientId; import org.jooq.Configuration; import org.jooq.DAO; @@ -162,7 +162,7 @@ public static class ReconDaoBindingModule extends AbstractModule { ImmutableList.of( FileCountBySizeDao.class, ReconTaskStatusDao.class, - UnhealthyContainersV2Dao.class, + UnhealthyContainersDao.class, GlobalStatsDao.class, ClusterGrowthDailyDao.class, ContainerCountBySizeDao.class diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java index bcc0d68eb080..970db3f10b1c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java @@ -51,7 +51,7 @@ import org.apache.hadoop.ozone.recon.spi.ReconGlobalStatsManager; import org.apache.hadoop.ozone.recon.tasks.GlobalStatsValue; import org.apache.hadoop.ozone.recon.tasks.OmTableInsightTask; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,7 +101,7 @@ public Response getClusterState() { List missingContainers = containerHealthSchemaManagerV2 .getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, MISSING_CONTAINER_COUNT_LIMIT); containerStateCounts.setMissingContainerCount( diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index afdcd5591886..499381da1ffc 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -82,7 +82,7 @@ import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager; import org.apache.hadoop.ozone.util.SeekableIterator; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -339,7 +339,7 @@ public Response getMissingContainers( ) { List missingContainers = new ArrayList<>(); containerHealthSchemaManagerV2.getUnhealthyContainers( - ContainerSchemaDefinitionV2.UnHealthyContainerStates.MISSING, + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, limit) .forEach(container -> { long containerID = container.getContainerId(); @@ -392,13 +392,14 @@ public Response getUnhealthyContainers( @QueryParam(RECON_QUERY_MAX_CONTAINER_ID) long maxContainerId, @DefaultValue(PREV_CONTAINER_ID_DEFAULT_VALUE) @QueryParam(RECON_QUERY_MIN_CONTAINER_ID) long minContainerId) { - return getUnhealthyContainersV2(state, limit, maxContainerId, minContainerId); + return getUnhealthyContainersFromSchema(state, limit, maxContainerId, + minContainerId); } /** - * V2 implementation - reads from UNHEALTHY_CONTAINERS_V2 table. + * V2 implementation - reads from UNHEALTHY_CONTAINERS table. */ - private Response getUnhealthyContainersV2( + private Response getUnhealthyContainersFromSchema( String state, int limit, long maxContainerId, @@ -407,11 +408,11 @@ private Response getUnhealthyContainersV2( List summary = new ArrayList<>(); try { - ContainerSchemaDefinitionV2.UnHealthyContainerStates v2State = null; + ContainerSchemaDefinition.UnHealthyContainerStates v2State = null; if (state != null) { // Convert V1 state string to V2 enum - v2State = ContainerSchemaDefinitionV2.UnHealthyContainerStates.valueOf(state); + v2State = ContainerSchemaDefinition.UnHealthyContainerStates.valueOf(state); } // Get summary from V2 table and convert to V1 format @@ -498,7 +499,8 @@ public Response getUnhealthyContainers( @QueryParam(RECON_QUERY_MAX_CONTAINER_ID) long maxContainerId, @DefaultValue(PREV_CONTAINER_ID_DEFAULT_VALUE) @QueryParam(RECON_QUERY_MIN_CONTAINER_ID) long minContainerId) { - return getUnhealthyContainers(null, limit, maxContainerId, minContainerId); + return getUnhealthyContainersFromSchema(null, limit, maxContainerId, + minContainerId); } /** diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java index eec52478d60f..350f9e8ceda1 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainersResponse.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collection; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; /** * Class that represents the API Response structure of Unhealthy Containers. diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java index 331745aa813d..5ccb3528f73c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java @@ -101,7 +101,7 @@ protected void run() { *
    *
  • Processes all containers in batch using inherited health check chain
  • *
  • Captures ALL unhealthy containers (no 100-sample limit)
  • - *
  • Stores results in UNHEALTHY_CONTAINERS_V2 table
  • + *
  • Stores results in UNHEALTHY_CONTAINERS table
  • *
*/ @Override diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 8050b1b039cb..f465a1bccaa7 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -41,7 +41,7 @@ import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.util.Time; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +52,7 @@ *
    *
  1. Uses NoOpsContainerReplicaPendingOps stub (no pending operations tracking)
  2. *
  3. Overrides processAll() to capture ALL container health states (no 100-sample limit)
  4. - *
  5. Stores results in Recon's UNHEALTHY_CONTAINERS_V2 table
  6. + *
  7. Stores results in Recon's UNHEALTHY_CONTAINERS table
  8. *
  9. Does not issue replication commands (read-only monitoring)
  10. *
* @@ -268,7 +268,7 @@ private boolean hasDataChecksumMismatch(Set replicas) { *
  • Process each container using inherited health check chain (SCM logic)
  • *
  • Additionally check for REPLICA_MISMATCH (Recon-specific)
  • *
  • Capture ALL unhealthy container IDs per health state (no sampling limit)
  • - *
  • Store results in Recon's UNHEALTHY_CONTAINERS_V2 table
  • + *
  • Store results in Recon's UNHEALTHY_CONTAINERS table
  • * * *

    Differences from SCM's processAll():

    diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java index c602a75e3ff8..1c235703a195 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java @@ -37,7 +37,7 @@ * for debugging/UI display.

    * *

    Recon, however, needs to track per-container health states for ALL containers - * to populate its UNHEALTHY_CONTAINERS_V2 table. This extended report removes + * to populate its UNHEALTHY_CONTAINERS table. This extended report removes * the sampling limitation while maintaining backward compatibility by still * calling the parent's incrementAndSample() method.

    * diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index e2453205cde4..8c9656b0fc47 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -17,8 +17,8 @@ package org.apache.hadoop.ozone.recon.persistence; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UNHEALTHY_CONTAINERS_V2_TABLE_NAME; -import static org.apache.ozone.recon.schema.generated.tables.UnhealthyContainersV2Table.UNHEALTHY_CONTAINERS_V2; +import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; +import static org.apache.ozone.recon.schema.generated.tables.UnhealthyContainersTable.UNHEALTHY_CONTAINERS; import static org.jooq.impl.DSL.count; import com.google.common.annotations.VisibleForTesting; @@ -29,11 +29,11 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; -import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; -import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2; -import org.apache.ozone.recon.schema.generated.tables.records.UnhealthyContainersV2Record; +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.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; +import org.apache.ozone.recon.schema.generated.tables.records.UnhealthyContainersRecord; import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.OrderField; @@ -44,7 +44,7 @@ import org.slf4j.LoggerFactory; /** - * Manager for UNHEALTHY_CONTAINERS_V2 table used by ContainerHealthTaskV2. + * Manager for UNHEALTHY_CONTAINERS table used by ContainerHealthTaskV2. */ @Singleton public class ContainerHealthSchemaManagerV2 { @@ -52,13 +52,13 @@ public class ContainerHealthSchemaManagerV2 { LoggerFactory.getLogger(ContainerHealthSchemaManagerV2.class); private static final int BATCH_INSERT_CHUNK_SIZE = 1000; - private final UnhealthyContainersV2Dao unhealthyContainersV2Dao; - private final ContainerSchemaDefinitionV2 containerSchemaDefinitionV2; + private final UnhealthyContainersDao unhealthyContainersV2Dao; + private final ContainerSchemaDefinition containerSchemaDefinitionV2; @Inject public ContainerHealthSchemaManagerV2( - ContainerSchemaDefinitionV2 containerSchemaDefinitionV2, - UnhealthyContainersV2Dao unhealthyContainersV2Dao) { + ContainerSchemaDefinition containerSchemaDefinitionV2, + UnhealthyContainersDao unhealthyContainersV2Dao) { this.unhealthyContainersV2Dao = unhealthyContainersV2Dao; this.containerSchemaDefinitionV2 = containerSchemaDefinitionV2; } @@ -91,7 +91,7 @@ public void insertUnhealthyContainerRecords(List rec recs.size(), e); fallbackInsertOrUpdate(recs); } catch (Exception e) { - LOG.error("Failed to batch insert records into {}", UNHEALTHY_CONTAINERS_V2_TABLE_NAME, e); + LOG.error("Failed to batch insert records into {}", UNHEALTHY_CONTAINERS_TABLE_NAME, e); throw new RuntimeException("Recon failed to insert " + recs.size() + " unhealthy container records.", e); } @@ -101,7 +101,7 @@ private void batchInsertInChunks(DSLContext dslContext, List recs) { dslContext.transaction(configuration -> { DSLContext txContext = configuration.dsl(); - List records = + List records = new ArrayList<>(BATCH_INSERT_CHUNK_SIZE); for (int from = 0; from < recs.size(); from += BATCH_INSERT_CHUNK_SIZE) { @@ -120,7 +120,7 @@ private void fallbackInsertOrUpdate(List recs) { connection.setAutoCommit(false); try { for (UnhealthyContainerRecordV2 rec : recs) { - UnhealthyContainersV2 jooqRec = toJooqPojo(rec); + UnhealthyContainers jooqRec = toJooqPojo(rec); try { unhealthyContainersV2Dao.insert(jooqRec); } catch (DataAccessException insertEx) { @@ -143,9 +143,9 @@ private void fallbackInsertOrUpdate(List recs) { } } - private UnhealthyContainersV2Record toJooqRecord(DSLContext txContext, + private UnhealthyContainersRecord toJooqRecord(DSLContext txContext, UnhealthyContainerRecordV2 rec) { - UnhealthyContainersV2Record record = txContext.newRecord(UNHEALTHY_CONTAINERS_V2); + UnhealthyContainersRecord record = txContext.newRecord(UNHEALTHY_CONTAINERS); record.setContainerId(rec.getContainerId()); record.setContainerState(rec.getContainerState()); record.setInStateSince(rec.getInStateSince()); @@ -156,8 +156,8 @@ private UnhealthyContainersV2Record toJooqRecord(DSLContext txContext, return record; } - private UnhealthyContainersV2 toJooqPojo(UnhealthyContainerRecordV2 rec) { - return new UnhealthyContainersV2( + private UnhealthyContainers toJooqPojo(UnhealthyContainerRecordV2 rec) { + return new UnhealthyContainers( rec.getContainerId(), rec.getContainerState(), rec.getInStateSince(), @@ -184,9 +184,9 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); try { - int deleted = dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2) - .where(UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.in(containerIds)) - .and(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.in( + int deleted = dslContext.deleteFrom(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(), @@ -211,10 +211,10 @@ public List getUnhealthyContainersSummary() { try { return dslContext - .select(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.as("containerState"), + .select(UNHEALTHY_CONTAINERS.CONTAINER_STATE.as("containerState"), count().as("cnt")) - .from(UNHEALTHY_CONTAINERS_V2) - .groupBy(UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE) + .from(UNHEALTHY_CONTAINERS) + .groupBy(UNHEALTHY_CONTAINERS.CONTAINER_STATE) .fetchInto(UnhealthyContainersSummaryV2.class); } catch (Exception e) { LOG.error("Failed to get summary from V2 table", e); @@ -230,28 +230,28 @@ public List getUnhealthyContainers( DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); SelectQuery query = dslContext.selectQuery(); - query.addFrom(UNHEALTHY_CONTAINERS_V2); + query.addFrom(UNHEALTHY_CONTAINERS); Condition containerCondition; OrderField[] orderField; if (maxContainerId > 0) { - containerCondition = UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.lessThan(maxContainerId); + containerCondition = UNHEALTHY_CONTAINERS.CONTAINER_ID.lessThan(maxContainerId); orderField = new OrderField[]{ - UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.desc(), - UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.asc() + UNHEALTHY_CONTAINERS.CONTAINER_ID.desc(), + UNHEALTHY_CONTAINERS.CONTAINER_STATE.asc() }; } else { - containerCondition = UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.greaterThan(minContainerId); + containerCondition = UNHEALTHY_CONTAINERS.CONTAINER_ID.greaterThan(minContainerId); orderField = new OrderField[]{ - UNHEALTHY_CONTAINERS_V2.CONTAINER_ID.asc(), - UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.asc() + UNHEALTHY_CONTAINERS.CONTAINER_ID.asc(), + UNHEALTHY_CONTAINERS.CONTAINER_STATE.asc() }; } if (state != null) { query.addConditions(containerCondition.and( - UNHEALTHY_CONTAINERS_V2.CONTAINER_STATE.eq(state.toString()))); + UNHEALTHY_CONTAINERS.CONTAINER_STATE.eq(state.toString()))); } else { query.addConditions(containerCondition); } @@ -260,8 +260,8 @@ public List getUnhealthyContainers( query.addLimit(limit); try { - return query.fetchInto(UnhealthyContainersV2Record.class).stream() - .sorted(Comparator.comparingLong(UnhealthyContainersV2Record::getContainerId)) + return query.fetchInto(UnhealthyContainersRecord.class).stream() + .sorted(Comparator.comparingLong(UnhealthyContainersRecord::getContainerId)) .map(record -> new UnhealthyContainerRecordV2( record.getContainerId(), record.getContainerState(), @@ -284,7 +284,7 @@ public List getUnhealthyContainers( public void clearAllUnhealthyContainerRecords() { DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); try { - dslContext.deleteFrom(UNHEALTHY_CONTAINERS_V2).execute(); + dslContext.deleteFrom(UNHEALTHY_CONTAINERS).execute(); LOG.info("Cleared all V2 unhealthy container records"); } catch (Exception e) { LOG.error("Failed to clear V2 unhealthy container records", e); @@ -292,7 +292,7 @@ public void clearAllUnhealthyContainerRecords() { } /** - * POJO representing a record in UNHEALTHY_CONTAINERS_V2 table. + * POJO representing a record in UNHEALTHY_CONTAINERS table. */ public static class UnhealthyContainerRecordV2 { private final long containerId; 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 2015b841c08f..bed1e857fb22 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 @@ -270,7 +270,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, PipelineSyncTask pipelineSyncTask = new PipelineSyncTask(pipelineManager, nodeManager, scmServiceProvider, reconTaskConfig, taskStatusUpdaterManager); - // Create ContainerHealthTaskV2 (always runs, writes to UNHEALTHY_CONTAINERS_V2) + // Create ContainerHealthTaskV2 (always runs, writes to UNHEALTHY_CONTAINERS) LOG.info("Creating ContainerHealthTaskV2"); containerHealthTaskV2 = new ContainerHealthTaskV2( reconTaskConfig, diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java new file mode 100644 index 000000000000..4b50ea1070ba --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java @@ -0,0 +1,102 @@ +/* + * 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.hadoop.ozone.recon.upgrade.ReconLayoutFeature.INITIAL_VERSION; +import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; +import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.sql.DataSource; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +import org.jooq.DSLContext; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Upgrade action for the INITIAL schema version to ensure unhealthy-container + * state constraints are aligned with the currently supported state set. + */ +@UpgradeActionRecon(feature = INITIAL_VERSION) +public class InitialConstraintUpgradeAction implements ReconUpgradeAction { + + private static final Logger LOG = + LoggerFactory.getLogger(InitialConstraintUpgradeAction.class); + + @Override + public void execute(DataSource source) throws SQLException { + try (Connection conn = source.getConnection()) { + DSLContext dslContext = DSL.using(conn); + updateConstraintIfTableExists(dslContext, conn, + UNHEALTHY_CONTAINERS_TABLE_NAME); + } catch (SQLException e) { + throw new SQLException("Failed to execute InitialConstraintUpgradeAction", e); + } + } + + private void updateConstraintIfTableExists( + DSLContext dslContext, Connection conn, String tableName) { + if (!TABLE_EXISTS_CHECK.test(conn, tableName)) { + return; + } + + dropConstraintIfPresent(dslContext, tableName, tableName + "ck1"); + dropConstraintIfPresent(dslContext, tableName, tableName + "_ck1"); + addUpdatedConstraint(dslContext, tableName); + } + + private void dropConstraintIfPresent( + DSLContext dslContext, String tableName, String constraintName) { + try { + dslContext.alterTable(tableName).dropConstraint(constraintName).execute(); + LOG.info("Dropped existing constraint {} on {}", constraintName, tableName); + } catch (DataAccessException ignored) { + LOG.debug("Constraint {} does not exist on {}", constraintName, tableName); + } + } + + private void addUpdatedConstraint(DSLContext dslContext, String tableName) { + String[] enumStates = getSupportedStates(); + String constraintName = tableName + "_ck1"; + dslContext.alterTable(tableName) + .add(DSL.constraint(constraintName) + .check(field(name("container_state")) + .in(enumStates))) + .execute(); + LOG.info("Updated unhealthy-container constraint {} on {}", constraintName, + tableName); + } + + private String[] getSupportedStates() { + Set states = new LinkedHashSet<>(); + Arrays.stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) + .map(Enum::name) + .forEach(states::add); + // Preserve compatibility with rows created by legacy V1 schema. + states.add("ALL_REPLICAS_BAD"); + return states.toArray(new String[0]); + } +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java new file mode 100644 index 000000000000..8e9149e8c362 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.recon.upgrade; + +import static org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature.UNHEALTHY_CONTAINER_REPLICA_MISMATCH; +import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; +import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.sql.DataSource; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +import org.jooq.DSLContext; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Upgrade action for the replica-mismatch layout feature to ensure unhealthy + * container state constraints allow the full supported state set. + */ +@UpgradeActionRecon(feature = UNHEALTHY_CONTAINER_REPLICA_MISMATCH) +public class UnhealthyContainerReplicaMismatchAction + implements ReconUpgradeAction { + + private static final Logger LOG = + LoggerFactory.getLogger(UnhealthyContainerReplicaMismatchAction.class); + + @Override + public void execute(DataSource source) throws SQLException { + try (Connection conn = source.getConnection()) { + DSLContext dslContext = DSL.using(conn); + updateConstraintIfTableExists(dslContext, conn, + UNHEALTHY_CONTAINERS_TABLE_NAME); + } catch (SQLException e) { + throw new SQLException( + "Failed to execute UnhealthyContainerReplicaMismatchAction", e); + } + } + + private void updateConstraintIfTableExists( + DSLContext dslContext, Connection conn, String tableName) { + if (!TABLE_EXISTS_CHECK.test(conn, tableName)) { + return; + } + + dropConstraintIfPresent(dslContext, tableName, tableName + "ck1"); + dropConstraintIfPresent(dslContext, tableName, tableName + "_ck1"); + addUpdatedConstraint(dslContext, tableName); + } + + private void dropConstraintIfPresent( + DSLContext dslContext, String tableName, String constraintName) { + try { + dslContext.alterTable(tableName).dropConstraint(constraintName).execute(); + LOG.info("Dropped existing constraint {} on {}", constraintName, tableName); + } catch (DataAccessException ignored) { + LOG.debug("Constraint {} does not exist on {}", constraintName, tableName); + } + } + + private void addUpdatedConstraint(DSLContext dslContext, String tableName) { + String[] enumStates = getSupportedStates(); + String constraintName = tableName + "_ck1"; + dslContext.alterTable(tableName) + .add(DSL.constraint(constraintName) + .check(field(name("container_state")) + .in(enumStates))) + .execute(); + LOG.info("Updated unhealthy-container constraint {} on {}", constraintName, + tableName); + } + + private String[] getSupportedStates() { + Set states = new LinkedHashSet<>(); + Arrays.stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) + .map(Enum::name) + .forEach(states::add); + states.add("ALL_REPLICAS_BAD"); + return states.toArray(new String[0]); + } +} 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 55d3ec037170..c5ad18ac3071 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 @@ -113,7 +113,7 @@ import org.apache.hadoop.ozone.recon.tasks.ContainerKeyMapperTaskOBS; import org.apache.hadoop.ozone.recon.tasks.NSSummaryTaskWithFSO; import org.apache.hadoop.ozone.recon.tasks.ReconOmTask; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; import org.apache.ozone.test.tag.Flaky; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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 index 518fe3f0c192..b1bddfc11951 100644 --- 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 @@ -25,9 +25,12 @@ 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 org.apache.hadoop.hdds.client.ReplicationConfig; import org.apache.hadoop.hdds.conf.OzoneConfiguration; @@ -46,9 +49,9 @@ import org.apache.hadoop.hdds.server.events.EventQueue; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2; -import org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UnHealthyContainerStates; -import org.apache.ozone.recon.schema.generated.tables.daos.UnhealthyContainersV2Dao; +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; @@ -69,7 +72,7 @@ public class TestReconReplicationManager extends AbstractReconSqlDBTest { private ContainerHealthSchemaManagerV2 schemaManagerV2; - private UnhealthyContainersV2Dao dao; + private UnhealthyContainersDao dao; private ContainerManager containerManager; private ReconReplicationManager reconRM; @@ -79,9 +82,9 @@ public TestReconReplicationManager() { @BeforeEach public void setUp() throws Exception { - dao = getDao(UnhealthyContainersV2Dao.class); + dao = getDao(UnhealthyContainersDao.class); schemaManagerV2 = new ContainerHealthSchemaManagerV2( - getSchemaDefinition(ContainerSchemaDefinitionV2.class), dao); + getSchemaDefinition(ContainerSchemaDefinition.class), dao); containerManager = mock(ContainerManager.class); PlacementPolicy placementPolicy = mock(PlacementPolicy.class); @@ -156,6 +159,65 @@ public void testProcessAllStoresEmptyMissingAndNegativeSizeRecords() 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 testReconReplicationManagerCreation() { // Verify ReconReplicationManager was created successfully @@ -198,8 +260,8 @@ public void testDatabaseOperationsWork() throws Exception { when(containerManager.getContainers()).thenReturn(new ArrayList<>()); // Insert a test record directly - org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2 record = - new org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainersV2(); + 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()); @@ -249,8 +311,17 @@ private ContainerInfo mockContainerInfo(long containerId, long numberOfKeys, } private ReconReplicationManager createStateInjectingReconRM( - long emptyMissingContainerId, - long negativeSizeContainerId) throws Exception { + 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); @@ -277,17 +348,24 @@ protected boolean processContainer(ContainerInfo containerInfo, boolean readOnly) { ReconReplicationManagerReport reconReport = (ReconReplicationManagerReport) report; - if (containerInfo.getContainerID() == emptyMissingContainerId) { - reconReport.incrementAndSample(ContainerHealthState.MISSING, containerInfo); - return true; - } - if (containerInfo.getContainerID() == negativeSizeContainerId) { - reconReport.incrementAndSample( - ContainerHealthState.UNDER_REPLICATED, containerInfo); + ContainerHealthState state = + stateByContainer.get(containerInfo.getContainerID()); + if (state != null) { + reconReport.incrementAndSample(state, containerInfo); 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; + } } diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java index c7063da1d2c4..3c01312c43bf 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java @@ -17,7 +17,7 @@ package org.apache.hadoop.ozone.recon.persistence; -import static org.apache.ozone.recon.schema.ContainerSchemaDefinitionV2.UNHEALTHY_CONTAINERS_V2_TABLE_NAME; +import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; import static org.apache.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME; import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; import static org.apache.ozone.recon.schema.SqlDbUtils.listAllTables; @@ -189,7 +189,7 @@ public void testPreUpgradedClusterScenario() throws Exception { dropTable(connection, SCHEMA_VERSION_TABLE_NAME); if (listAllTables(connection).isEmpty()) { createTable(connection, GLOBAL_STATS_TABLE_NAME); - createTable(connection, UNHEALTHY_CONTAINERS_V2_TABLE_NAME); + createTable(connection, UNHEALTHY_CONTAINERS_TABLE_NAME); } // Initialize the schema @@ -231,7 +231,7 @@ public void testUpgradedClusterScenario() throws Exception { if (listAllTables(connection).isEmpty()) { // Create necessary tables to simulate the cluster state createTable(connection, GLOBAL_STATS_TABLE_NAME); - createTable(connection, UNHEALTHY_CONTAINERS_V2_TABLE_NAME); + createTable(connection, UNHEALTHY_CONTAINERS_TABLE_NAME); // Create the schema version table createSchemaVersionTable(connection); } diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java new file mode 100644 index 000000000000..ff41cd4719ef --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java @@ -0,0 +1,112 @@ +/* + * 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.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.sql.DataSource; +import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +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 InitialConstraintUpgradeAction. + */ +public class TestInitialConstraintUpgradeAction extends AbstractReconSqlDBTest { + + private DSLContext dslContext; + private DataSource dataSource; + + @BeforeEach + public void setUp() { + dslContext = getDslContext(); + dataSource = getInjector().getInstance(DataSource.class); + createTableWithNarrowConstraint(UNHEALTHY_CONTAINERS_TABLE_NAME); + } + + @Test + public void testUpgradeExpandsAllowedStates() + throws Exception { + InitialConstraintUpgradeAction action = new InitialConstraintUpgradeAction(); + action.execute(dataSource); + + insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 1L, "MISSING"); + insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 2L, "REPLICA_MISMATCH"); + insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 3L, "EMPTY_MISSING"); + insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 4L, "NEGATIVE_SIZE"); + insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 5L, "ALL_REPLICAS_BAD"); + + assertThrows(org.jooq.exception.DataAccessException.class, + () -> insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 6L, + "INVALID_STATE")); + } + + private void createTableWithNarrowConstraint(String tableName) { + dslContext.dropTableIfExists(tableName).execute(); + dslContext.createTable(tableName) + .column("container_id", SQLDataType.BIGINT.nullable(false)) + .column("container_state", SQLDataType.VARCHAR(32).nullable(false)) + .column("in_state_since", SQLDataType.BIGINT.nullable(false)) + .column("expected_replica_count", SQLDataType.INTEGER.nullable(false)) + .column("actual_replica_count", SQLDataType.INTEGER.nullable(false)) + .column("replica_delta", SQLDataType.INTEGER.nullable(false)) + .column("reason", SQLDataType.VARCHAR(500).nullable(true)) + .constraint(DSL.constraint("pk_" + tableName.toLowerCase()) + .primaryKey("container_id", "container_state")) + .constraint(DSL.constraint(tableName + "_ck1") + .check(field(name("container_state")) + .in( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING + .toString(), + ContainerSchemaDefinition.UnHealthyContainerStates + .UNDER_REPLICATED.toString(), + ContainerSchemaDefinition.UnHealthyContainerStates + .OVER_REPLICATED.toString(), + ContainerSchemaDefinition.UnHealthyContainerStates + .MIS_REPLICATED.toString()))) + .execute(); + } + + private void insertState(String tableName, long containerId, String state) { + dslContext.insertInto(DSL.table(tableName)) + .columns( + field(name("container_id")), + field(name("container_state")), + field(name("in_state_since")), + field(name("expected_replica_count")), + field(name("actual_replica_count")), + field(name("replica_delta")), + field(name("reason"))) + .values( + containerId, + state, + System.currentTimeMillis(), + 3, + 2, + -1, + "test") + .execute(); + } +} From 44a2a1874e4f56dbdcb56a7a6761e1cfb2170021 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 27 Feb 2026 13:09:21 +0530 Subject: [PATCH 27/43] HDDS-13891. Added back the upgrade action claseses deleted by mistake. --- .../InitialConstraintUpgradeAction.java | 81 ++++---- ...healthyContainerReplicaMismatchAction.java | 84 ++++---- .../TestInitialConstraintUpgradeAction.java | 186 ++++++++++++------ 3 files changed, 205 insertions(+), 146 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java index 4b50ea1070ba..d9f7eef54344 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java @@ -23,80 +23,75 @@ import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.name; +import com.google.common.annotations.VisibleForTesting; import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; import javax.sql.DataSource; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.jooq.DSLContext; -import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Upgrade action for the INITIAL schema version to ensure unhealthy-container - * state constraints are aligned with the currently supported state set. + * Upgrade action for the INITIAL schema version, which manages constraints + * for the UNHEALTHY_CONTAINERS table. */ @UpgradeActionRecon(feature = INITIAL_VERSION) public class InitialConstraintUpgradeAction implements ReconUpgradeAction { - private static final Logger LOG = - LoggerFactory.getLogger(InitialConstraintUpgradeAction.class); + private static final Logger LOG = LoggerFactory.getLogger(InitialConstraintUpgradeAction.class); + private DSLContext dslContext; @Override public void execute(DataSource source) throws SQLException { try (Connection conn = source.getConnection()) { - DSLContext dslContext = DSL.using(conn); - updateConstraintIfTableExists(dslContext, conn, - UNHEALTHY_CONTAINERS_TABLE_NAME); + if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { + return; + } + dslContext = DSL.using(conn); + // Drop the existing constraint + dropConstraint(); + // Add the updated constraint with all enum states + addUpdatedConstraint(); } catch (SQLException e) { throw new SQLException("Failed to execute InitialConstraintUpgradeAction", e); } } - private void updateConstraintIfTableExists( - DSLContext dslContext, Connection conn, String tableName) { - if (!TABLE_EXISTS_CHECK.test(conn, tableName)) { - return; - } - - dropConstraintIfPresent(dslContext, tableName, tableName + "ck1"); - dropConstraintIfPresent(dslContext, tableName, tableName + "_ck1"); - addUpdatedConstraint(dslContext, tableName); + /** + * Drops the existing constraint from the UNHEALTHY_CONTAINERS table. + */ + private void dropConstraint() { + String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1"; + dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME) + .dropConstraint(constraintName) + .execute(); + LOG.debug("Dropped the existing constraint: {}", constraintName); } - private void dropConstraintIfPresent( - DSLContext dslContext, String tableName, String constraintName) { - try { - dslContext.alterTable(tableName).dropConstraint(constraintName).execute(); - LOG.info("Dropped existing constraint {} on {}", constraintName, tableName); - } catch (DataAccessException ignored) { - LOG.debug("Constraint {} does not exist on {}", constraintName, tableName); - } - } + /** + * Adds the updated constraint directly within this class. + */ + private void addUpdatedConstraint() { + String[] enumStates = Arrays + .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) + .map(Enum::name) + .toArray(String[]::new); - private void addUpdatedConstraint(DSLContext dslContext, String tableName) { - String[] enumStates = getSupportedStates(); - String constraintName = tableName + "_ck1"; - dslContext.alterTable(tableName) - .add(DSL.constraint(constraintName) + dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME) + .add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") .check(field(name("container_state")) .in(enumStates))) .execute(); - LOG.info("Updated unhealthy-container constraint {} on {}", constraintName, - tableName); + + LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table for enum state values: {}", + Arrays.toString(enumStates)); } - private String[] getSupportedStates() { - Set states = new LinkedHashSet<>(); - Arrays.stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) - .map(Enum::name) - .forEach(states::add); - // Preserve compatibility with rows created by legacy V1 schema. - states.add("ALL_REPLICAS_BAD"); - return states.toArray(new String[0]); + @VisibleForTesting + public void setDslContext(DSLContext dslContext) { + this.dslContext = dslContext; } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java index 8e9149e8c362..c2ce4ba8bf05 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java @@ -26,78 +26,66 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; import javax.sql.DataSource; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.jooq.DSLContext; -import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Upgrade action for the replica-mismatch layout feature to ensure unhealthy - * container state constraints allow the full supported state set. + * Upgrade action for handling the addition of a new unhealthy container state in Recon, which will be for containers, + * that have replicas with different data checksums. */ @UpgradeActionRecon(feature = UNHEALTHY_CONTAINER_REPLICA_MISMATCH) -public class UnhealthyContainerReplicaMismatchAction - implements ReconUpgradeAction { +public class UnhealthyContainerReplicaMismatchAction implements ReconUpgradeAction { - private static final Logger LOG = - LoggerFactory.getLogger(UnhealthyContainerReplicaMismatchAction.class); + private static final Logger LOG = LoggerFactory.getLogger(UnhealthyContainerReplicaMismatchAction.class); + private DSLContext dslContext; @Override - public void execute(DataSource source) throws SQLException { + public void execute(DataSource source) throws Exception { try (Connection conn = source.getConnection()) { - DSLContext dslContext = DSL.using(conn); - updateConstraintIfTableExists(dslContext, conn, - UNHEALTHY_CONTAINERS_TABLE_NAME); + if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { + return; + } + dslContext = DSL.using(conn); + // Drop the existing constraint + dropConstraint(); + // Add the updated constraint with all enum states + addUpdatedConstraint(); } catch (SQLException e) { - throw new SQLException( - "Failed to execute UnhealthyContainerReplicaMismatchAction", e); + throw new SQLException("Failed to execute UnhealthyContainerReplicaMismatchAction", e); } } - private void updateConstraintIfTableExists( - DSLContext dslContext, Connection conn, String tableName) { - if (!TABLE_EXISTS_CHECK.test(conn, tableName)) { - return; - } - - dropConstraintIfPresent(dslContext, tableName, tableName + "ck1"); - dropConstraintIfPresent(dslContext, tableName, tableName + "_ck1"); - addUpdatedConstraint(dslContext, tableName); + /** + * Drops the existing constraint from the UNHEALTHY_CONTAINERS table. + */ + private void dropConstraint() { + String constraintName = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1"; + dslContext.alterTable(UNHEALTHY_CONTAINERS_TABLE_NAME) + .dropConstraint(constraintName) + .execute(); + LOG.debug("Dropped the existing constraint: {}", constraintName); } - private void dropConstraintIfPresent( - DSLContext dslContext, String tableName, String constraintName) { - try { - dslContext.alterTable(tableName).dropConstraint(constraintName).execute(); - LOG.info("Dropped existing constraint {} on {}", constraintName, tableName); - } catch (DataAccessException ignored) { - LOG.debug("Constraint {} does not exist on {}", constraintName, tableName); - } - } + /** + * Adds the updated constraint directly within this class. + */ + private void addUpdatedConstraint() { + String[] enumStates = Arrays + .stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) + .map(Enum::name) + .toArray(String[]::new); - private void addUpdatedConstraint(DSLContext dslContext, String tableName) { - String[] enumStates = getSupportedStates(); - String constraintName = tableName + "_ck1"; - dslContext.alterTable(tableName) - .add(DSL.constraint(constraintName) + dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME) + .add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") .check(field(name("container_state")) .in(enumStates))) .execute(); - LOG.info("Updated unhealthy-container constraint {} on {}", constraintName, - tableName); - } - private String[] getSupportedStates() { - Set states = new LinkedHashSet<>(); - Arrays.stream(ContainerSchemaDefinition.UnHealthyContainerStates.values()) - .map(Enum::name) - .forEach(states::add); - states.add("ALL_REPLICAS_BAD"); - return states.toArray(new String[0]); + LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table for enum state values: {}", + Arrays.toString(enumStates)); } } diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java index ff41cd4719ef..7c4c8e551229 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java @@ -20,77 +20,140 @@ import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.name; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +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.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; 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 InitialConstraintUpgradeAction. + * Test class for InitialConstraintUpgradeAction. */ public class TestInitialConstraintUpgradeAction extends AbstractReconSqlDBTest { + private InitialConstraintUpgradeAction upgradeAction; private DSLContext dslContext; - private DataSource dataSource; + private ReconStorageContainerManagerFacade mockScmFacade; @BeforeEach - public void setUp() { + public void setUp() throws SQLException { + // Initialize the DSLContext dslContext = getDslContext(); - dataSource = getInjector().getInstance(DataSource.class); - createTableWithNarrowConstraint(UNHEALTHY_CONTAINERS_TABLE_NAME); + + // Initialize the upgrade action + upgradeAction = new InitialConstraintUpgradeAction(); + + // Mock the SCM facade to provide the DataSource + mockScmFacade = mock(ReconStorageContainerManagerFacade.class); + DataSource dataSource = getInjector().getInstance(DataSource.class); + when(mockScmFacade.getDataSource()).thenReturn(dataSource); + + // Set the DataSource and DSLContext directly + upgradeAction.setDslContext(dslContext); + + // Check if the table already exists + try (Connection conn = dataSource.getConnection()) { + DatabaseMetaData dbMetaData = conn.getMetaData(); + ResultSet tables = dbMetaData.getTables(null, null, UNHEALTHY_CONTAINERS_TABLE_NAME, null); + if (!tables.next()) { + // Create the initial table if it does not exist + dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME) + .column("container_id", org.jooq.impl.SQLDataType.BIGINT + .nullable(false)) + .column("container_state", org.jooq.impl.SQLDataType.VARCHAR(16) + .nullable(false)) + .constraint(DSL.constraint("pk_container_id") + .primaryKey("container_id", "container_state")) + .execute(); + } + } } @Test - public void testUpgradeExpandsAllowedStates() - throws Exception { - InitialConstraintUpgradeAction action = new InitialConstraintUpgradeAction(); - action.execute(dataSource); - - insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 1L, "MISSING"); - insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 2L, "REPLICA_MISMATCH"); - insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 3L, "EMPTY_MISSING"); - insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 4L, "NEGATIVE_SIZE"); - insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 5L, "ALL_REPLICAS_BAD"); - - assertThrows(org.jooq.exception.DataAccessException.class, - () -> insertState(UNHEALTHY_CONTAINERS_TABLE_NAME, 6L, - "INVALID_STATE")); + public void testUpgradeAppliesConstraintModificationForAllStates() throws SQLException { + // Run the upgrade action + upgradeAction.execute(mockScmFacade.getDataSource()); + + // Iterate over all valid states and insert records + for (ContainerSchemaDefinition.UnHealthyContainerStates state : + ContainerSchemaDefinition.UnHealthyContainerStates.values()) { + dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) + .columns( + field(name("container_id")), + field(name("container_state")), + field(name("in_state_since")), + field(name("expected_replica_count")), + field(name("actual_replica_count")), + field(name("replica_delta")), + field(name("reason")) + ) + .values( + System.currentTimeMillis(), // Unique container_id for each record + state.name(), System.currentTimeMillis(), 3, 2, 1, "Replica count mismatch" + ) + .execute(); + } + + // Verify that the number of inserted records matches the number of enum values + int count = dslContext.fetchCount(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)); + assertEquals(ContainerSchemaDefinition.UnHealthyContainerStates.values().length, + count, "Expected one record for each valid state"); + + // Try inserting an invalid state (should fail due to constraint) + assertThrows(org.jooq.exception.DataAccessException.class, () -> + dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) + .columns( + field(name("container_id")), + field(name("container_state")), + field(name("in_state_since")), + field(name("expected_replica_count")), + field(name("actual_replica_count")), + field(name("replica_delta")), + field(name("reason")) + ) + .values(999L, "INVALID_STATE", System.currentTimeMillis(), 3, 2, 1, + "Invalid state test").execute(), + "Inserting an invalid container_state should fail due to the constraint"); } - private void createTableWithNarrowConstraint(String tableName) { - dslContext.dropTableIfExists(tableName).execute(); - dslContext.createTable(tableName) - .column("container_id", SQLDataType.BIGINT.nullable(false)) - .column("container_state", SQLDataType.VARCHAR(32).nullable(false)) - .column("in_state_since", SQLDataType.BIGINT.nullable(false)) - .column("expected_replica_count", SQLDataType.INTEGER.nullable(false)) - .column("actual_replica_count", SQLDataType.INTEGER.nullable(false)) - .column("replica_delta", SQLDataType.INTEGER.nullable(false)) - .column("reason", SQLDataType.VARCHAR(500).nullable(true)) - .constraint(DSL.constraint("pk_" + tableName.toLowerCase()) - .primaryKey("container_id", "container_state")) - .constraint(DSL.constraint(tableName + "_ck1") - .check(field(name("container_state")) - .in( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING - .toString(), - ContainerSchemaDefinition.UnHealthyContainerStates - .UNDER_REPLICATED.toString(), - ContainerSchemaDefinition.UnHealthyContainerStates - .OVER_REPLICATED.toString(), - ContainerSchemaDefinition.UnHealthyContainerStates - .MIS_REPLICATED.toString()))) - .execute(); + @Test + public void testInsertionWithNullContainerState() { + assertThrows(org.jooq.exception.DataAccessException.class, () -> { + dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) + .columns( + field(name("container_id")), + field(name("container_state")), + field(name("in_state_since")), + field(name("expected_replica_count")), + field(name("actual_replica_count")), + field(name("replica_delta")), + field(name("reason")) + ) + .values( + 100L, // container_id + null, // container_state is NULL + System.currentTimeMillis(), 3, 2, 1, "Testing NULL state" + ) + .execute(); + }, "Inserting a NULL container_state should fail due to the NOT NULL constraint"); } - private void insertState(String tableName, long containerId, String state) { - dslContext.insertInto(DSL.table(tableName)) + @Test + public void testDuplicatePrimaryKeyInsertion() throws SQLException { + // Insert the first record + dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) .columns( field(name("container_id")), field(name("container_state")), @@ -98,15 +161,28 @@ private void insertState(String tableName, long containerId, String state) { field(name("expected_replica_count")), field(name("actual_replica_count")), field(name("replica_delta")), - field(name("reason"))) - .values( - containerId, - state, - System.currentTimeMillis(), - 3, - 2, - -1, - "test") + field(name("reason")) + ) + .values(200L, "MISSING", System.currentTimeMillis(), 3, 2, 1, "First insertion" + ) .execute(); + + // Try inserting a duplicate record with the same primary key + assertThrows(org.jooq.exception.DataAccessException.class, () -> { + dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) + .columns( + field(name("container_id")), + field(name("container_state")), + field(name("in_state_since")), + field(name("expected_replica_count")), + field(name("actual_replica_count")), + field(name("replica_delta")), + field(name("reason")) + ) + .values(200L, "MISSING", System.currentTimeMillis(), 3, 2, 1, "Duplicate insertion" + ) + .execute(); + }, "Inserting a duplicate primary key should fail due to the primary key constraint"); } + } From 46401ff53fa61f7d79a0d70ab290ebb6964611e9 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 27 Feb 2026 15:25:02 +0530 Subject: [PATCH 28/43] HDDS-13891. Added back the upgrade action claseses deleted by mistake. --- ...e.java => MonitoringReplicationQueue.java} | 2 +- .../replication/ReplicationManager.java | 2 +- .../hadoop/ozone/recon/TestReconTasks.java | 219 ++++++++++++++++++ .../recon/fsck/ReconReplicationManager.java | 6 +- .../InitialConstraintUpgradeAction.java | 4 +- ...healthyContainerReplicaMismatchAction.java | 1 - .../recon/fsck/TestContainerHealthTask.java | 93 ++++++++ 7 files changed, 319 insertions(+), 8 deletions(-) rename hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/{NoOpsReplicationQueue.java => MonitoringReplicationQueue.java} (95%) create mode 100644 hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java create mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NoOpsReplicationQueue.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/MonitoringReplicationQueue.java similarity index 95% rename from hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NoOpsReplicationQueue.java rename to hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/MonitoringReplicationQueue.java index 375540d69d98..0c0b94cc6c19 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/NoOpsReplicationQueue.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/MonitoringReplicationQueue.java @@ -22,7 +22,7 @@ * checking containers in a read-only mode, where we don't want to queue them * for replication. */ -public class NoOpsReplicationQueue extends ReplicationQueue { +public class MonitoringReplicationQueue extends ReplicationQueue { @Override public void enqueue(ContainerHealthResult.UnderReplicatedHealthResult diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java index cb8cea4cd1b7..fa01dd95f278 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java @@ -190,7 +190,7 @@ public class ReplicationManager implements SCMService, ContainerReplicaPendingOp private final OverReplicatedProcessor overReplicatedProcessor; private final HealthCheck containerCheckChain; private final ReplicationQueue noOpsReplicationQueue = - new NoOpsReplicationQueue(); + new MonitoringReplicationQueue(); /** * Constructs ReplicationManager instance with the given configuration. diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java new file mode 100644 index 000000000000..aa4541a33ecb --- /dev/null +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.recon; + +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; +import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; +import static org.apache.hadoop.ozone.container.ozoneimpl.TestOzoneContainer.runTestOzoneContainerViaDataNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.time.Duration; +import java.util.List; +import org.apache.hadoop.hdds.client.RatisReplicationConfig; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.protocol.proto.HddsProtos; +import org.apache.hadoop.hdds.scm.XceiverClientGrpc; +import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ContainerManager; +import org.apache.hadoop.hdds.scm.pipeline.Pipeline; +import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; +import org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher; +import org.apache.hadoop.hdds.scm.server.StorageContainerManager; +import org.apache.hadoop.hdds.utils.IOUtils; +import org.apache.hadoop.hdds.utils.db.RDBBatchOperation; +import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; +import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; +import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; +import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +import org.apache.ozone.test.GenericTestUtils; +import org.apache.ozone.test.LambdaTestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; + +/** + * Integration Tests for Recon SCM tasks using ContainerHealthTaskV2. + */ +public class TestReconTasks { + private MiniOzoneCluster cluster; + private OzoneConfiguration conf; + private ReconService recon; + + @BeforeEach + public void init() throws Exception { + conf = new OzoneConfiguration(); + conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); + conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); + + ReconTaskConfig taskConfig = conf.getObject(ReconTaskConfig.class); + taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); + conf.setFromObject(taskConfig); + + conf.set("ozone.scm.stale.node.interval", "6s"); + conf.set("ozone.scm.dead.node.interval", "8s"); + recon = new ReconService(conf); + cluster = MiniOzoneCluster.newBuilder(conf).setNumDatanodes(1) + .addService(recon) + .build(); + cluster.waitForClusterToBeReady(); + cluster.waitForPipelineTobeReady(ONE, 30000); + GenericTestUtils.setLogLevel(SCMDatanodeHeartbeatDispatcher.class, + Level.DEBUG); + } + + @AfterEach + public void shutdown() { + if (cluster != null) { + cluster.shutdown(); + } + } + + @Test + public void testSyncSCMContainerInfo() throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + StorageContainerManager scm = cluster.getStorageContainerManager(); + ContainerManager scmContainerManager = scm.getContainerManager(); + ContainerManager reconContainerManager = reconScm.getContainerManager(); + final ContainerInfo container1 = scmContainerManager.allocateContainer( + RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.ONE), "admin"); + final ContainerInfo container2 = scmContainerManager.allocateContainer( + RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.ONE), "admin"); + scmContainerManager.updateContainerState(container1.containerID(), + HddsProtos.LifeCycleEvent.FINALIZE); + scmContainerManager.updateContainerState(container2.containerID(), + HddsProtos.LifeCycleEvent.FINALIZE); + scmContainerManager.updateContainerState(container1.containerID(), + HddsProtos.LifeCycleEvent.CLOSE); + scmContainerManager.updateContainerState(container2.containerID(), + HddsProtos.LifeCycleEvent.CLOSE); + int scmContainersCount = scmContainerManager.getContainers().size(); + int reconContainersCount = reconContainerManager.getContainers().size(); + assertNotEquals(scmContainersCount, reconContainersCount); + reconScm.syncWithSCMContainerInfo(); + reconContainersCount = reconContainerManager.getContainers().size(); + assertEquals(scmContainersCount, reconContainersCount); + } + + @Test + public void testContainerHealthTaskV2WithSCMSync() throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + StorageContainerManager scm = cluster.getStorageContainerManager(); + PipelineManager reconPipelineManager = reconScm.getPipelineManager(); + PipelineManager scmPipelineManager = scm.getPipelineManager(); + + LambdaTestUtils.await(60000, 5000, + () -> (!reconPipelineManager.getPipelines().isEmpty())); + + ContainerManager scmContainerManager = scm.getContainerManager(); + ReconContainerManager reconContainerManager = + (ReconContainerManager) reconScm.getContainerManager(); + + ContainerInfo containerInfo = scmContainerManager.allocateContainer( + RatisReplicationConfig.getInstance(ONE), "test"); + long containerID = containerInfo.getContainerID(); + + Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); + runTestOzoneContainerViaDataNode(containerID, client); + + assertEquals(scmContainerManager.getContainers(), + reconContainerManager.getContainers()); + + cluster.shutdownHddsDatanode(pipeline.getFirstNode()); + + LambdaTestUtils.await(120000, 6000, () -> { + List allMissingContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + return allMissingContainers.size() == 1; + }); + + cluster.restartHddsDatanode(pipeline.getFirstNode(), true); + + LambdaTestUtils.await(120000, 10000, () -> { + List allMissingContainers = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + return allMissingContainers.isEmpty(); + }); + IOUtils.closeQuietly(client); + } + + @Test + public void testContainerHealthTaskV2EmptyMissingContainerDownNode() + throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + ReconContainerMetadataManager reconContainerMetadataManager = + recon.getReconServer().getReconContainerMetadataManager(); + StorageContainerManager scm = cluster.getStorageContainerManager(); + PipelineManager scmPipelineManager = scm.getPipelineManager(); + ReconContainerManager reconContainerManager = + (ReconContainerManager) reconScm.getContainerManager(); + + ContainerInfo containerInfo = scm.getContainerManager() + .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); + long containerID = containerInfo.getContainerID(); + + // Explicitly set key count to 0 so missing classification becomes EMPTY_MISSING. + try (RDBBatchOperation batch = RDBBatchOperation.newAtomicOperation()) { + reconContainerMetadataManager.batchStoreContainerKeyCounts(batch, containerID, 0L); + reconContainerMetadataManager.commitBatchOperation(batch); + } + + Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); + runTestOzoneContainerViaDataNode(containerID, client); + cluster.shutdownHddsDatanode(pipeline.getFirstNode()); + + LambdaTestUtils.await(120000, 6000, () -> { + List emptyMissing = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); + return emptyMissing.size() == 1; + }); + + cluster.restartHddsDatanode(pipeline.getFirstNode(), true); + LambdaTestUtils.await(120000, 10000, () -> { + List emptyMissing = + reconContainerManager.getContainerSchemaManagerV2() + .getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); + return emptyMissing.isEmpty(); + }); + IOUtils.closeQuietly(client); + } +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index f465a1bccaa7..9ba5b425e117 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -32,7 +32,7 @@ import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; import org.apache.hadoop.hdds.scm.container.ContainerReplica; -import org.apache.hadoop.hdds.scm.container.replication.NoOpsReplicationQueue; +import org.apache.hadoop.hdds.scm.container.replication.MonitoringReplicationQueue; 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; @@ -274,7 +274,7 @@ private boolean hasDataChecksumMismatch(Set replicas) { *

    Differences from SCM's processAll():

    *
      *
    • Uses ReconReplicationManagerReport (captures all containers)
    • - *
    • Uses NoOpsReplicationQueue (doesn't enqueue commands)
    • + *
    • Uses MonitoringReplicationQueue (doesn't enqueue commands)
    • *
    • Adds REPLICA_MISMATCH detection (not done by SCM)
    • *
    • Stores results in database instead of just keeping in-memory report
    • *
    @@ -287,7 +287,7 @@ public synchronized void processAll() { // Use extended report that captures ALL containers, not just 100 samples final ReconReplicationManagerReport report = new ReconReplicationManagerReport(); - final ReplicationQueue nullQueue = new NoOpsReplicationQueue(); + final ReplicationQueue nullQueue = new MonitoringReplicationQueue(); // Get all containers (same as parent) final List containers = containerManager.getContainers(); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java index d9f7eef54344..ea8af99d96e6 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java @@ -82,8 +82,8 @@ private void addUpdatedConstraint() { dslContext.alterTable(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME) .add(DSL.constraint(ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1") - .check(field(name("container_state")) - .in(enumStates))) + .check(field(name("container_state")) + .in(enumStates))) .execute(); LOG.info("Added the updated constraint to the UNHEALTHY_CONTAINERS table for enum state values: {}", diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java index c2ce4ba8bf05..ebf8556f5c49 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainerReplicaMismatchAction.java @@ -39,7 +39,6 @@ */ @UpgradeActionRecon(feature = UNHEALTHY_CONTAINER_REPLICA_MISMATCH) public class UnhealthyContainerReplicaMismatchAction implements ReconUpgradeAction { - private static final Logger LOG = LoggerFactory.getLogger(UnhealthyContainerReplicaMismatchAction.class); private DSLContext dslContext; 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 new file mode 100644 index 000000000000..cf271936dac1 --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/fsck/TestContainerHealthTask.java @@ -0,0 +1,93 @@ +/* + * 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.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.time.Duration; +import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; +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.junit.jupiter.api.Test; + +/** + * Unit tests for ContainerHealthTaskV2 execution flow. + */ +public class TestContainerHealthTask { + + @Test + public void testRunTaskInvokesReconReplicationManagerProcessAll() + throws Exception { + ReconReplicationManager reconReplicationManager = + mock(ReconReplicationManager.class); + ReconStorageContainerManagerFacade reconScm = + mock(ReconStorageContainerManagerFacade.class); + when(reconScm.getReplicationManager()).thenReturn(reconReplicationManager); + + ContainerHealthTaskV2 task = + new ContainerHealthTaskV2( + createTaskConfig(), + createTaskStatusUpdaterManagerMock(), + reconScm); + + task.runTask(); + verify(reconReplicationManager, times(1)).processAll(); + } + + @Test + public void testRunTaskPropagatesProcessAllFailure() throws Exception { + ReconReplicationManager reconReplicationManager = + mock(ReconReplicationManager.class); + ReconStorageContainerManagerFacade reconScm = + mock(ReconStorageContainerManagerFacade.class); + when(reconScm.getReplicationManager()).thenReturn(reconReplicationManager); + RuntimeException expected = new RuntimeException("processAll failed"); + org.mockito.Mockito.doThrow(expected).when(reconReplicationManager) + .processAll(); + + ContainerHealthTaskV2 task = + new ContainerHealthTaskV2( + createTaskConfig(), + createTaskStatusUpdaterManagerMock(), + reconScm); + + RuntimeException thrown = + assertThrows(RuntimeException.class, task::runTask); + assertEquals("processAll failed", thrown.getMessage()); + } + + private ReconTaskConfig createTaskConfig() { + ReconTaskConfig taskConfig = new ReconTaskConfig(); + taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); + return taskConfig; + } + + private ReconTaskStatusUpdaterManager createTaskStatusUpdaterManagerMock() { + ReconTaskStatusUpdaterManager manager = + mock(ReconTaskStatusUpdaterManager.class); + ReconTaskStatusUpdater updater = mock(ReconTaskStatusUpdater.class); + when(manager.getTaskStatusUpdater("ContainerHealthTaskV2")).thenReturn(updater); + return manager; + } +} From 91fd644ae02ba2eb1085ad3695c55497d5557717 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 27 Feb 2026 18:56:14 +0530 Subject: [PATCH 29/43] HDDS-13891. Failed tests. --- .../hadoop/ozone/recon/TestReconTasks.java | 93 ++++++++++--------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index aa4541a33ecb..223d30a4876c 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -39,6 +39,7 @@ import org.apache.hadoop.hdds.utils.IOUtils; import org.apache.hadoop.hdds.utils.db.RDBBatchOperation; import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.UniformDatanodesFactory; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; @@ -47,7 +48,8 @@ import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.apache.ozone.test.GenericTestUtils; import org.apache.ozone.test.LambdaTestUtils; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.event.Level; @@ -56,12 +58,13 @@ * Integration Tests for Recon SCM tasks using ContainerHealthTaskV2. */ public class TestReconTasks { - private MiniOzoneCluster cluster; - private OzoneConfiguration conf; - private ReconService recon; + private static MiniOzoneCluster cluster; + private static OzoneConfiguration conf; + private static ReconService recon; + private static ReconContainerManager reconContainerManager; - @BeforeEach - public void init() throws Exception { + @BeforeAll + public static void init() throws Exception { conf = new OzoneConfiguration(); conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); @@ -73,17 +76,30 @@ public void init() throws Exception { conf.set("ozone.scm.stale.node.interval", "6s"); conf.set("ozone.scm.dead.node.interval", "8s"); recon = new ReconService(conf); - cluster = MiniOzoneCluster.newBuilder(conf).setNumDatanodes(1) + cluster = MiniOzoneCluster.newBuilder(conf) + .setNumDatanodes(1) + .setDatanodeFactory(UniformDatanodesFactory.newBuilder().build()) .addService(recon) .build(); cluster.waitForClusterToBeReady(); cluster.waitForPipelineTobeReady(ONE, 30000); + + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); GenericTestUtils.setLogLevel(SCMDatanodeHeartbeatDispatcher.class, Level.DEBUG); } - @AfterEach - public void shutdown() { + @BeforeEach + public void cleanupBeforeEach() { + reconContainerManager.getContainerSchemaManagerV2() + .clearAllUnhealthyContainerRecords(); + } + + @AfterAll + public static void shutdown() { if (cluster != null) { cluster.shutdown(); } @@ -96,7 +112,8 @@ public void testSyncSCMContainerInfo() throws Exception { recon.getReconServer().getReconStorageContainerManager(); StorageContainerManager scm = cluster.getStorageContainerManager(); ContainerManager scmContainerManager = scm.getContainerManager(); - ContainerManager reconContainerManager = reconScm.getContainerManager(); + ContainerManager reconCm = reconScm.getContainerManager(); + final ContainerInfo container1 = scmContainerManager.allocateContainer( RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.ONE), "admin"); final ContainerInfo container2 = scmContainerManager.allocateContainer( @@ -109,11 +126,12 @@ public void testSyncSCMContainerInfo() throws Exception { HddsProtos.LifeCycleEvent.CLOSE); scmContainerManager.updateContainerState(container2.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + int scmContainersCount = scmContainerManager.getContainers().size(); - int reconContainersCount = reconContainerManager.getContainers().size(); + int reconContainersCount = reconCm.getContainers().size(); assertNotEquals(scmContainersCount, reconContainersCount); reconScm.syncWithSCMContainerInfo(); - reconContainersCount = reconContainerManager.getContainers().size(); + reconContainersCount = reconCm.getContainers().size(); assertEquals(scmContainersCount, reconContainersCount); } @@ -130,40 +148,33 @@ public void testContainerHealthTaskV2WithSCMSync() throws Exception { () -> (!reconPipelineManager.getPipelines().isEmpty())); ContainerManager scmContainerManager = scm.getContainerManager(); - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); + ReconContainerManager reconCm = (ReconContainerManager) reconScm.getContainerManager(); ContainerInfo containerInfo = scmContainerManager.allocateContainer( RatisReplicationConfig.getInstance(ONE), "test"); long containerID = containerInfo.getContainerID(); - Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); runTestOzoneContainerViaDataNode(containerID, client); - - assertEquals(scmContainerManager.getContainers(), - reconContainerManager.getContainers()); + assertEquals(scmContainerManager.getContainers(), reconCm.getContainers()); cluster.shutdownHddsDatanode(pipeline.getFirstNode()); - LambdaTestUtils.await(120000, 6000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, - 0L, 0L, 1000); - return allMissingContainers.size() == 1; + List allMissing = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + return allMissing.size() == 1; }); cluster.restartHddsDatanode(pipeline.getFirstNode(), true); - LambdaTestUtils.await(120000, 10000, () -> { - List allMissingContainers = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, - 0L, 0L, 1000); - return allMissingContainers.isEmpty(); + List allMissing = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + return allMissing.isEmpty(); }); IOUtils.closeQuietly(client); } @@ -178,14 +189,12 @@ public void testContainerHealthTaskV2EmptyMissingContainerDownNode() recon.getReconServer().getReconContainerMetadataManager(); StorageContainerManager scm = cluster.getStorageContainerManager(); PipelineManager scmPipelineManager = scm.getPipelineManager(); - ReconContainerManager reconContainerManager = - (ReconContainerManager) reconScm.getContainerManager(); + ReconContainerManager reconCm = (ReconContainerManager) reconScm.getContainerManager(); ContainerInfo containerInfo = scm.getContainerManager() .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); long containerID = containerInfo.getContainerID(); - // Explicitly set key count to 0 so missing classification becomes EMPTY_MISSING. try (RDBBatchOperation batch = RDBBatchOperation.newAtomicOperation()) { reconContainerMetadataManager.batchStoreContainerKeyCounts(batch, containerID, 0L); reconContainerMetadataManager.commitBatchOperation(batch); @@ -198,20 +207,18 @@ public void testContainerHealthTaskV2EmptyMissingContainerDownNode() LambdaTestUtils.await(120000, 6000, () -> { List emptyMissing = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, - 0L, 0L, 1000); + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); return emptyMissing.size() == 1; }); cluster.restartHddsDatanode(pipeline.getFirstNode(), true); LambdaTestUtils.await(120000, 10000, () -> { List emptyMissing = - reconContainerManager.getContainerSchemaManagerV2() - .getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, - 0L, 0L, 1000); + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); return emptyMissing.isEmpty(); }); IOUtils.closeQuietly(client); From b67f397daa0426790b72872204b3d728329d3369 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 27 Feb 2026 22:10:52 +0530 Subject: [PATCH 30/43] HDDS-13891. Failed tests. --- .../hadoop/ozone/recon/TestReconTasks.java | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 223d30a4876c..6011ca4ba4da 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -48,8 +48,7 @@ import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.apache.ozone.test.GenericTestUtils; import org.apache.ozone.test.LambdaTestUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.event.Level; @@ -58,13 +57,12 @@ * Integration Tests for Recon SCM tasks using ContainerHealthTaskV2. */ public class TestReconTasks { - private static MiniOzoneCluster cluster; - private static OzoneConfiguration conf; - private static ReconService recon; - private static ReconContainerManager reconContainerManager; + private MiniOzoneCluster cluster; + private OzoneConfiguration conf; + private ReconService recon; - @BeforeAll - public static void init() throws Exception { + @BeforeEach + public void init() throws Exception { conf = new OzoneConfiguration(); conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); @@ -83,23 +81,12 @@ public static void init() throws Exception { .build(); cluster.waitForClusterToBeReady(); cluster.waitForPipelineTobeReady(ONE, 30000); - - ReconStorageContainerManagerFacade reconScm = - (ReconStorageContainerManagerFacade) - recon.getReconServer().getReconStorageContainerManager(); - reconContainerManager = (ReconContainerManager) reconScm.getContainerManager(); GenericTestUtils.setLogLevel(SCMDatanodeHeartbeatDispatcher.class, Level.DEBUG); } - @BeforeEach - public void cleanupBeforeEach() { - reconContainerManager.getContainerSchemaManagerV2() - .clearAllUnhealthyContainerRecords(); - } - - @AfterAll - public static void shutdown() { + @AfterEach + public void shutdown() { if (cluster != null) { cluster.shutdown(); } From 423ad62197c87adc50fedb54571952909330c295 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sat, 28 Feb 2026 19:57:33 +0530 Subject: [PATCH 31/43] HDDS-13891. Failed tests. --- .../org/apache/hadoop/ozone/recon/TestReconTasks.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 6011ca4ba4da..611facb824b3 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -127,6 +127,8 @@ public void testContainerHealthTaskV2WithSCMSync() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) recon.getReconServer().getReconStorageContainerManager(); + ReconContainerMetadataManager reconContainerMetadataManager = + recon.getReconServer().getReconContainerMetadataManager(); StorageContainerManager scm = cluster.getStorageContainerManager(); PipelineManager reconPipelineManager = reconScm.getPipelineManager(); PipelineManager scmPipelineManager = scm.getPipelineManager(); @@ -142,6 +144,12 @@ public void testContainerHealthTaskV2WithSCMSync() throws Exception { long containerID = containerInfo.getContainerID(); Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + // Ensure this is treated as MISSING (not EMPTY_MISSING) when DN goes down. + try (RDBBatchOperation batch = RDBBatchOperation.newAtomicOperation()) { + reconContainerMetadataManager.batchStoreContainerKeyCounts(batch, containerID, 2L); + reconContainerMetadataManager.commitBatchOperation(batch); + } + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); runTestOzoneContainerViaDataNode(containerID, client); assertEquals(scmContainerManager.getContainers(), reconCm.getContainers()); From 1f0d5a31201057e5502a6b7d196a00b1ec44874e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sat, 28 Feb 2026 23:37:55 +0530 Subject: [PATCH 32/43] HDDS-13891. Failed tests. --- .../hadoop/ozone/recon/TestReconTasks.java | 639 ++++++++++++++++-- .../ContainerHealthSchemaManagerV2.java | 20 +- 2 files changed, 607 insertions(+), 52 deletions(-) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 611facb824b3..b0b4c52394b8 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -20,30 +20,39 @@ import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_REPORT_INTERVAL; import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_PIPELINE_REPORT_INTERVAL; import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; +import static org.apache.hadoop.ozone.container.ozoneimpl.TestOzoneContainer.createContainerForTesting; import static org.apache.hadoop.ozone.container.ozoneimpl.TestOzoneContainer.runTestOzoneContainerViaDataNode; 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.assertTrue; import java.time.Duration; import java.util.List; +import java.util.Set; import org.apache.hadoop.hdds.client.RatisReplicationConfig; import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.protocol.DatanodeDetails; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto; import org.apache.hadoop.hdds.scm.XceiverClientGrpc; +import org.apache.hadoop.hdds.scm.XceiverClientRatis; +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.pipeline.Pipeline; import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; import org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher; import org.apache.hadoop.hdds.scm.server.StorageContainerManager; import org.apache.hadoop.hdds.utils.IOUtils; -import org.apache.hadoop.hdds.utils.db.RDBBatchOperation; +import org.apache.hadoop.ozone.HddsDatanodeService; import org.apache.hadoop.ozone.MiniOzoneCluster; import org.apache.hadoop.ozone.UniformDatanodesFactory; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; -import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; import org.apache.ozone.test.GenericTestUtils; @@ -54,9 +63,47 @@ import org.slf4j.event.Level; /** - * Integration Tests for Recon SCM tasks using ContainerHealthTaskV2. + * Integration tests for Recon's ContainerHealthTaskV2. + * + *

    Covered unhealthy states (all states tracked in the UNHEALTHY_CONTAINERS + * table except the dead {@code ALL_REPLICAS_BAD} state):

    + *
      + *
    • {@code UNDER_REPLICATED}: RF3 CLOSED container loses one replica (node down) + * → {@link #testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure}
    • + *
    • {@code EMPTY_MISSING}: RF1 OPEN container's only replica lost (node down, + * container has no OM-tracked keys) → + * {@link #testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost}
    • + *
    • {@code MISSING}: RF1 CLOSED container with OM-tracked keys loses its only + * replica (metadata manipulation) → + * {@link #testContainerHealthTaskV2DetectsMissingForContainerWithKeys}
    • + *
    • {@code OVER_REPLICATED}: RF1 CLOSED container gains a phantom extra replica + * (metadata injection) → + * {@link #testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize}
    • + *
    • {@code NEGATIVE_SIZE}: Co-detected alongside {@code OVER_REPLICATED} when + * the container's {@code usedBytes} is negative → + * {@link #testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize}
    • + *
    • {@code REPLICA_MISMATCH}: RF3 CLOSED container where one replica reports a + * different data checksum (metadata injection) → + * {@link #testContainerHealthTaskV2DetectsReplicaMismatch}
    • + *
    + * + *

    States NOT covered:

    + *
      + *
    • {@code MIS_REPLICATED}: Requires a rack-aware placement policy with a specific + * multi-rack topology — not practical to set up in a mini-cluster integration + * test.
    • + *
    • {@code ALL_REPLICAS_BAD}: Defined in the schema enum but currently not + * populated by {@code ReconReplicationManager} — this is a dead code path.
    • + *
    */ public class TestReconTasks { + private static final int PIPELINE_READY_TIMEOUT_MS = 30000; + // Dead-node fires in stale(3s)+dead(4s)=7s; 20s is ample headroom. + private static final int STATE_TRANSITION_TIMEOUT_MS = 20000; + // Container reports every 1s; 10s is ample to see all replicas. + private static final int REPLICA_SYNC_TIMEOUT_MS = 10000; + private static final int POLL_INTERVAL_MS = 500; + private MiniOzoneCluster cluster; private OzoneConfiguration conf; private ReconService recon; @@ -64,23 +111,39 @@ public class TestReconTasks { @BeforeEach public void init() throws Exception { conf = new OzoneConfiguration(); - conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "5s"); - conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "5s"); + // 1s reports keep replica-sync waits short (down from the 2s default). + conf.set(HDDS_CONTAINER_REPORT_INTERVAL, "1s"); + conf.set(HDDS_PIPELINE_REPORT_INTERVAL, "1s"); ReconTaskConfig taskConfig = conf.getObject(ReconTaskConfig.class); - taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(10)); + taskConfig.setMissingContainerTaskInterval(Duration.ofSeconds(2)); conf.setFromObject(taskConfig); - conf.set("ozone.scm.stale.node.interval", "6s"); - conf.set("ozone.scm.dead.node.interval", "8s"); + conf.set("ozone.scm.stale.node.interval", "3s"); + conf.set("ozone.scm.dead.node.interval", "4s"); + // Keep SCM's remediation processors slow so Recon can deterministically + // observe unhealthy states before SCM heals them. + conf.set("hdds.scm.replication.under.replicated.interval", "1m"); + conf.set("hdds.scm.replication.over.replicated.interval", "2m"); recon = new ReconService(conf); cluster = MiniOzoneCluster.newBuilder(conf) - .setNumDatanodes(1) + .setNumDatanodes(3) .setDatanodeFactory(UniformDatanodesFactory.newBuilder().build()) .addService(recon) .build(); cluster.waitForClusterToBeReady(); cluster.waitForPipelineTobeReady(ONE, 30000); + cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.THREE, 30000); + + // Wait for Recon's pipeline manager to be populated from SCM. This is + // separate from the cluster-level wait above (which only checks SCM). + // Doing it here once avoids a redundant 30s inner wait inside each RF3 test. + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + LambdaTestUtils.await(PIPELINE_READY_TIMEOUT_MS, POLL_INTERVAL_MS, + () -> !reconScm.getPipelineManager().getPipelines().isEmpty()); + GenericTestUtils.setLogLevel(SCMDatanodeHeartbeatDispatcher.class, Level.DEBUG); } @@ -92,6 +155,10 @@ public void shutdown() { } } + /** + * Verifies that {@code syncWithSCMContainerInfo()} pulls CLOSED containers + * from SCM into Recon when they are not yet known to Recon. + */ @Test public void testSyncSCMContainerInfo() throws Exception { ReconStorageContainerManagerFacade reconScm = @@ -122,66 +189,172 @@ public void testSyncSCMContainerInfo() throws Exception { assertEquals(scmContainersCount, reconContainersCount); } + /** + * Verifies that ContainerHealthTaskV2 correctly detects {@code UNDER_REPLICATED} + * when a CLOSED RF3 container loses one replica due to a node failure, and that the + * state clears after the node recovers. + * + *

    Key design constraint: the container MUST be in CLOSED state (not OPEN or CLOSING) + * before the health scan runs. The check chain's {@code OpenContainerHandler} and + * {@code ClosingContainerHandler} both return early (stopping the chain) for OPEN/CLOSING + * containers without recording UNDER_REPLICATED. Only {@code RatisReplicationCheckHandler} + * — reached only for CLOSED/QUASI_CLOSED containers — records UNDER_REPLICATED.

    + * + *

    Note on key counts: {@code isEmptyMissing()} uses + * {@link ContainerInfo#getNumberOfKeys()} (SCM-tracked, OM-maintained). This is + * distinct from Recon's metadata key count store. Since this container is created + * via XceiverClient (bypassing OM), its SCM key count remains 0. This is intentional + * for this test as the container has 2 replicas and will be UNDER_REPLICATED, not + * MISSING/EMPTY_MISSING, regardless of key count.

    + */ @Test - public void testContainerHealthTaskV2WithSCMSync() throws Exception { + public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() + throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) recon.getReconServer().getReconStorageContainerManager(); - ReconContainerMetadataManager reconContainerMetadataManager = - recon.getReconServer().getReconContainerMetadataManager(); StorageContainerManager scm = cluster.getStorageContainerManager(); - PipelineManager reconPipelineManager = reconScm.getPipelineManager(); PipelineManager scmPipelineManager = scm.getPipelineManager(); - LambdaTestUtils.await(60000, 5000, - () -> (!reconPipelineManager.getPipelines().isEmpty())); - ContainerManager scmContainerManager = scm.getContainerManager(); ReconContainerManager reconCm = (ReconContainerManager) reconScm.getContainerManager(); ContainerInfo containerInfo = scmContainerManager.allocateContainer( - RatisReplicationConfig.getInstance(ONE), "test"); + RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE), + "test"); long containerID = containerInfo.getContainerID(); Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); - // Ensure this is treated as MISSING (not EMPTY_MISSING) when DN goes down. - try (RDBBatchOperation batch = RDBBatchOperation.newAtomicOperation()) { - reconContainerMetadataManager.batchStoreContainerKeyCounts(batch, containerID, 2L); - reconContainerMetadataManager.commitBatchOperation(batch); - } + // Use XceiverClientRatis (not XceiverClientGrpc) for RF3 Ratis pipelines. + // XceiverClientGrpc bypasses Ratis and writes to a randomly-selected single + // node; the container then only exists on that one node. XceiverClientRatis + // propagates the CreateContainer command through Raft consensus, so all 3 + // datanodes end up with the container — which is required for Recon to + // accumulate 3 replicas. + // + // We deliberately do NOT call runTestOzoneContainerViaDataNode here because + // that helper tests UpdateContainer (a deprecated standalone-only operation) + // which is not supported via Ratis and throws StateMachineException. We only + // need the container to exist on all 3 nodes; data content is irrelevant for + // the health-state detection under test. + XceiverClientRatis client = XceiverClientRatis.newXceiverClientRatis(pipeline, conf); + client.connect(); + createContainerForTesting(client, containerID); - XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); - runTestOzoneContainerViaDataNode(containerID, client); + // Wait for Recon to receive container reports from all 3 datanodes before + // proceeding. Container reports are asynchronous (sent every 2s), so we + // must confirm Recon has registered all replicas before closing the container + // or shutting down a node. + LambdaTestUtils.await(REPLICA_SYNC_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + try { + return reconCm.getContainerReplicas( + ContainerID.valueOf(containerID)).size() == 3; + } catch (Exception e) { + return false; + } + }); assertEquals(scmContainerManager.getContainers(), reconCm.getContainers()); + // Close the container to CLOSED state in both SCM and Recon BEFORE node shutdown. + // + // Rationale: the health-check handler chain is gated by container lifecycle state: + // OpenContainerHandler → stops chain for OPEN containers (returns true) + // ClosingContainerHandler → stops chain for CLOSING containers in readOnly mode + // RatisReplicationCheckHandler → only reached for CLOSED/QUASI_CLOSED containers; + // this is the ONLY handler that records UNDER_REPLICATED + // + // syncWithSCMContainerInfo() only discovers *new* CLOSED containers, not state + // changes to already-known ones, so we apply the transition to both managers directly. + scmContainerManager.updateContainerState(containerInfo.containerID(), + HddsProtos.LifeCycleEvent.FINALIZE); + scmContainerManager.updateContainerState(containerInfo.containerID(), + HddsProtos.LifeCycleEvent.CLOSE); + reconCm.updateContainerState(containerInfo.containerID(), + HddsProtos.LifeCycleEvent.FINALIZE); + reconCm.updateContainerState(containerInfo.containerID(), + HddsProtos.LifeCycleEvent.CLOSE); + + // Shut down one datanode. DeadNodeHandler will remove its replica from + // Recon's container manager after the dead-node timeout (~4s). cluster.shutdownHddsDatanode(pipeline.getFirstNode()); - LambdaTestUtils.await(120000, 6000, () -> { - List allMissing = + forceContainerHealthScan(reconScm); + + // Wait until the container appears as UNDER_REPLICATED (2 of 3 replicas present) + // and is NOT classified as MISSING or EMPTY_MISSING. + LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + forceContainerHealthScan(reconScm); + List underReplicated = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, + 0L, 0L, 1000); + List missing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); - return allMissing.size() == 1; + List emptyMissing = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); + return containsContainerId(underReplicated, containerID) + && !containsContainerId(missing, containerID) + && !containsContainerId(emptyMissing, containerID); }); + // Restart the dead datanode and wait for UNDER_REPLICATED to clear. cluster.restartHddsDatanode(pipeline.getFirstNode(), true); - LambdaTestUtils.await(120000, 10000, () -> { - List allMissing = + forceContainerHealthScan(reconScm); + LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + forceContainerHealthScan(reconScm); + List underReplicated = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( - ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); - return allMissing.isEmpty(); + return !containsContainerId(underReplicated, containerID); }); + + // After recovery: our container must not appear in any unhealthy state. + List missingAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + assertFalse(containsContainerId(missingAfterRecovery, containerID), + "Container should not be MISSING after node recovery"); + + List emptyMissingAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); + assertFalse(containsContainerId(emptyMissingAfterRecovery, containerID), + "Container should not be EMPTY_MISSING after node recovery"); + IOUtils.closeQuietly(client); } + /** + * Verifies that ContainerHealthTaskV2 correctly detects {@code EMPTY_MISSING} + * (not {@code MISSING} or {@code UNDER_REPLICATED}) when a CLOSING RF1 container + * loses its only replica due to a node failure, and the container has no + * OM-tracked keys (i.e., {@link ContainerInfo#getNumberOfKeys()} == 0). + * + *

    Classification logic: When a CLOSING container has zero replicas, + * {@code ClosingContainerHandler} samples it as {@code MISSING}. Then + * {@code handleMissingContainer()} calls {@code isEmptyMissing()} which checks + * {@link ContainerInfo#getNumberOfKeys()}. Since the container was created via + * XceiverClient bypassing Ozone Manager, SCM's key count is 0, so the container + * is classified as {@code EMPTY_MISSING} rather than {@code MISSING}.

    + * + *

    Note: this test relies on the CLOSING-state path (not the CLOSED-state path), + * so no explicit container close is needed before node shutdown. The dead-node + * handler fires CLOSE_CONTAINER for the OPEN container, transitioning it to + * CLOSING; then removes the lone replica; leaving a CLOSING container with 0 + * replicas for the health scan to classify as EMPTY_MISSING.

    + */ @Test - public void testContainerHealthTaskV2EmptyMissingContainerDownNode() + public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) recon.getReconServer().getReconStorageContainerManager(); - ReconContainerMetadataManager reconContainerMetadataManager = - recon.getReconServer().getReconContainerMetadataManager(); StorageContainerManager scm = cluster.getStorageContainerManager(); PipelineManager scmPipelineManager = scm.getPipelineManager(); ReconContainerManager reconCm = (ReconContainerManager) reconScm.getContainerManager(); @@ -189,33 +362,411 @@ public void testContainerHealthTaskV2EmptyMissingContainerDownNode() ContainerInfo containerInfo = scm.getContainerManager() .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); long containerID = containerInfo.getContainerID(); - - try (RDBBatchOperation batch = RDBBatchOperation.newAtomicOperation()) { - reconContainerMetadataManager.batchStoreContainerKeyCounts(batch, containerID, 0L); - reconContainerMetadataManager.commitBatchOperation(batch); - } - Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); runTestOzoneContainerViaDataNode(containerID, client); + + // Wait for Recon to receive the container report from the single datanode. + // This ensures DeadNodeHandler can find and remove the replica when the node dies. + LambdaTestUtils.await(REPLICA_SYNC_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + try { + return reconCm.getContainerReplicas( + ContainerID.valueOf(containerID)).size() == 1; + } catch (Exception e) { + return false; + } + }); + cluster.shutdownHddsDatanode(pipeline.getFirstNode()); + forceContainerHealthScan(reconScm); - LambdaTestUtils.await(120000, 6000, () -> { + // Wait until the container appears as EMPTY_MISSING (not MISSING or UNDER_REPLICATED). + // EMPTY_MISSING means: 0 replicas AND 0 OM-tracked keys (no data loss risk). + LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + forceContainerHealthScan(reconScm); List emptyMissing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); - return emptyMissing.size() == 1; + List missing = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + List underReplicated = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, + 0L, 0L, 1000); + // EMPTY_MISSING must be set; MISSING and UNDER_REPLICATED must NOT be set + // for this specific container (0 replicas → missing, not under-replicated; + // 0 keys → empty-missing, not regular missing). + return containsContainerId(emptyMissing, containerID) + && !containsContainerId(missing, containerID) + && !containsContainerId(underReplicated, containerID); }); + // Restart the node and wait for EMPTY_MISSING to clear. cluster.restartHddsDatanode(pipeline.getFirstNode(), true); - LambdaTestUtils.await(120000, 10000, () -> { + forceContainerHealthScan(reconScm); + LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + forceContainerHealthScan(reconScm); List emptyMissing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); - return emptyMissing.isEmpty(); + return !containsContainerId(emptyMissing, containerID); + }); + + // After recovery: our container must not appear in any unhealthy state. + List missingAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + assertFalse(containsContainerId(missingAfterRecovery, containerID), + "Container should not be MISSING after node recovery"); + + List underReplicatedAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, + 0L, 0L, 1000); + assertFalse(containsContainerId(underReplicatedAfterRecovery, containerID), + "Container should not be UNDER_REPLICATED after node recovery"); + + IOUtils.closeQuietly(client); + } + + /** + * Verifies that ContainerHealthTaskV2 correctly detects {@code MISSING} + * (distinct from {@code EMPTY_MISSING}) when a CLOSED RF1 container that has + * OM-tracked keys loses its only replica. + * + *

    Classification logic: {@code MISSING} is chosen over {@code EMPTY_MISSING} + * when {@link ContainerInfo#getNumberOfKeys()} > 0. In this test we directly + * set {@code numberOfKeys = 1} on Recon's in-memory {@link ContainerInfo} + * (bypassing the OM write path for test efficiency). Because + * {@link ContainerInfo} is stored by reference in the in-memory state map, + * this mutation is reflected in all subsequent health-check reads.

    + * + *

    The replica is removed from Recon's container manager directly (no node + * death required), making the test fast and deterministic. Recovery is verified + * by re-adding the replica and running another health scan.

    + */ + @Test + public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() + throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + StorageContainerManager scm = cluster.getStorageContainerManager(); + PipelineManager scmPipelineManager = scm.getPipelineManager(); + ReconContainerManager reconCm = + (ReconContainerManager) reconScm.getContainerManager(); + + ContainerInfo containerInfo = scm.getContainerManager() + .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); + long containerID = containerInfo.getContainerID(); + Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); + runTestOzoneContainerViaDataNode(containerID, client); + + // Wait for Recon to register the single replica from the datanode. + LambdaTestUtils.await(REPLICA_SYNC_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + try { + return reconCm.getContainerReplicas( + ContainerID.valueOf(containerID)).size() == 1; + } catch (Exception e) { + return false; + } + }); + + // Close the container in both SCM and Recon so the health-check chain + // reaches RatisReplicationCheckHandler (which detects MISSING for CLOSED + // containers with 0 replicas). + scm.getContainerManager().updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.FINALIZE); + scm.getContainerManager().updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + reconCm.updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.FINALIZE); + reconCm.updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + + // Simulate OM-tracked key count by setting numberOfKeys = 1 on Recon's + // in-memory ContainerInfo. getContainer() returns a direct reference to + // the object stored in the ContainerStateMap, so this mutation is visible + // to all subsequent reads (including handleMissingContainer's isEmptyMissing + // check) without needing a DB flush. + ContainerID cid = ContainerID.valueOf(containerID); + reconCm.getContainer(cid).setNumberOfKeys(1); + + // Capture the single known replica and remove it from Recon's manager to + // simulate total replica loss (no node death required). + Set replicas = reconCm.getContainerReplicas(cid); + assertEquals(1, replicas.size(), "Expected exactly 1 replica before removal"); + ContainerReplica theReplica = replicas.iterator().next(); + reconCm.removeContainerReplica(cid, theReplica); + + // Health scan: CLOSED container, 0 replicas, numberOfKeys=1 → MISSING + // (not EMPTY_MISSING because numberOfKeys > 0). + forceContainerHealthScan(reconScm); + + List missing = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + List emptyMissing = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, + 0L, 0L, 1000); + + assertTrue(containsContainerId(missing, containerID), + "Container with keys should be MISSING when all replicas are gone"); + assertFalse(containsContainerId(emptyMissing, containerID), + "Container with keys must NOT be classified as EMPTY_MISSING"); + + // Recovery: re-add the replica; the next health scan should clear MISSING. + reconCm.updateContainerReplica(cid, theReplica); + forceContainerHealthScan(reconScm); + + List missingAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, + 0L, 0L, 1000); + assertFalse(containsContainerId(missingAfterRecovery, containerID), + "Container should no longer be MISSING after replica is restored"); + + IOUtils.closeQuietly(client); + } + + /** + * Verifies that ContainerHealthTaskV2 correctly detects {@code OVER_REPLICATED} + * when a CLOSED RF1 container has more replicas in Recon than its replication + * factor, and simultaneously detects {@code NEGATIVE_SIZE} when the same + * container has a negative {@code usedBytes} value. + * + *

    Strategy: inject a phantom {@link ContainerReplica} from a second alive + * datanode directly into Recon's container manager (Recon trusts its own replica + * set without re-validating against the DN). This makes Recon believe 2 replicas + * exist for an RF1 container → OVER_REPLICATED. Setting {@code usedBytes = -1} + * on the same container's in-memory {@link ContainerInfo} triggers the + * {@code NEGATIVE_SIZE} co-detection path (which fires inside + * {@code handleReplicaStateContainer}).

    + * + *

    Recovery is verified by removing the phantom replica and resetting + * {@code usedBytes} to a non-negative value.

    + */ + @Test + public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() + throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + StorageContainerManager scm = cluster.getStorageContainerManager(); + PipelineManager scmPipelineManager = scm.getPipelineManager(); + ReconContainerManager reconCm = + (ReconContainerManager) reconScm.getContainerManager(); + + ContainerInfo containerInfo = scm.getContainerManager() + .allocateContainer(RatisReplicationConfig.getInstance(ONE), "test"); + long containerID = containerInfo.getContainerID(); + Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + + XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf); + runTestOzoneContainerViaDataNode(containerID, client); + + // Wait for Recon to register the single replica. + LambdaTestUtils.await(REPLICA_SYNC_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + try { + return reconCm.getContainerReplicas( + ContainerID.valueOf(containerID)).size() == 1; + } catch (Exception e) { + return false; + } }); + + // Close in SCM and Recon so RatisReplicationCheckHandler processes this + // container and can detect OVER_REPLICATED. + scm.getContainerManager().updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.FINALIZE); + scm.getContainerManager().updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + reconCm.updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.FINALIZE); + reconCm.updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + + ContainerID cid = ContainerID.valueOf(containerID); + + // Find a datanode that is alive in the cluster but does NOT have this + // RF1 container (any DN other than the pipeline's primary node). + DatanodeDetails primaryDn = pipeline.getFirstNode(); + DatanodeDetails secondDn = cluster.getHddsDatanodes().stream() + .map(HddsDatanodeService::getDatanodeDetails) + .filter(dd -> !dd.getUuid().equals(primaryDn.getUuid())) + .findFirst() + .orElseThrow(() -> new AssertionError("No second datanode available")); + + // Inject a phantom replica from the second DN so Recon sees 2 replicas + // for an RF1 container → OVER_REPLICATED. + ContainerReplica phantomReplica = ContainerReplica.newBuilder() + .setContainerID(cid) + .setContainerState(ContainerReplicaProto.State.CLOSED) + .setDatanodeDetails(secondDn) + .setKeyCount(0) + .setBytesUsed(0) + .setSequenceId(0) + .build(); + reconCm.updateContainerReplica(cid, phantomReplica); + + // Set usedBytes = -1 on Recon's in-memory ContainerInfo to trigger + // NEGATIVE_SIZE detection. This fires inside handleReplicaStateContainer + // alongside the OVER_REPLICATED record. + reconCm.getContainer(cid).setUsedBytes(-1L); + + forceContainerHealthScan(reconScm); + + List overReplicated = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, + 0L, 0L, 1000); + List negativeSize = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.NEGATIVE_SIZE, + 0L, 0L, 1000); + + assertTrue(containsContainerId(overReplicated, containerID), + "RF1 container with 2 replicas should be OVER_REPLICATED"); + assertTrue(containsContainerId(negativeSize, containerID), + "Container with usedBytes=-1 should be NEGATIVE_SIZE"); + + // Recovery: remove the phantom replica and restore usedBytes to a valid value. + reconCm.removeContainerReplica(cid, phantomReplica); + reconCm.getContainer(cid).setUsedBytes(0L); + forceContainerHealthScan(reconScm); + + List overReplicatedAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, + 0L, 0L, 1000); + List negativeSizeAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.NEGATIVE_SIZE, + 0L, 0L, 1000); + + assertFalse(containsContainerId(overReplicatedAfterRecovery, containerID), + "Container should no longer be OVER_REPLICATED after phantom replica removed"); + assertFalse(containsContainerId(negativeSizeAfterRecovery, containerID), + "Container should no longer be NEGATIVE_SIZE after usedBytes restored"); + IOUtils.closeQuietly(client); } + + /** + * Verifies that ContainerHealthTaskV2 correctly detects {@code REPLICA_MISMATCH} + * when replicas of a CLOSED RF3 container report different data checksums, and + * that the state clears once the checksums are made uniform again. + * + *

    Strategy: after writing data and closing the RF3 container, one replica's + * checksum is replaced with a non-zero value via {@link ContainerReplica#toBuilder()}. + * Because {@link ContainerReplica} is immutable, + * {@code reconCm.updateContainerReplica()} replaces the existing replica entry + * (keyed by {@code containerID + datanodeDetails}) with the modified copy. + * This makes the set have distinct checksums (e.g. {0, 0, 12345}), which triggers + * {@code hasDataChecksumMismatch()}'s {@code distinctChecksums > 1} check.

    + * + *

    Note: {@code REPLICA_MISMATCH} records are now properly cleaned up by + * {@code batchDeleteSCMStatesForContainers} on each scan cycle (previously they + * were excluded and could linger indefinitely after a mismatch was resolved).

    + */ + @Test + public void testContainerHealthTaskV2DetectsReplicaMismatch() throws Exception { + ReconStorageContainerManagerFacade reconScm = + (ReconStorageContainerManagerFacade) + recon.getReconServer().getReconStorageContainerManager(); + StorageContainerManager scm = cluster.getStorageContainerManager(); + PipelineManager scmPipelineManager = scm.getPipelineManager(); + ReconContainerManager reconCm = + (ReconContainerManager) reconScm.getContainerManager(); + + ContainerInfo containerInfo = scm.getContainerManager().allocateContainer( + RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE), + "test"); + long containerID = containerInfo.getContainerID(); + Pipeline pipeline = scmPipelineManager.getPipeline(containerInfo.getPipelineID()); + + // Use XceiverClientRatis so CreateContainer is committed through Raft + // consensus and all 3 nodes end up with the container. We skip + // runTestOzoneContainerViaDataNode because UpdateContainer (tested inside + // that helper) is a deprecated standalone-only operation that throws + // StateMachineException when routed through Ratis. + XceiverClientRatis client = XceiverClientRatis.newXceiverClientRatis(pipeline, conf); + client.connect(); + createContainerForTesting(client, containerID); + + // Wait for Recon to register replicas from all 3 datanodes. + ContainerID cid = ContainerID.valueOf(containerID); + LambdaTestUtils.await(REPLICA_SYNC_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { + try { + return reconCm.getContainerReplicas(cid).size() == 3; + } catch (Exception e) { + return false; + } + }); + + // Close in SCM and Recon so RatisReplicationCheckHandler is reached and + // REPLICA_MISMATCH (detected in the additional Recon-specific pass in + // ReconReplicationManager.processAll) can be evaluated. + scm.getContainerManager().updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.FINALIZE); + scm.getContainerManager().updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + reconCm.updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.FINALIZE); + reconCm.updateContainerState( + containerInfo.containerID(), HddsProtos.LifeCycleEvent.CLOSE); + + // Inject a checksum mismatch: replace one replica with an identical copy + // that has a non-zero dataChecksum. The other two replicas have checksum=0 + // (ContainerChecksums.unknown()), giving distinct checksums {0, 12345} and + // triggering distinctChecksums > 1. + Set currentReplicas = reconCm.getContainerReplicas(cid); + ContainerReplica originalReplica = currentReplicas.iterator().next(); + ContainerReplica mismatchedReplica = originalReplica.toBuilder() + .setChecksums(ContainerChecksums.of(12345L)) + .build(); + reconCm.updateContainerReplica(cid, mismatchedReplica); + + forceContainerHealthScan(reconScm); + + List replicaMismatch = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.REPLICA_MISMATCH, + 0L, 0L, 1000); + assertTrue(containsContainerId(replicaMismatch, containerID), + "Container with differing replica checksums should be REPLICA_MISMATCH"); + + // Recovery: restore the original replica (uniform checksums → no mismatch). + reconCm.updateContainerReplica(cid, originalReplica); + forceContainerHealthScan(reconScm); + + List replicaMismatchAfterRecovery = + reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + ContainerSchemaDefinition.UnHealthyContainerStates.REPLICA_MISMATCH, + 0L, 0L, 1000); + assertFalse(containsContainerId(replicaMismatchAfterRecovery, containerID), + "Container should no longer be REPLICA_MISMATCH after checksums are uniform"); + + IOUtils.closeQuietly(client); + } + + private void forceContainerHealthScan( + ReconStorageContainerManagerFacade reconScm) { + reconScm.getReplicationManager().processAll(); + } + + private boolean containsContainerId( + List records, long containerId) { + return records.stream().anyMatch(r -> r.getContainerId() == containerId); + } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index 8c9656b0fc47..f5f8e2947fd1 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -168,12 +168,15 @@ private UnhealthyContainers toJooqPojo(UnhealthyContainerRecordV2 rec) { } /** - * Batch delete SCM-tracked states for multiple containers. + * 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 and NEGATIVE_SIZE for all containers in the list in a - * single transaction. - * REPLICA_MISMATCH is NOT deleted as it's tracked locally by Recon. + * MIS_REPLICATED, NEGATIVE_SIZE and REPLICA_MISMATCH for all containers + * in the list in a single transaction. + * + *

    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. * * @param containerIds List of container IDs to delete states for */ @@ -192,13 +195,14 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { UnHealthyContainerStates.UNDER_REPLICATED.toString(), UnHealthyContainerStates.OVER_REPLICATED.toString(), UnHealthyContainerStates.MIS_REPLICATED.toString(), - UnHealthyContainerStates.NEGATIVE_SIZE.toString())) + UnHealthyContainerStates.NEGATIVE_SIZE.toString(), + UnHealthyContainerStates.REPLICA_MISMATCH.toString())) .execute(); - LOG.debug("Batch deleted {} SCM-tracked state records for {} containers", + LOG.debug("Batch deleted {} health state records for {} containers", deleted, containerIds.size()); } catch (Exception e) { - LOG.error("Failed to batch delete SCM states for {} containers", containerIds.size(), e); - throw new RuntimeException("Failed to batch delete SCM states", e); + LOG.error("Failed to batch delete health states for {} containers", containerIds.size(), e); + throw new RuntimeException("Failed to batch delete health states", e); } } From 65b1737d421e80bbb714e18bb514cb2b8117acd4 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sat, 28 Feb 2026 23:39:38 +0530 Subject: [PATCH 33/43] HDDS-13891. Remove formatted change from ozone-default.xml. --- hadoop-hdds/common/src/main/resources/ozone-default.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml index e9c3f2a6237a..d63dedb15416 100644 --- a/hadoop-hdds/common/src/main/resources/ozone-default.xml +++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml @@ -4505,7 +4505,6 @@ Interval in MINUTES by Recon to request SCM DB Snapshot. - ozone.om.snapshot.compaction.dag.max.time.allowed 30d From 294ed284b51cad02f3995613d81ed6658d6b89a1 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sat, 28 Feb 2026 23:51:01 +0530 Subject: [PATCH 34/43] HDDS-13891. Remove formatted changes. --- .../dist/src/main/compose/ozone/docker-config | 2 -- .../schema/ContainerSchemaDefinition.java | 25 ++++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/hadoop-ozone/dist/src/main/compose/ozone/docker-config b/hadoop-ozone/dist/src/main/compose/ozone/docker-config index 816dd83e4e9f..0631cba616d4 100644 --- a/hadoop-ozone/dist/src/main/compose/ozone/docker-config +++ b/hadoop-ozone/dist/src/main/compose/ozone/docker-config @@ -66,5 +66,3 @@ no_proxy=om,scm,s3g,recon,kdc,localhost,127.0.0.1 # Explicitly enable filesystem snapshot feature for this Docker compose cluster OZONE-SITE.XML_ozone.filesystem.snapshot.enabled=true -#OZONE-SITE.XML_ozone.recon.container.health.use.scm.report=true -OZONE-SITE.XML_ozone.recon.task.missingcontainer.interval=10s diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java index c99ac911855a..05bf4f158d8e 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java @@ -33,12 +33,11 @@ import org.slf4j.LoggerFactory; /** - * Schema definition for unhealthy containers. + * Class used to create tables that are required for tracking containers. */ @Singleton public class ContainerSchemaDefinition implements ReconSchemaDefinition { - private static final Logger LOG = - LoggerFactory.getLogger(ContainerSchemaDefinition.class); + private static final Logger LOG = LoggerFactory.getLogger(ContainerSchemaDefinition.class); public static final String UNHEALTHY_CONTAINERS_TABLE_NAME = "UNHEALTHY_CONTAINERS"; @@ -58,11 +57,14 @@ public void initializeSchema() throws SQLException { Connection conn = dataSource.getConnection(); dslContext = DSL.using(conn); if (!TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { - LOG.info("UNHEALTHY_CONTAINERS is missing, creating new one."); + LOG.info("UNHEALTHY_CONTAINERS is missing creating new one."); createUnhealthyContainersTable(); } } + /** + * Create the Missing Containers table. + */ private void createUnhealthyContainersTable() { dslContext.createTableIfNotExists(UNHEALTHY_CONTAINERS_TABLE_NAME) .column(CONTAINER_ID, SQLDataType.BIGINT.nullable(false)) @@ -79,8 +81,7 @@ private void createUnhealthyContainersTable() { .in(UnHealthyContainerStates.values()))) .execute(); dslContext.createIndex("idx_container_state") - .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME), - DSL.field(name(CONTAINER_STATE))) + .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME), DSL.field(name(CONTAINER_STATE))) .execute(); } @@ -93,17 +94,17 @@ public DataSource getDataSource() { } /** - * ENUM describing the allowed container states in the unhealthy containers - * table. + * ENUM describing the allowed container states which can be stored in the + * unhealthy containers table. */ public enum UnHealthyContainerStates { MISSING, + EMPTY_MISSING, UNDER_REPLICATED, OVER_REPLICATED, MIS_REPLICATED, - REPLICA_MISMATCH, - EMPTY_MISSING, - NEGATIVE_SIZE, - ALL_REPLICAS_BAD + ALL_REPLICAS_BAD, + NEGATIVE_SIZE, // Added new state to track containers with negative sizes + REPLICA_MISMATCH } } From 0244599b234bc3eafdae75173fac99026c363f4a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sun, 1 Mar 2026 00:08:26 +0530 Subject: [PATCH 35/43] HDDS-13891. Fixed build error. --- hadoop-ozone/integration-test-recon/pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hadoop-ozone/integration-test-recon/pom.xml b/hadoop-ozone/integration-test-recon/pom.xml index b9a75db3bd69..45523b8a7d39 100644 --- a/hadoop-ozone/integration-test-recon/pom.xml +++ b/hadoop-ozone/integration-test-recon/pom.xml @@ -102,6 +102,11 @@ hdds-interface-client test + + org.apache.ozone + hdds-interface-server + test + org.apache.ozone hdds-server-framework @@ -210,6 +215,16 @@ none + + org.apache.maven.plugins + maven-dependency-plugin + + + org.mockito:mockito-inline:jar + org.mockito:mockito-junit-jupiter:jar + + + From 23e966797a6a9aeaf4a83ed611c87d953359fd81 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Sun, 1 Mar 2026 17:19:19 +0530 Subject: [PATCH 36/43] HDDS-13891. Added performance tests and results of test in PR description. --- .../schema/ContainerSchemaDefinition.java | 20 +- .../ContainerHealthSchemaManagerV2.java | 89 ++- ...stUnhealthyContainersDerbyPerformance.java | 714 ++++++++++++++++++ 3 files changed, 800 insertions(+), 23 deletions(-) create mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestUnhealthyContainersDerbyPerformance.java diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java index 05bf4f158d8e..0b3c6c9ff233 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/apache/ozone/recon/schema/ContainerSchemaDefinition.java @@ -80,8 +80,24 @@ private void createUnhealthyContainersTable() { .check(field(name(CONTAINER_STATE)) .in(UnHealthyContainerStates.values()))) .execute(); - dslContext.createIndex("idx_container_state") - .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME), DSL.field(name(CONTAINER_STATE))) + // Composite index (container_state, container_id) serves two query patterns: + // + // 1. COUNT(*)/GROUP-BY filtered by state: + // WHERE container_state = ? + // Derby uses the index prefix (container_state) — same efficiency as the old + // single-column idx_container_state. + // + // 2. Paginated reads filtered by state + cursor: + // WHERE container_state = ? AND container_id > ? ORDER BY container_id ASC + // With the old single-column index Derby had to: + // a) Scan ALL rows for the state (e.g. 200K), then + // b) Sort them by container_id for every page call — O(n) per page. + // With this composite index Derby jumps directly to (state, minId) and reads + // the next LIMIT entries sequentially — O(1) per page, ~10–14× faster. + dslContext.createIndex("idx_state_container_id") + .on(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME), + DSL.field(name(CONTAINER_STATE)), + DSL.field(name(CONTAINER_ID))) .execute(); } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index f5f8e2947fd1..6aa5c5e6c017 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -29,6 +29,7 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; 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; @@ -52,6 +53,20 @@ public class ContainerHealthSchemaManagerV2 { LoggerFactory.getLogger(ContainerHealthSchemaManagerV2.class); private static final int BATCH_INSERT_CHUNK_SIZE = 1000; + /** + * Maximum number of container IDs to include in a single + * {@code DELETE … WHERE container_id IN (…)} statement. + * + *

    Derby's SQL compiler translates each prepared statement into a Java + * class. A large IN-predicate generates a deeply nested expression tree + * whose compiled bytecode can exceed the JVM hard limit of 65,535 bytes + * per method (ERROR XBCM4). Empirically, 5,000 IDs combined with the + * 7-state container_state IN-predicate generates ~148 KB — more than + * twice the limit. 1,000 IDs stays well under ~30 KB, providing a safe + * 2× margin.

    + */ + static final int MAX_DELETE_CHUNK_SIZE = 1_000; + private final UnhealthyContainersDao unhealthyContainersV2Dao; private final ContainerSchemaDefinition containerSchemaDefinitionV2; @@ -172,12 +187,21 @@ private UnhealthyContainers toJooqPojo(UnhealthyContainerRecordV2 rec) { * 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 in a single transaction. + * 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) { @@ -186,24 +210,36 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { } DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); - try { - int deleted = dslContext.deleteFrom(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())) - .execute(); - LOG.debug("Batch deleted {} health state records for {} containers", - deleted, containerIds.size()); - } catch (Exception e) { - LOG.error("Failed to batch delete health states for {} containers", containerIds.size(), e); - throw new RuntimeException("Failed to batch delete health states", e); + int totalDeleted = 0; + + // Chunk the container IDs so each DELETE statement stays within Derby's + // generated-bytecode limit (MAX_DELETE_CHUNK_SIZE IDs per statement). + 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); + + try { + 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; + } catch (Exception e) { + LOG.error("Failed to batch delete health states for {} containers (chunk {}-{})", + chunk.size(), from, to, e); + throw new RuntimeException("Failed to batch delete health states", e); + } } + + LOG.debug("Batch deleted {} health state records for {} containers", + totalDeleted, containerIds.size()); } /** @@ -263,10 +299,21 @@ public List getUnhealthyContainers( query.addOrderBy(orderField); query.addLimit(limit); + // Pre-buffer `limit` rows per JDBC round-trip instead of Derby's default of 1 row. + query.fetchSize(limit); + try { - return query.fetchInto(UnhealthyContainersRecord.class).stream() - .sorted(Comparator.comparingLong(UnhealthyContainersRecord::getContainerId)) - .map(record -> new UnhealthyContainerRecordV2( + 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 UnhealthyContainerRecordV2( record.getContainerId(), record.getContainerState(), record.getInStateSince(), 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..3c5d6f700018 --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestUnhealthyContainersDerbyPerformance.java @@ -0,0 +1,714 @@ +/* + * 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.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainersSummaryV2; +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

    + *
      + *
    1. Bulk INSERT throughput – 1 M records via JOOQ batchInsert in + * chunks of 1,000 inside a single Derby transaction.
    2. + *
    3. COUNT(*) by state – index-covered aggregate, one per state.
    4. + *
    5. GROUP BY summary – single pass over the idx_container_state + * index to aggregate all states.
    6. + *
    7. 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.
    8. + *
    9. Batch DELETE throughput – removes records for half the + * container IDs (100 K × 5 states = 500 K rows) via a single + * IN-clause DELETE.
    10. + *
    + * + *

    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 the 1 M-row dataset is + * inserted exactly once in {@code @BeforeAll} and shared across all + * {@code @Test} methods in the class.
    • + *
    + */ +@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 ContainerHealthSchemaManagerV2#batchDeleteSCMStatesForContainers} + * call in the delete test. + * + *

    {@code batchDeleteSCMStatesForContainers} now handles internal + * chunking at {@link ContainerHealthSchemaManagerV2#MAX_DELETE_CHUNK_SIZE} + * ({@value ContainerHealthSchemaManagerV2#MAX_DELETE_CHUNK_SIZE} 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; // matches MAX_DELETE_CHUNK_SIZE + + /** + * 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 500 K rows. */ + private static final long MAX_DELETE_SECONDS = 60; + + // ----------------------------------------------------------------------- + // Infrastructure (shared for the life of this test class) + // ----------------------------------------------------------------------- + + private ContainerHealthSchemaManagerV2 schemaManager; + private UnhealthyContainersDao dao; + private ContainerSchemaDefinition schemaDefinition; + + // ----------------------------------------------------------------------- + // One-time setup: create Derby schema + insert 1 M records + // ----------------------------------------------------------------------- + + /** + * Initialises the embedded Derby database, creates the Recon schema, and + * inserts {@value #TOTAL_RECORDS} records. This runs exactly once for the + * entire test class. + * + *

    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 setUpDatabaseAndInsertData(@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 ContainerHealthSchemaManagerV2(schemaDefinition, dao); + + // ----- Insert 1 M records in small per-transaction chunks ----- + // + // Why chunked? insertUnhealthyContainerRecords wraps its entire input in + // a single Derby transaction. Passing all 1 M records at once forces Derby + // to buffer the full WAL before committing, which exhausts its log and + // causes the call to hang. Committing every CONTAINERS_PER_TX containers + // (= 10 K rows) keeps each transaction small and lets Derby flush the log. + int txCount = (int) Math.ceil((double) CONTAINER_ID_RANGE / CONTAINERS_PER_TX); + LOG.info("Starting bulk INSERT: {} records ({} containers/tx, {} transactions)", + TOTAL_RECORDS, CONTAINERS_PER_TX, txCount); + + long now = System.currentTimeMillis(); + long insertStart = 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 insertElapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - insertStart); + double insertThroughput = (double) TOTAL_RECORDS / (insertElapsedMs / 1000.0); + LOG.info("INSERT complete: {} records in {} ms ({} rec/sec, {} tx)", + TOTAL_RECORDS, insertElapsedMs, String.format("%.0f", insertThroughput), txCount); + + assertTrue(insertElapsedMs <= TimeUnit.SECONDS.toMillis(MAX_INSERT_SECONDS), + String.format("INSERT took %d ms, exceeded %d s threshold", + insertElapsedMs, MAX_INSERT_SECONDS)); + } + + // ----------------------------------------------------------------------- + // Test 1 — Verify the inserted row count + // ----------------------------------------------------------------------- + + /** + * Verifies that all {@value #TOTAL_RECORDS} rows are present using a + * COUNT(*) over the full table. This is the baseline correctness check + * for every subsequent read test. + */ + @Test + @Order(1) + public void testTotalInsertedRecordCountIsOneMillion() { + LOG.info("--- Test 1: 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(2) + public void testCountByStatePerformanceUsesIndex() { + LOG.info("--- Test 2: 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 ContainerHealthSchemaManagerV2#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(3) + public void testGroupBySummaryQueryPerformance() { + LOG.info("--- Test 3: 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 (UnhealthyContainersSummaryV2 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(4) + public void testPaginatedReadByStatePerformance() { + UnHealthyContainerStates targetState = UnHealthyContainerStates.UNDER_REPLICATED; + LOG.info("--- Test 4: 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 (UnhealthyContainerRecordV2 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(5) + public void testFullDatasetReadThroughputAllStates() { + LOG.info("--- Test 5: 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 6 — Batch DELETE performance + // ----------------------------------------------------------------------- + + /** + * Deletes records for the first half of container IDs (1 – 100,000) across + * all five states by passing the complete 100 K ID list in one call to + * {@link ContainerHealthSchemaManagerV2#batchDeleteSCMStatesForContainers}. + * + *

    {@code batchDeleteSCMStatesForContainers} now handles internal + * chunking at {@link ContainerHealthSchemaManagerV2#MAX_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 100 statements of + * 1,000 IDs each — matching Recon's real scan-cycle pattern for large + * clusters.

    + * + *

    Expected outcome: 100 K × 5 states = 500 K rows deleted, 500 K remain.

    + * + *

    Note: this test modifies the shared dataset, so it runs after + * all read-only tests.

    + */ + @Test + @Order(6) + public void testBatchDeletePerformanceHalfTheContainers() { + int deleteCount = CONTAINER_ID_RANGE / 2; // 100 000 container IDs + int expectedDeleted = deleteCount * STATE_COUNT; // 500 000 rows + int expectedRemaining = TOTAL_RECORDS - expectedDeleted; + int internalChunks = (int) Math.ceil( + (double) deleteCount / ContainerHealthSchemaManagerV2.MAX_DELETE_CHUNK_SIZE); + + LOG.info("--- Test 6: Batch DELETE — {} IDs × {} states = {} rows " + + "({} internal SQL statements of {} IDs) ---", + deleteCount, STATE_COUNT, expectedDeleted, + internalChunks, ContainerHealthSchemaManagerV2.MAX_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 7 — Re-read counts after partial delete + // ----------------------------------------------------------------------- + + /** + * After the deletion in Test 6, verifies that each state has exactly + * {@code CONTAINER_ID_RANGE / 2} records (100 K), confirming that the + * index-covered COUNT query remains accurate after a large delete. + */ + @Test + @Order(7) + public void testCountByStateAfterPartialDelete() { + int expectedPerState = CONTAINER_ID_RANGE / 2; + LOG.info("--- Test 7: COUNT by state after 50%% 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 partial 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 UnhealthyContainerRecordV2( + containerId, + state.toString(), + timestamp, + expectedReplicas, + actualReplicas, + expectedReplicas - actualReplicas, + reason)); + } + } + return records; + } +} From 4882fe5b9e7ceacecc48c3e5fdfab812ed60550e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Wed, 4 Mar 2026 23:16:54 +0530 Subject: [PATCH 37/43] HDDS-13891. Fixed review comments. --- .../ozone/recon/api/ContainerEndpoint.java | 10 +- .../api/types/UnhealthyContainerMetadata.java | 22 +-- .../recon/fsck/ReconReplicationManager.java | 177 ++++++++++++------ .../ContainerHealthSchemaManagerV2.java | 69 +++++++ .../recon/api/TestContainerEndpoint.java | 4 +- .../persistence/AbstractReconSqlDBTest.java | 4 +- 6 files changed, 208 insertions(+), 78 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 499381da1ffc..6bcbac78a74b 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -83,6 +83,7 @@ import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager; import org.apache.hadoop.ozone.util.SeekableIterator; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; +import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -461,17 +462,16 @@ private UnhealthyContainerMetadata toUnhealthyMetadata( List datanodes = containerManager.getLatestContainerHistory(containerID, containerInfo.getReplicationConfig().getRequiredNodes()); - return new UnhealthyContainerMetadata( + UnhealthyContainers unhealthyContainers = new UnhealthyContainers( record.getContainerId(), record.getContainerState(), record.getInStateSince(), record.getExpectedReplicaCount(), record.getActualReplicaCount(), record.getReplicaDelta(), - record.getReason(), - datanodes, - pipelineID, - keyCount); + record.getReason()); + return new UnhealthyContainerMetadata(unhealthyContainers, datanodes, + pipelineID, keyCount); } catch (IOException ioEx) { throw new UncheckedIOException(ioEx); } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java index 7418863fab10..bc6bb57b4a69 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/UnhealthyContainerMetadata.java @@ -23,6 +23,7 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; +import org.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; /** * Metadata object that represents an unhealthy Container. @@ -60,18 +61,15 @@ public class UnhealthyContainerMetadata { @XmlElement(name = "replicas") private List replicas; - @SuppressWarnings("checkstyle:ParameterNumber") - public UnhealthyContainerMetadata(long containerID, String containerState, - long unhealthySince, long expectedReplicaCount, long actualReplicaCount, - long replicaDeltaCount, String reason, List replicas, - UUID pipelineID, long keyCount) { - this.containerID = containerID; - this.containerState = containerState; - this.unhealthySince = unhealthySince; - this.actualReplicaCount = actualReplicaCount; - this.expectedReplicaCount = expectedReplicaCount; - this.replicaDeltaCount = replicaDeltaCount; - this.reason = reason; + public UnhealthyContainerMetadata(UnhealthyContainers rec, + List replicas, UUID pipelineID, long keyCount) { + this.containerID = rec.getContainerId(); + this.containerState = rec.getContainerState(); + this.unhealthySince = rec.getInStateSince(); + this.actualReplicaCount = rec.getActualReplicaCount(); + this.expectedReplicaCount = rec.getExpectedReplicaCount(); + this.replicaDeltaCount = rec.getReplicaDelta(); + this.reason = rec.getReason(); this.replicas = replicas; this.pipelineID = pipelineID; this.keys = keyCount; diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 9ba5b425e117..f62716e8f595 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.hadoop.hdds.conf.ConfigurationSource; @@ -39,6 +40,7 @@ import org.apache.hadoop.hdds.scm.node.NodeManager; import org.apache.hadoop.hdds.server.events.EventPublisher; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.ContainerStateKey; import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; import org.apache.hadoop.util.Time; import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; @@ -75,6 +77,7 @@ public class ReconReplicationManager extends ReplicationManager { private static final Logger LOG = LoggerFactory.getLogger(ReconReplicationManager.class); + private static final int PERSIST_CHUNK_SIZE = 50_000; private final ContainerHealthSchemaManagerV2 healthSchemaManager; private final ContainerManager containerManager; @@ -342,64 +345,79 @@ public synchronized void processAll() { private void storeHealthStatesToDatabase( ReconReplicationManagerReport report, List allContainers) { - long currentTime = System.currentTimeMillis(); - List recordsToInsert = new ArrayList<>(); - List containerIdsToDelete = collectContainerIds(allContainers); - ProcessingStats stats = new ProcessingStats(); - Set negativeSizeRecorded = new HashSet<>(); - - report.forEachContainerByState((state, cid) -> { - try { - handleScmStateContainer(state, cid, currentTime, recordsToInsert, - negativeSizeRecorded, stats); - } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing {} state", cid, state, e); - } - }); - - logProcessingStats(stats, report.getReplicaMismatchCount()); - - int replicaMismatchCount = processReplicaMismatchContainers( - report, currentTime, recordsToInsert); - persistUnhealthyRecords(containerIdsToDelete, recordsToInsert); + ProcessingStats totalStats = new ProcessingStats(); + int totalReplicaMismatchCount = 0; + + for (int from = 0; from < allContainers.size(); from += PERSIST_CHUNK_SIZE) { + int to = Math.min(from + PERSIST_CHUNK_SIZE, allContainers.size()); + List chunkContainerIds = collectContainerIds(allContainers, from, to); + Set chunkContainerIdSet = new HashSet<>(chunkContainerIds); + Map existingInStateSinceByContainerAndState = + healthSchemaManager.getExistingInStateSinceByContainerIds(chunkContainerIds); + List recordsToInsert = new ArrayList<>(); + ProcessingStats chunkStats = new ProcessingStats(); + Set negativeSizeRecorded = new HashSet<>(); + + report.forEachContainerByState((state, cid) -> { + if (!chunkContainerIdSet.contains(cid.getId())) { + return; + } + try { + handleScmStateContainer(state, cid, currentTime, + existingInStateSinceByContainerAndState, recordsToInsert, + negativeSizeRecorded, chunkStats); + } catch (ContainerNotFoundException e) { + LOG.warn("Container {} not found when processing {} state", cid, state, e); + } + }); + + int chunkReplicaMismatchCount = processReplicaMismatchContainersForChunk( + report, currentTime, existingInStateSinceByContainerAndState, + recordsToInsert, chunkContainerIdSet); + totalReplicaMismatchCount += chunkReplicaMismatchCount; + totalStats.add(chunkStats); + persistUnhealthyRecords(chunkContainerIds, recordsToInsert); + } LOG.info("Stored {} MISSING, {} EMPTY_MISSING, {} UNDER_REPLICATED, " + "{} OVER_REPLICATED, {} MIS_REPLICATED, {} NEGATIVE_SIZE, " + "{} REPLICA_MISMATCH", - stats.missingCount, stats.emptyMissingCount, stats.underRepCount, - stats.overRepCount, stats.misRepCount, stats.negativeSizeCount, - replicaMismatchCount); + totalStats.missingCount, totalStats.emptyMissingCount, totalStats.underRepCount, + totalStats.overRepCount, totalStats.misRepCount, totalStats.negativeSizeCount, + totalReplicaMismatchCount); } private void handleScmStateContainer( ContainerHealthState state, ContainerID containerId, long currentTime, + Map existingInStateSinceByContainerAndState, List recordsToInsert, Set negativeSizeRecorded, ProcessingStats stats) throws ContainerNotFoundException { switch (state) { case MISSING: - handleMissingContainer(containerId, currentTime, recordsToInsert, stats); + handleMissingContainer(containerId, currentTime, + existingInStateSinceByContainerAndState, recordsToInsert, stats); break; case UNDER_REPLICATED: stats.incrementUnderRepCount(); handleReplicaStateContainer(containerId, currentTime, - UnHealthyContainerStates.UNDER_REPLICATED, "Insufficient replicas", - recordsToInsert, negativeSizeRecorded, stats); + UnHealthyContainerStates.UNDER_REPLICATED, + existingInStateSinceByContainerAndState, recordsToInsert, negativeSizeRecorded, stats); break; case OVER_REPLICATED: stats.incrementOverRepCount(); handleReplicaStateContainer(containerId, currentTime, - UnHealthyContainerStates.OVER_REPLICATED, "Excess replicas", - recordsToInsert, negativeSizeRecorded, stats); + UnHealthyContainerStates.OVER_REPLICATED, + existingInStateSinceByContainerAndState, recordsToInsert, negativeSizeRecorded, stats); break; case MIS_REPLICATED: stats.incrementMisRepCount(); handleReplicaStateContainer(containerId, currentTime, - UnHealthyContainerStates.MIS_REPLICATED, "Placement policy violated", - recordsToInsert, negativeSizeRecorded, stats); + UnHealthyContainerStates.MIS_REPLICATED, + existingInStateSinceByContainerAndState, recordsToInsert, negativeSizeRecorded, stats); break; default: break; @@ -409,6 +427,7 @@ private void handleScmStateContainer( private void handleMissingContainer( ContainerID containerId, long currentTime, + Map existingInStateSinceByContainerAndState, List recordsToInsert, ProcessingStats stats) throws ContainerNotFoundException { ContainerInfo container = containerManager.getContainer(containerId); @@ -416,14 +435,22 @@ private void handleMissingContainer( if (isEmptyMissing(container)) { stats.incrementEmptyMissingCount(); recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.EMPTY_MISSING, currentTime, expected, 0, + UnHealthyContainerStates.EMPTY_MISSING, + resolveInStateSince(container.getContainerID(), + UnHealthyContainerStates.EMPTY_MISSING, currentTime, + existingInStateSinceByContainerAndState), + expected, 0, "Container has no replicas and no keys")); return; } stats.incrementMissingCount(); recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.MISSING, currentTime, expected, 0, + UnHealthyContainerStates.MISSING, + resolveInStateSince(container.getContainerID(), + UnHealthyContainerStates.MISSING, currentTime, + existingInStateSinceByContainerAndState), + expected, 0, "No replicas available")); } @@ -431,7 +458,7 @@ private void handleReplicaStateContainer( ContainerID containerId, long currentTime, UnHealthyContainerStates targetState, - String reason, + Map existingInStateSinceByContainerAndState, List recordsToInsert, Set negativeSizeRecorded, ProcessingStats stats) throws ContainerNotFoundException { @@ -439,36 +466,51 @@ private void handleReplicaStateContainer( Set replicas = containerManager.getContainerReplicas(containerId); int expected = container.getReplicationConfig().getRequiredNodes(); int actual = replicas.size(); - recordsToInsert.add(createRecord(container, targetState, currentTime, expected, actual, reason)); + recordsToInsert.add(createRecord(container, targetState, + resolveInStateSince(container.getContainerID(), targetState, + currentTime, existingInStateSinceByContainerAndState), + expected, actual, reasonForState(targetState))); addNegativeSizeRecordIfNeeded(container, currentTime, actual, recordsToInsert, - negativeSizeRecorded, stats); + existingInStateSinceByContainerAndState, negativeSizeRecorded, stats); } - private int processReplicaMismatchContainers( + private int processReplicaMismatchContainersForChunk( ReconReplicationManagerReport report, long currentTime, - List recordsToInsert) { + Map existingInStateSinceByContainerAndState, + List recordsToInsert, + Set chunkContainerIdSet) { List replicaMismatchContainers = report.getReplicaMismatchContainers(); + int chunkReplicaMismatchCount = 0; for (ContainerID cid : replicaMismatchContainers) { + if (!chunkContainerIdSet.contains(cid.getId())) { + continue; + } try { ContainerInfo container = containerManager.getContainer(cid); Set replicas = containerManager.getContainerReplicas(cid); int expected = container.getReplicationConfig().getRequiredNodes(); int actual = replicas.size(); recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.REPLICA_MISMATCH, currentTime, expected, actual, + UnHealthyContainerStates.REPLICA_MISMATCH, + resolveInStateSince(container.getContainerID(), + UnHealthyContainerStates.REPLICA_MISMATCH, currentTime, + existingInStateSinceByContainerAndState), + expected, actual, "Data checksum mismatch across replicas")); + chunkReplicaMismatchCount++; } catch (ContainerNotFoundException e) { LOG.warn("Container {} not found when processing REPLICA_MISMATCH state", cid, e); } } - return replicaMismatchContainers.size(); + return chunkReplicaMismatchCount; } - private List collectContainerIds(List allContainers) { - List containerIds = new ArrayList<>(allContainers.size()); - for (ContainerInfo container : allContainers) { - containerIds.add(container.getContainerID()); + private List collectContainerIds(List allContainers, + int fromInclusive, int toExclusive) { + List containerIds = new ArrayList<>(toExclusive - fromInclusive); + for (int i = fromInclusive; i < toExclusive; i++) { + containerIds.add(allContainers.get(i).getContainerID()); } return containerIds; } @@ -483,19 +525,6 @@ private void persistUnhealthyRecords( healthSchemaManager.insertUnhealthyContainerRecords(recordsToInsert); } - private void logProcessingStats(ProcessingStats stats, int replicaMismatchCount) { - LOG.info("Processing health states: MISSING={}, EMPTY_MISSING={}, " + - "UNDER_REPLICATED={}, OVER_REPLICATED={}, MIS_REPLICATED={}, " + - "NEGATIVE_SIZE={}, REPLICA_MISMATCH={}", - stats.missingCount, - stats.emptyMissingCount, - stats.underRepCount, - stats.overRepCount, - stats.misRepCount, - stats.negativeSizeCount, - replicaMismatchCount); - } - private boolean isEmptyMissing(ContainerInfo container) { return container.getNumberOfKeys() == 0; } @@ -509,18 +538,43 @@ private void addNegativeSizeRecordIfNeeded( long currentTime, int actualReplicaCount, List recordsToInsert, + Map existingInStateSinceByContainerAndState, Set negativeSizeRecorded, ProcessingStats stats) { if (isNegativeSize(container) && negativeSizeRecorded.add(container.getContainerID())) { int expected = container.getReplicationConfig().getRequiredNodes(); recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.NEGATIVE_SIZE, currentTime, expected, actualReplicaCount, + UnHealthyContainerStates.NEGATIVE_SIZE, + resolveInStateSince(container.getContainerID(), + UnHealthyContainerStates.NEGATIVE_SIZE, currentTime, + existingInStateSinceByContainerAndState), + expected, actualReplicaCount, "Container reports negative usedBytes")); stats.incrementNegativeSizeCount(); } } + private long resolveInStateSince(long containerId, UnHealthyContainerStates state, + long currentTime, Map existingInStateSinceByContainerAndState) { + Long inStateSince = existingInStateSinceByContainerAndState.get( + new ContainerStateKey(containerId, state.toString())); + return inStateSince == null ? currentTime : inStateSince; + } + + private String reasonForState(UnHealthyContainerStates state) { + switch (state) { + case UNDER_REPLICATED: + return "Insufficient replicas"; + case OVER_REPLICATED: + return "Excess replicas"; + case MIS_REPLICATED: + return "Placement policy violated"; + default: + return null; + } + } + private static final class ProcessingStats { private int missingCount; private int underRepCount; @@ -552,6 +606,15 @@ void incrementEmptyMissingCount() { void incrementNegativeSizeCount() { negativeSizeCount++; } + + void add(ProcessingStats other) { + missingCount += other.missingCount; + underRepCount += other.underRepCount; + overRepCount += other.overRepCount; + misRepCount += other.misRepCount; + emptyMissingCount += other.emptyMissingCount; + negativeSizeCount += other.negativeSizeCount; + } } /** diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java index 6aa5c5e6c017..1434c5da0a29 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java @@ -27,7 +27,9 @@ import java.sql.Connection; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; @@ -242,6 +244,43 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { totalDeleted, containerIds.size()); } + /** + * 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 = containerSchemaDefinitionV2.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; + } + /** * Get summary of unhealthy containers grouped by state from V2 table. */ @@ -395,6 +434,36 @@ public String getReason() { } } + /** + * 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; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ContainerStateKey)) { + return false; + } + ContainerStateKey that = (ContainerStateKey) other; + return containerId == that.containerId && state.equals(that.state); + } + + @Override + public int hashCode() { + return Long.hashCode(containerId) * 31 + state.hashCode(); + } + } + /** * POJO representing a summary record for unhealthy containers. */ 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 c5ad18ac3071..3cdd4e1f5e1c 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 @@ -949,8 +949,8 @@ public void testUnhealthyContainers() throws IOException, TimeoutException { public void testUnhealthyContainersFilteredResponse() throws IOException, TimeoutException { String missing = UnHealthyContainerStates.MISSING.toString(); - String emptyMissing = "EMPTY_MISSING"; - String negativeSize = "NEGATIVE_SIZE"; + String emptyMissing = UnHealthyContainerStates.EMPTY_MISSING.toString(); + String negativeSize = UnHealthyContainerStates.NEGATIVE_SIZE.toString(); // Initial empty response verification Response response = containerEndpoint diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java index 78d9ebba4e03..5ac3b32169c8 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java @@ -179,12 +179,12 @@ public String getDriverClass() { @Override public String getJdbcUrl() { return "jdbc:derby:" + tempDir.getAbsolutePath() + - File.separator + "derby_recon.db;create=true"; + File.separator + "derby_recon.db"; } @Override public String getUserName() { - return "RECON"; + return null; } @Override From 2a13be0c5c2e607fd870a420905af0832825b7d7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 9 Mar 2026 21:38:33 +0530 Subject: [PATCH 38/43] HDDS-13891. Fixed review comments. --- .../replication/ReplicationManager.java | 20 +- .../recon/TestReconContainerEndpoint.java | 2 +- .../hadoop/ozone/recon/TestReconTasks.java | 59 ++-- .../recon/TestReconTasksV2MultiNode.java | 16 +- .../ozone/recon/ReconControllerModule.java | 4 +- .../ozone/recon/api/ClusterStateEndpoint.java | 14 +- .../ozone/recon/api/ContainerEndpoint.java | 22 +- ...thTaskV2.java => ContainerHealthTask.java} | 33 +-- .../recon/fsck/ReconReplicationManager.java | 267 +++++++++++------- ...s.java => ContainerHealthTaskMetrics.java} | 22 +- ...java => ContainerHealthSchemaManager.java} | 172 ++++++----- .../recon/scm/ReconContainerManager.java | 12 +- .../ReconStorageContainerManagerFacade.java | 20 +- .../recon/upgrade/ReconLayoutFeature.java | 4 +- ...ersStateContainerIdIndexUpgradeAction.java | 86 ++++++ .../recon/api/TestClusterStateEndpoint.java | 18 +- .../recon/api/TestContainerEndpoint.java | 12 +- .../hadoop/ozone/recon/api/TestEndpoints.java | 10 +- .../recon/fsck/TestContainerHealthTask.java | 12 +- .../fsck/TestReconReplicationManager.java | 142 +++++++++- ...stUnhealthyContainersDerbyPerformance.java | 42 +-- .../AbstractReconContainerManagerTest.java | 4 +- ...ersStateContainerIdIndexUpgradeAction.java | 117 ++++++++ 23 files changed, 772 insertions(+), 338 deletions(-) rename hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/{ContainerHealthTaskV2.java => ContainerHealthTask.java} (83%) rename hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/{ContainerHealthTaskV2Metrics.java => ContainerHealthTaskMetrics.java} (76%) rename hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/{ContainerHealthSchemaManagerV2.java => ContainerHealthSchemaManager.java} (77%) create mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UnhealthyContainersStateContainerIdIndexUpgradeAction.java create mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java index fa01dd95f278..32808fc85a5e 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java @@ -853,18 +853,24 @@ protected void processContainer(ContainerInfo containerInfo, protected boolean processContainer(ContainerInfo containerInfo, ReplicationQueue repQueue, ReplicationManagerReport report, boolean readOnly) throws ContainerNotFoundException { + ContainerID containerID = containerInfo.containerID(); + Set replicas = containerManager.getContainerReplicas( + containerID); + List pendingOps = + containerReplicaPendingOps.getPendingOps(containerID); + return processContainer(containerInfo, replicas, pendingOps, repQueue, report, + readOnly); + } + + protected boolean processContainer(ContainerInfo containerInfo, + Set replicas, List pendingOps, + ReplicationQueue repQueue, ReplicationManagerReport report, + boolean readOnly) throws ContainerNotFoundException { synchronized (containerInfo) { // Reset health state to HEALTHY before processing this container report.resetContainerHealthState(); - - ContainerID containerID = containerInfo.containerID(); final boolean isEC = isEC(containerInfo.getReplicationConfig()); - Set replicas = containerManager.getContainerReplicas( - containerID); - List pendingOps = - containerReplicaPendingOps.getPendingOps(containerID); - ContainerCheckRequest checkRequest = new ContainerCheckRequest.Builder() .setContainerInfo(containerInfo) .setContainerReplicas(replicas) diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java index 8b4703ec1f1c..a8863046f6ee 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconContainerEndpoint.java @@ -219,7 +219,7 @@ private Response getContainerEndpointResponse(long containerId) { .getOMMetadataManagerInstance(); ContainerEndpoint containerEndpoint = new ContainerEndpoint(reconSCM, - null, // ContainerHealthSchemaManagerV2 - not needed for this test + null, // ContainerHealthSchemaManager - not needed for this test recon.getReconServer().getReconNamespaceSummaryManager(), recon.getReconServer().getReconContainerMetadataManager(), omMetadataManagerInstance); diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index b0b4c52394b8..21f25edd4486 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -50,7 +50,8 @@ import org.apache.hadoop.ozone.HddsDatanodeService; import org.apache.hadoop.ozone.MiniOzoneCluster; import org.apache.hadoop.ozone.UniformDatanodesFactory; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager.UnhealthyContainerRecord; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; @@ -63,7 +64,7 @@ import org.slf4j.event.Level; /** - * Integration tests for Recon's ContainerHealthTaskV2. + * Integration tests for Recon's ContainerHealthTask. * *

    Covered unhealthy states (all states tracked in the UNHEALTHY_CONTAINERS * table except the dead {@code ALL_REPLICAS_BAD} state):

    @@ -190,7 +191,7 @@ public void testSyncSCMContainerInfo() throws Exception { } /** - * Verifies that ContainerHealthTaskV2 correctly detects {@code UNDER_REPLICATED} + * Verifies that ContainerHealthTask correctly detects {@code UNDER_REPLICATED} * when a CLOSED RF3 container loses one replica due to a node failure, and that the * state clears after the node recovers. * @@ -283,15 +284,15 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() // and is NOT classified as MISSING or EMPTY_MISSING. LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); - List underReplicated = + List underReplicated = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); - List missing = + List missing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); - List emptyMissing = + List emptyMissing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); @@ -305,7 +306,7 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() forceContainerHealthScan(reconScm); LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); - List underReplicated = + List underReplicated = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); @@ -313,14 +314,14 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() }); // After recovery: our container must not appear in any unhealthy state. - List missingAfterRecovery = + List missingAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); assertFalse(containsContainerId(missingAfterRecovery, containerID), "Container should not be MISSING after node recovery"); - List emptyMissingAfterRecovery = + List emptyMissingAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); @@ -331,7 +332,7 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() } /** - * Verifies that ContainerHealthTaskV2 correctly detects {@code EMPTY_MISSING} + * Verifies that ContainerHealthTask correctly detects {@code EMPTY_MISSING} * (not {@code MISSING} or {@code UNDER_REPLICATED}) when a CLOSING RF1 container * loses its only replica due to a node failure, and the container has no * OM-tracked keys (i.e., {@link ContainerInfo#getNumberOfKeys()} == 0). @@ -385,15 +386,15 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() // EMPTY_MISSING means: 0 replicas AND 0 OM-tracked keys (no data loss risk). LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); - List emptyMissing = + List emptyMissing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); - List missing = + List missing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); - List underReplicated = + List underReplicated = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); @@ -410,7 +411,7 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() forceContainerHealthScan(reconScm); LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); - List emptyMissing = + List emptyMissing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); @@ -418,14 +419,14 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() }); // After recovery: our container must not appear in any unhealthy state. - List missingAfterRecovery = + List missingAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); assertFalse(containsContainerId(missingAfterRecovery, containerID), "Container should not be MISSING after node recovery"); - List underReplicatedAfterRecovery = + List underReplicatedAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); @@ -436,7 +437,7 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() } /** - * Verifies that ContainerHealthTaskV2 correctly detects {@code MISSING} + * Verifies that ContainerHealthTask correctly detects {@code MISSING} * (distinct from {@code EMPTY_MISSING}) when a CLOSED RF1 container that has * OM-tracked keys loses its only replica. * @@ -511,11 +512,11 @@ public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() // (not EMPTY_MISSING because numberOfKeys > 0). forceContainerHealthScan(reconScm); - List missing = + List missing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); - List emptyMissing = + List emptyMissing = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); @@ -529,7 +530,7 @@ public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() reconCm.updateContainerReplica(cid, theReplica); forceContainerHealthScan(reconScm); - List missingAfterRecovery = + List missingAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); @@ -540,7 +541,7 @@ public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() } /** - * Verifies that ContainerHealthTaskV2 correctly detects {@code OVER_REPLICATED} + * Verifies that ContainerHealthTask correctly detects {@code OVER_REPLICATED} * when a CLOSED RF1 container has more replicas in Recon than its replication * factor, and simultaneously detects {@code NEGATIVE_SIZE} when the same * container has a negative {@code usedBytes} value. @@ -626,11 +627,11 @@ public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() forceContainerHealthScan(reconScm); - List overReplicated = + List overReplicated = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, 0L, 0L, 1000); - List negativeSize = + List negativeSize = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.NEGATIVE_SIZE, 0L, 0L, 1000); @@ -645,11 +646,11 @@ public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() reconCm.getContainer(cid).setUsedBytes(0L); forceContainerHealthScan(reconScm); - List overReplicatedAfterRecovery = + List overReplicatedAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, 0L, 0L, 1000); - List negativeSizeAfterRecovery = + List negativeSizeAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.NEGATIVE_SIZE, 0L, 0L, 1000); @@ -663,7 +664,7 @@ public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() } /** - * Verifies that ContainerHealthTaskV2 correctly detects {@code REPLICA_MISMATCH} + * Verifies that ContainerHealthTask correctly detects {@code REPLICA_MISMATCH} * when replicas of a CLOSED RF3 container report different data checksums, and * that the state clears once the checksums are made uniform again. * @@ -739,7 +740,7 @@ public void testContainerHealthTaskV2DetectsReplicaMismatch() throws Exception { forceContainerHealthScan(reconScm); - List replicaMismatch = + List replicaMismatch = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.REPLICA_MISMATCH, 0L, 0L, 1000); @@ -750,7 +751,7 @@ public void testContainerHealthTaskV2DetectsReplicaMismatch() throws Exception { reconCm.updateContainerReplica(cid, originalReplica); forceContainerHealthScan(reconScm); - List replicaMismatchAfterRecovery = + List replicaMismatchAfterRecovery = reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.REPLICA_MISMATCH, 0L, 0L, 1000); @@ -766,7 +767,7 @@ private void forceContainerHealthScan( } private boolean containsContainerId( - List records, long containerId) { + List records, long containerId) { return records.stream().anyMatch(r -> r.getContainerId() == containerId); } } diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java index 1344aedf53c6..44540a9b1761 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java @@ -27,7 +27,7 @@ import org.apache.hadoop.hdds.protocol.proto.HddsProtos; import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; import org.apache.hadoop.ozone.MiniOzoneCluster; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager.UnhealthyContainerRecord; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; @@ -39,7 +39,7 @@ import org.junit.jupiter.api.Test; /** - * Integration tests for ContainerHealthTaskV2 with multi-node clusters. + * Integration tests for ContainerHealthTask with multi-node clusters. * * These tests are separate from TestReconTasks because they require * different cluster configurations (3 datanodes) and would conflict @@ -95,7 +95,7 @@ public static void shutdownCluster() { } /** - * Test that ContainerHealthTaskV2 can query UNDER_REPLICATED containers. + * Test that ContainerHealthTask can query UNDER_REPLICATED containers. * Steps: * 1. Create a cluster with 3 datanodes * 2. Verify the query mechanism for UNDER_REPLICATED state works @@ -119,7 +119,7 @@ public static void shutdownCluster() { * 2. Write actual data to container (creates physical replicas) * 3. Shut down 1 datanode * 4. Wait for SCM to mark datanode as dead (stale/dead intervals) - * 5. Wait for ContainerHealthTaskV2 to run (task interval) + * 5. Wait for ContainerHealthTask to run (task interval) * 6. Verify UNDER_REPLICATED state in V2 table with correct replica counts * 7. Restart datanode and verify container becomes healthy */ @@ -128,7 +128,7 @@ public void testContainerHealthTaskV2UnderReplicated() throws Exception { cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.THREE, 60000); // Verify the query mechanism for UNDER_REPLICATED state works - List underReplicatedContainers = + List underReplicatedContainers = reconContainerManager.getContainerSchemaManagerV2() .getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, @@ -139,13 +139,13 @@ public void testContainerHealthTaskV2UnderReplicated() throws Exception { } /** - * Test that ContainerHealthTaskV2 detects OVER_REPLICATED containers. + * Test that ContainerHealthTask detects OVER_REPLICATED containers. * Steps: * 1. Create a cluster with 3 datanodes * 2. Allocate a container with replication factor 1 * 3. Write data to the container * 4. Manually add the container to additional datanodes to create over-replication - * 5. Verify ContainerHealthTaskV2 detects OVER_REPLICATED state in V2 table + * 5. Verify ContainerHealthTask detects OVER_REPLICATED state in V2 table * * Note: Creating over-replication scenarios is complex in integration tests * as it requires manipulating the container replica state artificially. @@ -167,7 +167,7 @@ public void testContainerHealthTaskV2OverReplicated() throws Exception { // should contain the record with proper replica counts. // For now, just verify that the query mechanism works - List overReplicatedContainers = + List overReplicatedContainers = reconContainerManager.getContainerSchemaManagerV2() .getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java index 99abbca14267..3f7e99056e44 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java @@ -42,7 +42,7 @@ import org.apache.hadoop.ozone.om.protocolPB.OmTransportFactory; import org.apache.hadoop.ozone.om.protocolPB.OzoneManagerProtocolClientSideTranslatorPB; import org.apache.hadoop.ozone.recon.heatmap.HeatMapServiceImpl; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.DataSourceConfiguration; import org.apache.hadoop.ozone.recon.persistence.JooqPersistenceModule; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; @@ -102,7 +102,7 @@ protected void configure() { .to(ReconOmMetadataManagerImpl.class); bind(OMMetadataManager.class).to(ReconOmMetadataManagerImpl.class); - bind(ContainerHealthSchemaManagerV2.class).in(Singleton.class); + bind(ContainerHealthSchemaManager.class).in(Singleton.class); bind(ReconContainerMetadataManager.class) .to(ReconContainerMetadataManagerImpl.class).in(Singleton.class); bind(ReconFileMetadataManager.class) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java index 970db3f10b1c..608c4846d383 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ClusterStateEndpoint.java @@ -44,7 +44,7 @@ import org.apache.hadoop.ozone.recon.api.types.ClusterStateResponse; import org.apache.hadoop.ozone.recon.api.types.ClusterStorageReport; import org.apache.hadoop.ozone.recon.api.types.ContainerStateCounts; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconNodeManager; import org.apache.hadoop.ozone.recon.scm.ReconPipelineManager; @@ -71,13 +71,13 @@ public class ClusterStateEndpoint { private final ReconContainerManager containerManager; private final ReconGlobalStatsManager reconGlobalStatsManager; private final OzoneConfiguration ozoneConfiguration; - private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; + private final ContainerHealthSchemaManager containerHealthSchemaManager; @Inject ClusterStateEndpoint(OzoneStorageContainerManager reconSCM, ReconGlobalStatsManager reconGlobalStatsManager, - ContainerHealthSchemaManagerV2 - containerHealthSchemaManagerV2, + ContainerHealthSchemaManager + containerHealthSchemaManager, OzoneConfiguration ozoneConfiguration) { this.nodeManager = (ReconNodeManager) reconSCM.getScmNodeManager(); @@ -85,7 +85,7 @@ public class ClusterStateEndpoint { this.containerManager = (ReconContainerManager) reconSCM.getContainerManager(); this.reconGlobalStatsManager = reconGlobalStatsManager; - this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; + this.containerHealthSchemaManager = containerHealthSchemaManager; this.ozoneConfiguration = ozoneConfiguration; } @@ -98,8 +98,8 @@ public Response getClusterState() { ContainerStateCounts containerStateCounts = new ContainerStateCounts(); int pipelines = this.pipelineManager.getPipelines().size(); - List missingContainers = - containerHealthSchemaManagerV2 + List missingContainers = + containerHealthSchemaManager .getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, MISSING_CONTAINER_COUNT_LIMIT); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 6bcbac78a74b..1a20e02f7efc 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -75,7 +75,7 @@ import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersSummary; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; @@ -101,7 +101,7 @@ public class ContainerEndpoint { private final ReconContainerManager containerManager; private final PipelineManager pipelineManager; - private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; + private final ContainerHealthSchemaManager containerHealthSchemaManager; private final ReconNamespaceSummaryManager reconNamespaceSummaryManager; private final OzoneStorageContainerManager reconSCM; private static final Logger LOG = @@ -142,14 +142,14 @@ public static DataFilter fromValue(String value) { @Inject public ContainerEndpoint(OzoneStorageContainerManager reconSCM, - ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2, + ContainerHealthSchemaManager containerHealthSchemaManager, ReconNamespaceSummaryManager reconNamespaceSummaryManager, ReconContainerMetadataManager reconContainerMetadataManager, ReconOMMetadataManager omMetadataManager) { this.containerManager = (ReconContainerManager) reconSCM.getContainerManager(); this.pipelineManager = reconSCM.getPipelineManager(); - this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; + this.containerHealthSchemaManager = containerHealthSchemaManager; this.reconNamespaceSummaryManager = reconNamespaceSummaryManager; this.reconSCM = reconSCM; this.reconContainerMetadataManager = reconContainerMetadataManager; @@ -339,7 +339,7 @@ public Response getMissingContainers( int limit ) { List missingContainers = new ArrayList<>(); - containerHealthSchemaManagerV2.getUnhealthyContainers( + containerHealthSchemaManager.getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, limit) .forEach(container -> { @@ -417,15 +417,15 @@ private Response getUnhealthyContainersFromSchema( } // Get summary from V2 table and convert to V1 format - List v2Summary = - containerHealthSchemaManagerV2.getUnhealthyContainersSummary(); - for (ContainerHealthSchemaManagerV2.UnhealthyContainersSummaryV2 s : v2Summary) { + List v2Summary = + containerHealthSchemaManager.getUnhealthyContainersSummary(); + for (ContainerHealthSchemaManager.UnhealthyContainersSummary s : v2Summary) { summary.add(new UnhealthyContainersSummary(s.getContainerState(), s.getCount())); } // Get containers from V2 table - List v2Containers = - containerHealthSchemaManagerV2.getUnhealthyContainers(v2State, minContainerId, maxContainerId, limit); + List v2Containers = + containerHealthSchemaManager.getUnhealthyContainers(v2State, minContainerId, maxContainerId, limit); unhealthyMeta = v2Containers.stream() .map(this::toUnhealthyMetadata) @@ -452,7 +452,7 @@ private Response getUnhealthyContainersFromSchema( } private UnhealthyContainerMetadata toUnhealthyMetadata( - ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2 record) { + ContainerHealthSchemaManager.UnhealthyContainerRecord record) { try { long containerID = record.getContainerId(); ContainerInfo containerInfo = diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java similarity index 83% rename from hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java rename to hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java index 5ccb3528f73c..7b761068d748 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTaskV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java @@ -18,7 +18,7 @@ package org.apache.hadoop.ozone.recon.fsck; import javax.inject.Inject; -import org.apache.hadoop.ozone.recon.metrics.ContainerHealthTaskV2Metrics; +import org.apache.hadoop.ozone.recon.metrics.ContainerHealthTaskMetrics; import org.apache.hadoop.ozone.recon.scm.ReconScmTask; import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; import org.apache.hadoop.ozone.recon.tasks.ReconTaskConfig; @@ -51,25 +51,26 @@ * @see ReconReplicationManager * @see NoOpsContainerReplicaPendingOps */ -public class ContainerHealthTaskV2 extends ReconScmTask { +public class ContainerHealthTask extends ReconScmTask { private static final Logger LOG = - LoggerFactory.getLogger(ContainerHealthTaskV2.class); + LoggerFactory.getLogger(ContainerHealthTask.class); + private static final long MIN_NEXT_RUN_INTERVAL_MS = 60_000L; private final ReconStorageContainerManagerFacade reconScm; private final long interval; - private final ContainerHealthTaskV2Metrics metrics; + private final ContainerHealthTaskMetrics taskMetrics; @Inject - public ContainerHealthTaskV2( + public ContainerHealthTask( ReconTaskConfig reconTaskConfig, ReconTaskStatusUpdaterManager taskStatusUpdaterManager, ReconStorageContainerManagerFacade reconScm) { super(taskStatusUpdaterManager); this.reconScm = reconScm; this.interval = reconTaskConfig.getMissingContainerTaskInterval().toMillis(); - this.metrics = ContainerHealthTaskV2Metrics.create(); - LOG.info("Initialized ContainerHealthTaskV2 with Local ReplicationManager, interval={}ms", + this.taskMetrics = ContainerHealthTaskMetrics.create(); + LOG.info("Initialized ContainerHealthTask with Local ReplicationManager, interval={}ms", interval); } @@ -80,16 +81,16 @@ protected void run() { try { initializeAndRunTask(); long elapsed = Time.monotonicNow() - cycleStart; - long sleepMs = Math.max(0, interval - elapsed); + long sleepMs = Math.max(MIN_NEXT_RUN_INTERVAL_MS, interval - elapsed); if (sleepMs > 0) { Thread.sleep(sleepMs); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOG.info("ContainerHealthTaskV2 interrupted"); + LOG.info("ContainerHealthTask interrupted"); break; } catch (Exception e) { - LOG.error("Error in ContainerHealthTaskV2", e); + LOG.error("Error in ContainerHealthTask", e); } } } @@ -107,7 +108,7 @@ protected void run() { @Override protected void runTask() throws Exception { long start = Time.monotonicNow(); - LOG.info("ContainerHealthTaskV2 starting - using local ReplicationManager"); + LOG.info("ContainerHealthTask starting - using local ReplicationManager"); // Get Recon's ReplicationManager (actually a ReconReplicationManager instance) ReconReplicationManager reconRM = @@ -121,15 +122,15 @@ protected void runTask() throws Exception { boolean succeeded = false; try { reconRM.processAll(); - metrics.incrSuccess(); + taskMetrics.incrSuccess(); succeeded = true; } catch (Exception e) { - metrics.incrFailure(); + taskMetrics.incrFailure(); throw e; } finally { long durationMs = Time.monotonicNow() - start; - metrics.addRunTime(durationMs); - LOG.info("ContainerHealthTaskV2 completed with status={} in {} ms", + taskMetrics.addRunTime(durationMs); + LOG.info("ContainerHealthTask completed with status={} in {} ms", succeeded ? "success" : "failure", durationMs); } } @@ -137,6 +138,6 @@ protected void runTask() throws Exception { @Override public synchronized void stop() { super.stop(); - metrics.unRegister(); + taskMetrics.unRegister(); } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index f62716e8f595..6b142a62458a 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.time.Clock; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -34,14 +35,15 @@ import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; import org.apache.hadoop.hdds.scm.container.ContainerReplica; import org.apache.hadoop.hdds.scm.container.replication.MonitoringReplicationQueue; +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.EventPublisher; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.ContainerStateKey; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager.ContainerStateKey; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager.UnhealthyContainerRecord; import org.apache.hadoop.util.Time; import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; import org.slf4j.Logger; @@ -79,7 +81,7 @@ public class ReconReplicationManager extends ReplicationManager { LoggerFactory.getLogger(ReconReplicationManager.class); private static final int PERSIST_CHUNK_SIZE = 50_000; - private final ContainerHealthSchemaManagerV2 healthSchemaManager; + private final ContainerHealthSchemaManager healthSchemaManager; private final ContainerManager containerManager; /** @@ -182,7 +184,7 @@ public InitContext build() { public ReconReplicationManager( InitContext initContext, - ContainerHealthSchemaManagerV2 healthSchemaManager) throws IOException { + ContainerHealthSchemaManager healthSchemaManager) throws IOException { // Call parent with stub PendingOps (proven to not cause false positives) super( @@ -208,7 +210,7 @@ public ReconReplicationManager( *

    In Recon, we don't want the ReplicationManager's background threads * (replicationMonitor, underReplicatedProcessor, overReplicatedProcessor) * to run continuously. Instead, we call processAll() manually from - * ContainerHealthTaskV2 on a schedule.

    + * ContainerHealthTask on a schedule.

    * *

    This prevents: *

      @@ -221,7 +223,7 @@ public ReconReplicationManager( @Override public synchronized void start() { LOG.info("ReconReplicationManager.start() called - no-op (manual invocation via processAll())"); - // Do nothing - we call processAll() manually from ContainerHealthTaskV2 + // Do nothing - we call processAll() manually from ContainerHealthTask } /** @@ -303,13 +305,14 @@ public synchronized void processAll() { report.increment(container.getState()); try { ContainerID cid = container.containerID(); + Set replicas = containerManager.getContainerReplicas(cid); + List pendingOps = getPendingReplicationOps(cid); // Call inherited processContainer - this runs SCM's health check chain // readOnly=true ensures no commands are generated - processContainer(container, nullQueue, report, true); + processContainer(container, replicas, pendingOps, nullQueue, report, true); // ADDITIONAL CHECK: Detect REPLICA_MISMATCH (Recon-specific, not in SCM) - Set replicas = containerManager.getContainerReplicas(cid); if (hasDataChecksumMismatch(replicas)) { report.addReplicaMismatchContainer(cid); LOG.debug("Container {} has data checksum mismatch across replicas", cid); @@ -348,36 +351,78 @@ private void storeHealthStatesToDatabase( long currentTime = System.currentTimeMillis(); ProcessingStats totalStats = new ProcessingStats(); int totalReplicaMismatchCount = 0; + Set missingContainers = unionAsIdSet(report, + ContainerHealthState.MISSING, + ContainerHealthState.QUASI_CLOSED_STUCK_MISSING, + ContainerHealthState.MISSING_UNDER_REPLICATED); + Set underReplicatedContainers = unionAsIdSet(report, + ContainerHealthState.UNDER_REPLICATED, + ContainerHealthState.UNHEALTHY_UNDER_REPLICATED, + ContainerHealthState.QUASI_CLOSED_STUCK_UNDER_REPLICATED, + ContainerHealthState.MISSING_UNDER_REPLICATED); + Set overReplicatedContainers = unionAsIdSet(report, + ContainerHealthState.OVER_REPLICATED, + ContainerHealthState.UNHEALTHY_OVER_REPLICATED, + ContainerHealthState.QUASI_CLOSED_STUCK_OVER_REPLICATED); + Set misReplicatedContainers = + asIdSet(report, ContainerHealthState.MIS_REPLICATED); + logUnmappedScmStates(report); + Set replicaMismatchContainers = + new HashSet<>(report.getReplicaMismatchContainers()); for (int from = 0; from < allContainers.size(); from += PERSIST_CHUNK_SIZE) { int to = Math.min(from + PERSIST_CHUNK_SIZE, allContainers.size()); List chunkContainerIds = collectContainerIds(allContainers, from, to); - Set chunkContainerIdSet = new HashSet<>(chunkContainerIds); Map existingInStateSinceByContainerAndState = healthSchemaManager.getExistingInStateSinceByContainerIds(chunkContainerIds); - List recordsToInsert = new ArrayList<>(); + List recordsToInsert = new ArrayList<>(); + List existingContainerIdsToDelete = + collectExistingContainerIds(existingInStateSinceByContainerAndState); ProcessingStats chunkStats = new ProcessingStats(); Set negativeSizeRecorded = new HashSet<>(); - report.forEachContainerByState((state, cid) -> { - if (!chunkContainerIdSet.contains(cid.getId())) { - return; - } + for (int i = from; i < to; i++) { + ContainerInfo container = allContainers.get(i); + ContainerID containerId = container.containerID(); try { - handleScmStateContainer(state, cid, currentTime, - existingInStateSinceByContainerAndState, recordsToInsert, - negativeSizeRecorded, chunkStats); + if (missingContainers.contains(containerId)) { + handleMissingContainer(containerId, currentTime, + existingInStateSinceByContainerAndState, recordsToInsert, chunkStats); + } + if (underReplicatedContainers.contains(containerId)) { + chunkStats.incrementUnderRepCount(); + handleReplicaStateContainer(containerId, currentTime, + UnHealthyContainerStates.UNDER_REPLICATED, + existingInStateSinceByContainerAndState, recordsToInsert, + negativeSizeRecorded, chunkStats); + } + if (overReplicatedContainers.contains(containerId)) { + chunkStats.incrementOverRepCount(); + handleReplicaStateContainer(containerId, currentTime, + UnHealthyContainerStates.OVER_REPLICATED, + existingInStateSinceByContainerAndState, recordsToInsert, + negativeSizeRecorded, chunkStats); + } + if (misReplicatedContainers.contains(containerId)) { + chunkStats.incrementMisRepCount(); + handleReplicaStateContainer(containerId, currentTime, + UnHealthyContainerStates.MIS_REPLICATED, + existingInStateSinceByContainerAndState, recordsToInsert, + negativeSizeRecorded, chunkStats); + } + if (replicaMismatchContainers.contains(containerId)) { + processReplicaMismatchContainer(containerId, currentTime, + existingInStateSinceByContainerAndState, recordsToInsert); + totalReplicaMismatchCount++; + } } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing {} state", cid, state, e); + LOG.warn("Container {} not found when processing unhealthy states", + containerId, e); } - }); + } - int chunkReplicaMismatchCount = processReplicaMismatchContainersForChunk( - report, currentTime, existingInStateSinceByContainerAndState, - recordsToInsert, chunkContainerIdSet); - totalReplicaMismatchCount += chunkReplicaMismatchCount; totalStats.add(chunkStats); - persistUnhealthyRecords(chunkContainerIds, recordsToInsert); + persistUnhealthyRecords(existingContainerIdsToDelete, recordsToInsert); } LOG.info("Stored {} MISSING, {} EMPTY_MISSING, {} UNDER_REPLICATED, " + @@ -388,47 +433,11 @@ private void storeHealthStatesToDatabase( totalReplicaMismatchCount); } - private void handleScmStateContainer( - ContainerHealthState state, - ContainerID containerId, - long currentTime, - Map existingInStateSinceByContainerAndState, - List recordsToInsert, - Set negativeSizeRecorded, - ProcessingStats stats) throws ContainerNotFoundException { - switch (state) { - case MISSING: - handleMissingContainer(containerId, currentTime, - existingInStateSinceByContainerAndState, recordsToInsert, stats); - break; - case UNDER_REPLICATED: - stats.incrementUnderRepCount(); - handleReplicaStateContainer(containerId, currentTime, - UnHealthyContainerStates.UNDER_REPLICATED, - existingInStateSinceByContainerAndState, recordsToInsert, negativeSizeRecorded, stats); - break; - case OVER_REPLICATED: - stats.incrementOverRepCount(); - handleReplicaStateContainer(containerId, currentTime, - UnHealthyContainerStates.OVER_REPLICATED, - existingInStateSinceByContainerAndState, recordsToInsert, negativeSizeRecorded, stats); - break; - case MIS_REPLICATED: - stats.incrementMisRepCount(); - handleReplicaStateContainer(containerId, currentTime, - UnHealthyContainerStates.MIS_REPLICATED, - existingInStateSinceByContainerAndState, recordsToInsert, negativeSizeRecorded, stats); - break; - default: - break; - } - } - private void handleMissingContainer( ContainerID containerId, long currentTime, Map existingInStateSinceByContainerAndState, - List recordsToInsert, + List recordsToInsert, ProcessingStats stats) throws ContainerNotFoundException { ContainerInfo container = containerManager.getContainer(containerId); int expected = container.getReplicationConfig().getRequiredNodes(); @@ -459,7 +468,7 @@ private void handleReplicaStateContainer( long currentTime, UnHealthyContainerStates targetState, Map existingInStateSinceByContainerAndState, - List recordsToInsert, + List recordsToInsert, Set negativeSizeRecorded, ProcessingStats stats) throws ContainerNotFoundException { ContainerInfo container = containerManager.getContainer(containerId); @@ -474,36 +483,22 @@ private void handleReplicaStateContainer( existingInStateSinceByContainerAndState, negativeSizeRecorded, stats); } - private int processReplicaMismatchContainersForChunk( - ReconReplicationManagerReport report, + private void processReplicaMismatchContainer( + ContainerID containerId, long currentTime, Map existingInStateSinceByContainerAndState, - List recordsToInsert, - Set chunkContainerIdSet) { - List replicaMismatchContainers = report.getReplicaMismatchContainers(); - int chunkReplicaMismatchCount = 0; - for (ContainerID cid : replicaMismatchContainers) { - if (!chunkContainerIdSet.contains(cid.getId())) { - continue; - } - try { - ContainerInfo container = containerManager.getContainer(cid); - Set replicas = containerManager.getContainerReplicas(cid); - int expected = container.getReplicationConfig().getRequiredNodes(); - int actual = replicas.size(); - recordsToInsert.add(createRecord(container, - UnHealthyContainerStates.REPLICA_MISMATCH, - resolveInStateSince(container.getContainerID(), - UnHealthyContainerStates.REPLICA_MISMATCH, currentTime, - existingInStateSinceByContainerAndState), - expected, actual, - "Data checksum mismatch across replicas")); - chunkReplicaMismatchCount++; - } catch (ContainerNotFoundException e) { - LOG.warn("Container {} not found when processing REPLICA_MISMATCH state", cid, e); - } - } - return chunkReplicaMismatchCount; + List recordsToInsert) throws ContainerNotFoundException { + ContainerInfo container = containerManager.getContainer(containerId); + Set replicas = containerManager.getContainerReplicas(containerId); + int expected = container.getReplicationConfig().getRequiredNodes(); + int actual = replicas.size(); + recordsToInsert.add(createRecord(container, + UnHealthyContainerStates.REPLICA_MISMATCH, + resolveInStateSince(container.getContainerID(), + UnHealthyContainerStates.REPLICA_MISMATCH, currentTime, + existingInStateSinceByContainerAndState), + expected, actual, + "Data checksum mismatch across replicas")); } private List collectContainerIds(List allContainers, @@ -517,12 +512,11 @@ private List collectContainerIds(List allContainers, private void persistUnhealthyRecords( List containerIdsToDelete, - List recordsToInsert) { - LOG.info("Deleting SCM states for {} containers", containerIdsToDelete.size()); - healthSchemaManager.batchDeleteSCMStatesForContainers(containerIdsToDelete); - - LOG.info("Inserting {} unhealthy container records", recordsToInsert.size()); - healthSchemaManager.insertUnhealthyContainerRecords(recordsToInsert); + List recordsToInsert) { + LOG.info("Replacing unhealthy container records atomically: deleteRowsFor={} containers, insert={}", + containerIdsToDelete.size(), recordsToInsert.size()); + healthSchemaManager.replaceUnhealthyContainerRecordsAtomically( + containerIdsToDelete, recordsToInsert); } private boolean isEmptyMissing(ContainerInfo container) { @@ -537,7 +531,7 @@ private void addNegativeSizeRecordIfNeeded( ContainerInfo container, long currentTime, int actualReplicaCount, - List recordsToInsert, + List recordsToInsert, Map existingInStateSinceByContainerAndState, Set negativeSizeRecorded, ProcessingStats stats) { @@ -575,6 +569,77 @@ private String reasonForState(UnHealthyContainerStates state) { } } + private Set asIdSet(ReconReplicationManagerReport report, + ContainerHealthState state) { + List containers = report.getAllContainers(state); + if (containers.isEmpty()) { + return Collections.emptySet(); + } + return new HashSet<>(containers); + } + + private Set unionAsIdSet(ReconReplicationManagerReport report, + ContainerHealthState... states) { + Set result = null; + for (ContainerHealthState state : states) { + List containers = report.getAllContainers(state); + if (containers.isEmpty()) { + continue; + } + if (result == null) { + result = new HashSet<>(); + } + result.addAll(containers); + } + return result == null ? Collections.emptySet() : result; + } + + private void logUnmappedScmStates(ReconReplicationManagerReport report) { + for (Map.Entry> entry : + report.getAllContainersByState().entrySet()) { + ContainerHealthState state = entry.getKey(); + if (isMappedScmState(state)) { + continue; + } + int count = entry.getValue().size(); + if (count > 0) { + LOG.warn("SCM state {} has {} containers but is not mapped to " + + "UNHEALTHY_CONTAINERS allowed states; skipping persistence " + + "for this state in this run", + state, count); + } + } + } + + private boolean isMappedScmState(ContainerHealthState state) { + switch (state) { + case MISSING: + case UNDER_REPLICATED: + case OVER_REPLICATED: + case MIS_REPLICATED: + case UNHEALTHY_UNDER_REPLICATED: + case UNHEALTHY_OVER_REPLICATED: + case MISSING_UNDER_REPLICATED: + case QUASI_CLOSED_STUCK_MISSING: + case QUASI_CLOSED_STUCK_UNDER_REPLICATED: + case QUASI_CLOSED_STUCK_OVER_REPLICATED: + return true; + default: + return false; + } + } + + private List collectExistingContainerIds( + Map existingInStateSinceByContainerAndState) { + if (existingInStateSinceByContainerAndState.isEmpty()) { + return Collections.emptyList(); + } + Set existingContainerIds = new HashSet<>(); + existingInStateSinceByContainerAndState.keySet() + .forEach(key -> existingContainerIds.add(key.getContainerId())); + return new ArrayList<>(existingContainerIds); + } + private static final class ProcessingStats { private int missingCount; private int underRepCount; @@ -626,16 +691,16 @@ void add(ProcessingStats other) { * @param expectedReplicaCount Expected number of replicas * @param actualReplicaCount Actual number of replicas * @param reason Human-readable reason for the health state - * @return UnhealthyContainerRecordV2 ready for insertion + * @return UnhealthyContainerRecord ready for insertion */ - private UnhealthyContainerRecordV2 createRecord( + private ContainerHealthSchemaManager.UnhealthyContainerRecord createRecord( ContainerInfo container, UnHealthyContainerStates state, long timestamp, int expectedReplicaCount, int actualReplicaCount, String reason) { - return new UnhealthyContainerRecordV2( + return new UnhealthyContainerRecord( container.getContainerID(), state.toString(), timestamp, diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskMetrics.java similarity index 76% rename from hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java rename to hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskMetrics.java index 2449f21f8604..eb1cc3bf4eec 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskV2Metrics.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/metrics/ContainerHealthTaskMetrics.java @@ -27,33 +27,33 @@ import org.apache.hadoop.ozone.OzoneConsts; /** - * Runtime metrics for ContainerHealthTaskV2 execution. + * Runtime metrics for ContainerHealthTask execution. */ @InterfaceAudience.Private -@Metrics(about = "ContainerHealthTaskV2 Metrics", context = OzoneConsts.OZONE) -public final class ContainerHealthTaskV2Metrics { +@Metrics(about = "ContainerHealthTask Metrics", context = OzoneConsts.OZONE) +public final class ContainerHealthTaskMetrics { private static final String SOURCE_NAME = - ContainerHealthTaskV2Metrics.class.getSimpleName(); + ContainerHealthTaskMetrics.class.getSimpleName(); - @Metric(about = "ContainerHealthTaskV2 runtime in milliseconds") + @Metric(about = "ContainerHealthTask runtime in milliseconds") private MutableRate runTimeMs; - @Metric(about = "ContainerHealthTaskV2 successful runs") + @Metric(about = "ContainerHealthTask successful runs") private MutableCounterLong runSuccessCount; - @Metric(about = "ContainerHealthTaskV2 failed runs") + @Metric(about = "ContainerHealthTask failed runs") private MutableCounterLong runFailureCount; - private ContainerHealthTaskV2Metrics() { + private ContainerHealthTaskMetrics() { } - public static ContainerHealthTaskV2Metrics create() { + public static ContainerHealthTaskMetrics create() { MetricsSystem ms = DefaultMetricsSystem.instance(); return ms.register( SOURCE_NAME, - "ContainerHealthTaskV2 Metrics", - new ContainerHealthTaskV2Metrics()); + "ContainerHealthTask Metrics", + new ContainerHealthTaskMetrics()); } public void unRegister() { diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java similarity index 77% rename from hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java rename to hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java index 1434c5da0a29..d3c9c7d7195d 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManagerV2.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.ozone.recon.schema.ContainerSchemaDefinition; @@ -47,12 +48,12 @@ import org.slf4j.LoggerFactory; /** - * Manager for UNHEALTHY_CONTAINERS table used by ContainerHealthTaskV2. + * Manager for UNHEALTHY_CONTAINERS table used by ContainerHealthTask. */ @Singleton -public class ContainerHealthSchemaManagerV2 { +public class ContainerHealthSchemaManager { private static final Logger LOG = - LoggerFactory.getLogger(ContainerHealthSchemaManagerV2.class); + LoggerFactory.getLogger(ContainerHealthSchemaManager.class); private static final int BATCH_INSERT_CHUNK_SIZE = 1000; /** @@ -69,15 +70,15 @@ public class ContainerHealthSchemaManagerV2 { */ static final int MAX_DELETE_CHUNK_SIZE = 1_000; - private final UnhealthyContainersDao unhealthyContainersV2Dao; - private final ContainerSchemaDefinition containerSchemaDefinitionV2; + private final UnhealthyContainersDao unhealthyContainersDao; + private final ContainerSchemaDefinition containerSchemaDefinition; @Inject - public ContainerHealthSchemaManagerV2( - ContainerSchemaDefinition containerSchemaDefinitionV2, - UnhealthyContainersDao unhealthyContainersV2Dao) { - this.unhealthyContainersV2Dao = unhealthyContainersV2Dao; - this.containerSchemaDefinitionV2 = containerSchemaDefinitionV2; + public ContainerHealthSchemaManager( + ContainerSchemaDefinition containerSchemaDefinition, + UnhealthyContainersDao unhealthyContainersDao) { + this.unhealthyContainersDao = unhealthyContainersDao; + this.containerSchemaDefinition = containerSchemaDefinition; } /** @@ -85,7 +86,7 @@ public ContainerHealthSchemaManagerV2( * Uses JOOQ's batch API for optimal performance (single SQL statement for all records). * Falls back to individual insert-or-update if batch insert fails (e.g., duplicate keys). */ - public void insertUnhealthyContainerRecords(List recs) { + public void insertUnhealthyContainerRecords(List recs) { if (recs == null || recs.isEmpty()) { return; } @@ -95,10 +96,11 @@ public void insertUnhealthyContainerRecords(List rec rec.getContainerId(), rec.getContainerState())); } - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); try { - batchInsertInChunks(dslContext, recs); + dslContext.transaction(configuration -> + batchInsertInChunks(configuration.dsl(), recs)); LOG.debug("Batch inserted {} unhealthy container records", recs.size()); @@ -115,34 +117,31 @@ public void insertUnhealthyContainerRecords(List rec } private void batchInsertInChunks(DSLContext dslContext, - List recs) { - dslContext.transaction(configuration -> { - DSLContext txContext = configuration.dsl(); - List 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(txContext, recs.get(i))); - } - txContext.batchInsert(records).execute(); + List recs) { + List 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 void fallbackInsertOrUpdate(List recs) { - try (Connection connection = containerSchemaDefinitionV2.getDataSource().getConnection()) { + private void fallbackInsertOrUpdate(List recs) { + try (Connection connection = containerSchemaDefinition.getDataSource().getConnection()) { connection.setAutoCommit(false); try { - for (UnhealthyContainerRecordV2 rec : recs) { + for (UnhealthyContainerRecord rec : recs) { UnhealthyContainers jooqRec = toJooqPojo(rec); try { - unhealthyContainersV2Dao.insert(jooqRec); + unhealthyContainersDao.insert(jooqRec); } catch (DataAccessException insertEx) { // Duplicate key - update existing record - unhealthyContainersV2Dao.update(jooqRec); + unhealthyContainersDao.update(jooqRec); } } connection.commit(); @@ -161,7 +160,7 @@ private void fallbackInsertOrUpdate(List recs) { } private UnhealthyContainersRecord toJooqRecord(DSLContext txContext, - UnhealthyContainerRecordV2 rec) { + UnhealthyContainerRecord rec) { UnhealthyContainersRecord record = txContext.newRecord(UNHEALTHY_CONTAINERS); record.setContainerId(rec.getContainerId()); record.setContainerState(rec.getContainerState()); @@ -173,7 +172,7 @@ private UnhealthyContainersRecord toJooqRecord(DSLContext txContext, return record; } - private UnhealthyContainers toJooqPojo(UnhealthyContainerRecordV2 rec) { + private UnhealthyContainers toJooqPojo(UnhealthyContainerRecord rec) { return new UnhealthyContainers( rec.getContainerId(), rec.getContainerState(), @@ -211,37 +210,58 @@ public void batchDeleteSCMStatesForContainers(List containerIds) { return; } - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + 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; - // Chunk the container IDs so each DELETE statement stays within Derby's - // generated-bytecode limit (MAX_DELETE_CHUNK_SIZE IDs per statement). 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); - try { - 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; - } catch (Exception e) { - LOG.error("Failed to batch delete health states for {} containers (chunk {}-{})", - chunk.size(), from, to, e); - throw new RuntimeException("Failed to batch delete health states", e); - } + 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; } - - LOG.debug("Batch deleted {} health state records for {} containers", - totalDeleted, containerIds.size()); + return totalDeleted; } /** @@ -254,7 +274,7 @@ public Map getExistingInStateSinceByContainerIds( return new HashMap<>(); } - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); Map existing = new HashMap<>(); try { dslContext.select( @@ -284,9 +304,9 @@ public Map getExistingInStateSinceByContainerIds( /** * Get summary of unhealthy containers grouped by state from V2 table. */ - public List getUnhealthyContainersSummary() { - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); - List result = new ArrayList<>(); + public List getUnhealthyContainersSummary() { + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); + List result = new ArrayList<>(); try { return dslContext @@ -294,7 +314,7 @@ public List getUnhealthyContainersSummary() { count().as("cnt")) .from(UNHEALTHY_CONTAINERS) .groupBy(UNHEALTHY_CONTAINERS.CONTAINER_STATE) - .fetchInto(UnhealthyContainersSummaryV2.class); + .fetchInto(UnhealthyContainersSummary.class); } catch (Exception e) { LOG.error("Failed to get summary from V2 table", e); return result; @@ -304,9 +324,9 @@ public List getUnhealthyContainersSummary() { /** * Get unhealthy containers from V2 table. */ - public List getUnhealthyContainers( + public List getUnhealthyContainers( UnHealthyContainerStates state, long minContainerId, long maxContainerId, int limit) { - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); SelectQuery query = dslContext.selectQuery(); query.addFrom(UNHEALTHY_CONTAINERS); @@ -352,7 +372,7 @@ public List getUnhealthyContainers( } // Forward-pagination path: SQL already orders ASC — no Java re-sort needed. - return stream.map(record -> new UnhealthyContainerRecordV2( + return stream.map(record -> new UnhealthyContainerRecord( record.getContainerId(), record.getContainerState(), record.getInStateSince(), @@ -372,7 +392,7 @@ public List getUnhealthyContainers( */ @VisibleForTesting public void clearAllUnhealthyContainerRecords() { - DSLContext dslContext = containerSchemaDefinitionV2.getDSLContext(); + DSLContext dslContext = containerSchemaDefinition.getDSLContext(); try { dslContext.deleteFrom(UNHEALTHY_CONTAINERS).execute(); LOG.info("Cleared all V2 unhealthy container records"); @@ -384,7 +404,7 @@ public void clearAllUnhealthyContainerRecords() { /** * POJO representing a record in UNHEALTHY_CONTAINERS table. */ - public static class UnhealthyContainerRecordV2 { + public static class UnhealthyContainerRecord { private final long containerId; private final String containerState; private final long inStateSince; @@ -393,9 +413,9 @@ public static class UnhealthyContainerRecordV2 { private final int replicaDelta; private final String reason; - public UnhealthyContainerRecordV2(long containerId, String containerState, - long inStateSince, int expectedReplicaCount, int actualReplicaCount, - int replicaDelta, 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; @@ -446,6 +466,10 @@ public ContainerStateKey(long containerId, String state) { this.state = state; } + public long getContainerId() { + return containerId; + } + @Override public boolean equals(Object other) { if (this == other) { @@ -460,18 +484,18 @@ public boolean equals(Object other) { @Override public int hashCode() { - return Long.hashCode(containerId) * 31 + state.hashCode(); + return Objects.hash(containerId, state); } } /** * POJO representing a summary record for unhealthy containers. */ - public static class UnhealthyContainersSummaryV2 { + public static class UnhealthyContainersSummary { private final String containerState; private final int count; - public UnhealthyContainersSummaryV2(String containerState, int count) { + public UnhealthyContainersSummary(String containerState, int count) { this.containerState = containerState; this.count = count; } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java index 1290c5962abd..42564658ba8a 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java @@ -50,7 +50,7 @@ import org.apache.hadoop.hdds.utils.db.DBStore; import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.ozone.common.statemachine.InvalidStateTransitionException; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; @@ -66,7 +66,7 @@ public class ReconContainerManager extends ContainerManagerImpl { LoggerFactory.getLogger(ReconContainerManager.class); private final StorageContainerServiceProvider scmClient; private final PipelineManager pipelineManager; - private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; + private final ContainerHealthSchemaManager containerHealthSchemaManager; private final ReconContainerMetadataManager cdbServiceProvider; private final Table nodeDB; // Container ID -> Datanode UUID -> Timestamp @@ -81,7 +81,7 @@ public ReconContainerManager( Table containerStore, PipelineManager pipelineManager, StorageContainerServiceProvider scm, - ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2, + ContainerHealthSchemaManager containerHealthSchemaManager, ReconContainerMetadataManager reconContainerMetadataManager, SCMHAManager scmhaManager, SequenceIdGenerator sequenceIdGen, @@ -91,7 +91,7 @@ public ReconContainerManager( pendingOps); this.scmClient = scm; this.pipelineManager = pipelineManager; - this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; + this.containerHealthSchemaManager = containerHealthSchemaManager; this.cdbServiceProvider = reconContainerMetadataManager; this.nodeDB = ReconSCMDBDefinition.NODES.getTable(store); this.replicaHistoryMap = new ConcurrentHashMap<>(); @@ -341,8 +341,8 @@ public void removeContainerReplica(ContainerID containerID, } @VisibleForTesting - public ContainerHealthSchemaManagerV2 getContainerSchemaManagerV2() { - return containerHealthSchemaManagerV2; + public ContainerHealthSchemaManager getContainerSchemaManagerV2() { + return containerHealthSchemaManager; } @VisibleForTesting 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 bed1e857fb22..79ac3ad95bcd 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 @@ -114,10 +114,10 @@ import org.apache.hadoop.ozone.recon.ReconContext; import org.apache.hadoop.ozone.recon.ReconServerConfigKeys; import org.apache.hadoop.ozone.recon.ReconUtils; -import org.apache.hadoop.ozone.recon.fsck.ContainerHealthTaskV2; +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.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; import org.apache.hadoop.ozone.recon.tasks.ContainerSizeCountTask; @@ -157,7 +157,7 @@ public class ReconStorageContainerManagerFacade private final SequenceIdGenerator sequenceIdGen; private final ReconScmTask containerHealthTaskV2; private final DataSource dataSource; - private final ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2; + private final ContainerHealthSchemaManager containerHealthSchemaManager; private DBStore dbStore; private ReconNodeManager nodeManager; @@ -187,7 +187,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, ReconContext reconContext, DataSource dataSource, ReconTaskStatusUpdaterManager taskStatusUpdaterManager, - ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2) + ContainerHealthSchemaManager containerHealthSchemaManager) throws IOException { reconNodeDetails = reconUtils.getReconNodeDetails(conf); this.threadNamePrefix = reconNodeDetails.threadNamePrefix(); @@ -249,7 +249,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, dbStore, ReconSCMDBDefinition.CONTAINERS.getTable(dbStore), pipelineManager, scmServiceProvider, - containerHealthSchemaManagerV2, + containerHealthSchemaManager, reconContainerMetadataManager, scmhaManager, sequenceIdGen, pendingOps); this.scmServiceProvider = scmServiceProvider; @@ -270,9 +270,9 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, PipelineSyncTask pipelineSyncTask = new PipelineSyncTask(pipelineManager, nodeManager, scmServiceProvider, reconTaskConfig, taskStatusUpdaterManager); - // Create ContainerHealthTaskV2 (always runs, writes to UNHEALTHY_CONTAINERS) - LOG.info("Creating ContainerHealthTaskV2"); - containerHealthTaskV2 = new ContainerHealthTaskV2( + // Create ContainerHealthTask (always runs, writes to UNHEALTHY_CONTAINERS) + LOG.info("Creating ContainerHealthTask"); + containerHealthTaskV2 = new ContainerHealthTask( reconTaskConfig, taskStatusUpdaterManager, this // ReconStorageContainerManagerFacade - provides access to ReconReplicationManager @@ -281,7 +281,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, this.containerSizeCountTask = new ContainerSizeCountTask(containerManager, reconTaskConfig, containerCountBySizeDao, utilizationSchemaDefinition, taskStatusUpdaterManager); - this.containerHealthSchemaManagerV2 = containerHealthSchemaManagerV2; + this.containerHealthSchemaManager = containerHealthSchemaManager; this.dataSource = dataSource; // Initialize Recon's ReplicationManager for local health checks @@ -300,7 +300,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, .setNodeManager(nodeManager) .setClock(Clock.system(ZoneId.systemDefault())) .build(), - containerHealthSchemaManagerV2 + containerHealthSchemaManager ); LOG.info("Successfully created ReconReplicationManager"); } catch (IOException e) { 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 92adcf931fd1..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 @@ -54,7 +54,7 @@ import org.apache.hadoop.ozone.recon.api.types.ClusterStateResponse; import org.apache.hadoop.ozone.recon.api.types.ClusterStorageReport; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; import org.apache.hadoop.ozone.recon.scm.ReconNodeManager; @@ -110,7 +110,7 @@ public void setUp() throws Exception { .addBinding(StorageContainerServiceProvider.class, mock(StorageContainerServiceProviderImpl.class)) .addBinding(ClusterStateEndpoint.class) - .addBinding(ContainerHealthSchemaManagerV2.class) + .addBinding(ContainerHealthSchemaManager.class) .build(); OzoneStorageContainerManager ozoneStorageContainerManager = reconTestInjector.getInstance(OzoneStorageContainerManager.class); @@ -118,14 +118,14 @@ public void setUp() throws Exception { ozoneStorageContainerManager.getContainerManager(); ReconPipelineManager reconPipelineManager = (ReconPipelineManager) ozoneStorageContainerManager.getPipelineManager(); - ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2 = - reconTestInjector.getInstance(ContainerHealthSchemaManagerV2.class); + ContainerHealthSchemaManager containerHealthSchemaManager = + reconTestInjector.getInstance(ContainerHealthSchemaManager.class); ReconGlobalStatsManager reconGlobalStatsManager = reconTestInjector.getInstance(ReconGlobalStatsManager.class); conf = mock(OzoneConfiguration.class); clusterStateEndpoint = new ClusterStateEndpoint(ozoneStorageContainerManager, - reconGlobalStatsManager, containerHealthSchemaManagerV2, conf); + reconGlobalStatsManager, containerHealthSchemaManager, conf); pipeline = getRandomPipeline(); pipelineID = pipeline.getId(); reconPipelineManager.addPipeline(pipeline); @@ -177,8 +177,8 @@ public void testStorageReportIsClusterStorageReport() { ReconNodeManager mockNodeManager = mock(ReconNodeManager.class); ReconPipelineManager mockPipelineManager = mock(ReconPipelineManager.class); ReconContainerManager mockContainerManager = mock(ReconContainerManager.class); - ContainerHealthSchemaManagerV2 mockContainerHealthSchemaManagerV2 = - mock(ContainerHealthSchemaManagerV2.class); + ContainerHealthSchemaManager mockContainerHealthSchemaManager = + mock(ContainerHealthSchemaManager.class); ReconGlobalStatsManager mockGlobalStatsManager = mock(ReconGlobalStatsManager.class); OzoneConfiguration mockConf = mock(OzoneConfiguration.class); @@ -194,7 +194,7 @@ public void testStorageReportIsClusterStorageReport() { .thenReturn(0); when(mockContainerManager.getContainerStateCount(HddsProtos.LifeCycleState.DELETED)) .thenReturn(0); - when(mockContainerHealthSchemaManagerV2.getUnhealthyContainers( + when(mockContainerHealthSchemaManager.getUnhealthyContainers( any(), anyLong(), anyLong(), anyInt())).thenReturn(Collections.emptyList()); SCMNodeStat scmNodeStat = new SCMNodeStat( @@ -209,7 +209,7 @@ public void testStorageReportIsClusterStorageReport() { .thenReturn(new SpaceUsageSource.Fixed(2000L, 1500L, 500L)); ClusterStateEndpoint endpoint = new ClusterStateEndpoint( - mockScm, mockGlobalStatsManager, mockContainerHealthSchemaManagerV2, mockConf); + mockScm, mockGlobalStatsManager, mockContainerHealthSchemaManager, mockConf); ClusterStateResponse response = (ClusterStateResponse) endpoint.getClusterState().getEntity(); 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 3cdd4e1f5e1c..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 @@ -97,7 +97,7 @@ import org.apache.hadoop.ozone.recon.api.types.MissingContainersResponse; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainerMetadata; import org.apache.hadoop.ozone.recon.api.types.UnhealthyContainersResponse; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.persistence.ContainerHistory; import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager; import org.apache.hadoop.ozone.recon.scm.ReconContainerManager; @@ -140,7 +140,7 @@ public class TestContainerEndpoint { private ReconContainerMetadataManager reconContainerMetadataManager; private ContainerEndpoint containerEndpoint; private boolean isSetupDone = false; - private ContainerHealthSchemaManagerV2 containerHealthSchemaManager; + private ContainerHealthSchemaManager containerHealthSchemaManager; private ReconOMMetadataManager reconOMMetadataManager; private OzoneConfiguration omConfiguration; @@ -197,7 +197,7 @@ private void initializeInjector() throws Exception { .addBinding(StorageContainerServiceProvider.class, mock(StorageContainerServiceProviderImpl.class)) .addBinding(ContainerEndpoint.class) - .addBinding(ContainerHealthSchemaManagerV2.class) + .addBinding(ContainerHealthSchemaManager.class) .build(); OzoneStorageContainerManager ozoneStorageContainerManager = @@ -210,7 +210,7 @@ private void initializeInjector() throws Exception { reconTestInjector.getInstance(ReconContainerMetadataManager.class); containerEndpoint = reconTestInjector.getInstance(ContainerEndpoint.class); containerHealthSchemaManager = - reconTestInjector.getInstance(ContainerHealthSchemaManagerV2.class); + reconTestInjector.getInstance(ContainerHealthSchemaManager.class); this.reconNamespaceSummaryManager = reconTestInjector.getInstance(ReconNamespaceSummaryManager.class); @@ -1157,9 +1157,9 @@ 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); - ArrayList records = + ArrayList records = new ArrayList<>(); - records.add(new ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2( + records.add(new ContainerHealthSchemaManager.UnhealthyContainerRecord( cID, state, 12345L, expected, actual, delta, reason)); containerHealthSchemaManager.insertUnhealthyContainerRecords(records); diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java index a543e076085f..07dd11d8021e 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestEndpoints.java @@ -123,7 +123,7 @@ import org.apache.hadoop.ozone.recon.api.types.VolumesResponse; import org.apache.hadoop.ozone.recon.common.ReconTestUtils; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +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; @@ -280,7 +280,7 @@ private void initializeInjector() throws Exception { .addBinding(VolumeEndpoint.class) .addBinding(BucketEndpoint.class) .addBinding(MetricsServiceProviderFactory.class) - .addBinding(ContainerHealthSchemaManagerV2.class) + .addBinding(ContainerHealthSchemaManager.class) .addBinding(UtilizationEndpoint.class) .addBinding(ReconUtils.class, reconUtilsMock) .addBinding(StorageContainerLocationProtocol.class, mockScmClient) @@ -309,11 +309,11 @@ private void initializeInjector() throws Exception { omTableInsightTask = new OmTableInsightTask(reconGlobalStatsManager, reconOMMetadataManager); - ContainerHealthSchemaManagerV2 containerHealthSchemaManagerV2 = - reconTestInjector.getInstance(ContainerHealthSchemaManagerV2.class); + ContainerHealthSchemaManager containerHealthSchemaManager = + reconTestInjector.getInstance(ContainerHealthSchemaManager.class); clusterStateEndpoint = new ClusterStateEndpoint(reconScm, reconGlobalStatsManager, - containerHealthSchemaManagerV2, mock(OzoneConfiguration.class)); + containerHealthSchemaManager, mock(OzoneConfiguration.class)); containerSizeCountTask = reconScm.getContainerSizeCountTask(); MetricsServiceProviderFactory metricsServiceProviderFactory = reconTestInjector.getInstance(MetricsServiceProviderFactory.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 cf271936dac1..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 @@ -32,7 +32,7 @@ import org.junit.jupiter.api.Test; /** - * Unit tests for ContainerHealthTaskV2 execution flow. + * Unit tests for ContainerHealthTask execution flow. */ public class TestContainerHealthTask { @@ -45,8 +45,8 @@ public void testRunTaskInvokesReconReplicationManagerProcessAll() mock(ReconStorageContainerManagerFacade.class); when(reconScm.getReplicationManager()).thenReturn(reconReplicationManager); - ContainerHealthTaskV2 task = - new ContainerHealthTaskV2( + ContainerHealthTask task = + new ContainerHealthTask( createTaskConfig(), createTaskStatusUpdaterManagerMock(), reconScm); @@ -66,8 +66,8 @@ public void testRunTaskPropagatesProcessAllFailure() throws Exception { org.mockito.Mockito.doThrow(expected).when(reconReplicationManager) .processAll(); - ContainerHealthTaskV2 task = - new ContainerHealthTaskV2( + ContainerHealthTask task = + new ContainerHealthTask( createTaskConfig(), createTaskStatusUpdaterManagerMock(), reconScm); @@ -87,7 +87,7 @@ private ReconTaskStatusUpdaterManager createTaskStatusUpdaterManagerMock() { ReconTaskStatusUpdaterManager manager = mock(ReconTaskStatusUpdaterManager.class); ReconTaskStatusUpdater updater = mock(ReconTaskStatusUpdater.class); - when(manager.getTaskStatusUpdater("ContainerHealthTaskV2")).thenReturn(updater); + when(manager.getTaskStatusUpdater("ContainerHealthTask")).thenReturn(updater); return manager; } } 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 index b1bddfc11951..9787eaf0a470 100644 --- 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 @@ -18,6 +18,7 @@ package org.apache.hadoop.ozone.recon.fsck; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -42,13 +43,14 @@ 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.ContainerHealthSchemaManagerV2; +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; @@ -71,7 +73,7 @@ */ public class TestReconReplicationManager extends AbstractReconSqlDBTest { - private ContainerHealthSchemaManagerV2 schemaManagerV2; + private ContainerHealthSchemaManager schemaManagerV2; private UnhealthyContainersDao dao; private ContainerManager containerManager; private ReconReplicationManager reconRM; @@ -83,7 +85,7 @@ public TestReconReplicationManager() { @BeforeEach public void setUp() throws Exception { dao = getDao(UnhealthyContainersDao.class); - schemaManagerV2 = new ContainerHealthSchemaManagerV2( + schemaManagerV2 = new ContainerHealthSchemaManager( getSchemaDefinition(ContainerSchemaDefinition.class), dao); containerManager = mock(ContainerManager.class); @@ -146,13 +148,13 @@ public void testProcessAllStoresEmptyMissingAndNegativeSizeRecords() emptyMissingContainerId, negativeSizeContainerId); reconRM.processAll(); - List emptyMissing = + List emptyMissing = schemaManagerV2.getUnhealthyContainers( UnHealthyContainerStates.EMPTY_MISSING, 0, 0, 100); assertEquals(1, emptyMissing.size()); assertEquals(emptyMissingContainerId, emptyMissing.get(0).getContainerId()); - List negativeSize = + List negativeSize = schemaManagerV2.getUnhealthyContainers( UnHealthyContainerStates.NEGATIVE_SIZE, 0, 0, 100); assertEquals(1, negativeSize.size()); @@ -218,6 +220,114 @@ public void testProcessAllStoresAllPrimaryV2States() throws Exception { 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 @@ -342,6 +452,22 @@ private ReconReplicationManager createStateInjectingReconRM( .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); + return true; + } + return false; + } + @Override protected boolean processContainer(ContainerInfo containerInfo, ReplicationQueue repQueue, ReplicationManagerReport report, @@ -368,4 +494,10 @@ private Set setOfMockReplicasWithChecksums(Long... checksums) } 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/persistence/TestUnhealthyContainersDerbyPerformance.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestUnhealthyContainersDerbyPerformance.java index 3c5d6f700018..1c810fde7c1a 100644 --- 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 @@ -38,8 +38,8 @@ 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.ContainerHealthSchemaManagerV2.UnhealthyContainerRecordV2; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2.UnhealthyContainersSummaryV2; +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; @@ -158,12 +158,12 @@ public class TestUnhealthyContainersDerbyPerformance { /** * Number of container IDs to pass per - * {@link ContainerHealthSchemaManagerV2#batchDeleteSCMStatesForContainers} + * {@link ContainerHealthSchemaManager#batchDeleteSCMStatesForContainers} * call in the delete test. * *

      {@code batchDeleteSCMStatesForContainers} now handles internal - * chunking at {@link ContainerHealthSchemaManagerV2#MAX_DELETE_CHUNK_SIZE} - * ({@value ContainerHealthSchemaManagerV2#MAX_DELETE_CHUNK_SIZE} IDs per + * chunking at {@link ContainerHealthSchemaManager#MAX_DELETE_CHUNK_SIZE} + * ({@value ContainerHealthSchemaManager#MAX_DELETE_CHUNK_SIZE} 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 @@ -209,7 +209,7 @@ public class TestUnhealthyContainersDerbyPerformance { // Infrastructure (shared for the life of this test class) // ----------------------------------------------------------------------- - private ContainerHealthSchemaManagerV2 schemaManager; + private ContainerHealthSchemaManager schemaManager; private UnhealthyContainersDao dao; private ContainerSchemaDefinition schemaDefinition; @@ -272,7 +272,7 @@ protected void configure() { dao = injector.getInstance(UnhealthyContainersDao.class); schemaDefinition = injector.getInstance(ContainerSchemaDefinition.class); - schemaManager = new ContainerHealthSchemaManagerV2(schemaDefinition, dao); + schemaManager = new ContainerHealthSchemaManager(schemaDefinition, dao); // ----- Insert 1 M records in small per-transaction chunks ----- // @@ -290,7 +290,7 @@ protected void configure() { 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); + List chunk = generateRecordsForRange(startId, endId, now); schemaManager.insertUnhealthyContainerRecords(chunk); } @@ -372,7 +372,7 @@ public void testCountByStatePerformanceUsesIndex() { // ----------------------------------------------------------------------- /** - * Runs the {@link ContainerHealthSchemaManagerV2#getUnhealthyContainersSummary()} + * 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. * @@ -385,7 +385,7 @@ public void testGroupBySummaryQueryPerformance() { LOG.info("--- Test 3: GROUP BY summary over {} rows ---", TOTAL_RECORDS); long start = System.nanoTime(); - List summary = + List summary = schemaManager.getUnhealthyContainersSummary(); long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); @@ -397,7 +397,7 @@ public void testGroupBySummaryQueryPerformance() { assertEquals(STATE_COUNT, summary.size(), "Summary must contain one entry per tested state"); - for (UnhealthyContainersSummaryV2 entry : summary) { + for (UnhealthyContainersSummary entry : summary) { assertEquals(CONTAINER_ID_RANGE, entry.getCount(), "Each state must have " + CONTAINER_ID_RANGE + " records in the summary"); } @@ -440,7 +440,7 @@ public void testPaginatedReadByStatePerformance() { long start = System.nanoTime(); while (true) { - List page = + List page = schemaManager.getUnhealthyContainers( targetState, minContainerId, 0, READ_PAGE_SIZE); @@ -448,7 +448,7 @@ public void testPaginatedReadByStatePerformance() { break; } - for (UnhealthyContainerRecordV2 rec : page) { + for (ContainerHealthSchemaManager.UnhealthyContainerRecord rec : page) { if (rec.getContainerId() <= lastContainerId) { orderedCorrectly = false; } @@ -506,7 +506,7 @@ public void testFullDatasetReadThroughputAllStates() { long minId = 0; while (true) { - List page = + List page = schemaManager.getUnhealthyContainers(state, minId, 0, READ_PAGE_SIZE); if (page.isEmpty()) { break; @@ -547,10 +547,10 @@ public void testFullDatasetReadThroughputAllStates() { /** * Deletes records for the first half of container IDs (1 – 100,000) across * all five states by passing the complete 100 K ID list in one call to - * {@link ContainerHealthSchemaManagerV2#batchDeleteSCMStatesForContainers}. + * {@link ContainerHealthSchemaManager#batchDeleteSCMStatesForContainers}. * *

      {@code batchDeleteSCMStatesForContainers} now handles internal - * chunking at {@link ContainerHealthSchemaManagerV2#MAX_DELETE_CHUNK_SIZE} + * chunking at {@link ContainerHealthSchemaManager#MAX_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 100 statements of @@ -569,12 +569,12 @@ public void testBatchDeletePerformanceHalfTheContainers() { int expectedDeleted = deleteCount * STATE_COUNT; // 500 000 rows int expectedRemaining = TOTAL_RECORDS - expectedDeleted; int internalChunks = (int) Math.ceil( - (double) deleteCount / ContainerHealthSchemaManagerV2.MAX_DELETE_CHUNK_SIZE); + (double) deleteCount / ContainerHealthSchemaManager.MAX_DELETE_CHUNK_SIZE); LOG.info("--- Test 6: Batch DELETE — {} IDs × {} states = {} rows " + "({} internal SQL statements of {} IDs) ---", deleteCount, STATE_COUNT, expectedDeleted, - internalChunks, ContainerHealthSchemaManagerV2.MAX_DELETE_CHUNK_SIZE); + internalChunks, ContainerHealthSchemaManager.MAX_DELETE_CHUNK_SIZE); long start = System.nanoTime(); @@ -656,10 +656,10 @@ public void testCountByStateAfterPartialDelete() { * @param timestamp epoch millis to use as {@code in_state_since} * @return list of {@code (endId - startId + 1) × STATE_COUNT} records */ - private List generateRecordsForRange( + private List generateRecordsForRange( int startId, int endId, long timestamp) { int size = (endId - startId + 1) * STATE_COUNT; - List records = new ArrayList<>(size); + List records = new ArrayList<>(size); for (int containerId = startId; containerId <= endId; containerId++) { for (UnHealthyContainerStates state : TESTED_STATES) { @@ -699,7 +699,7 @@ private List generateRecordsForRange( reason = "Unknown state"; } - records.add(new UnhealthyContainerRecordV2( + records.add(new ContainerHealthSchemaManager.UnhealthyContainerRecord( containerId, state.toString(), timestamp, diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java index b7ebdcba1af2..33e20413bfd6 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java @@ -58,7 +58,7 @@ import org.apache.hadoop.hdds.utils.db.DBStoreBuilder; import org.apache.hadoop.hdds.utils.db.Table; import org.apache.hadoop.ozone.recon.ReconUtils; -import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManagerV2; +import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager; import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager; import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider; import org.junit.jupiter.api.AfterEach; @@ -112,7 +112,7 @@ public void setUp(@TempDir File tempDir) throws Exception { ReconSCMDBDefinition.CONTAINERS.getTable(store), pipelineManager, getScmServiceProvider(), - mock(ContainerHealthSchemaManagerV2.class), + mock(ContainerHealthSchemaManager.class), mock(ReconContainerMetadataManager.class), scmhaManager, sequenceIdGen, 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..669ccc755bbf --- /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.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +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.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; + } +} From 1af2f3248a7402ee1245c98c6e5e8965fad72637 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 9 Mar 2026 21:49:27 +0530 Subject: [PATCH 39/43] HDDS-13891. Fixed review comments. --- .../apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java | 2 +- .../hadoop/ozone/recon/fsck/TestReconReplicationManager.java | 2 +- ...stUnhealthyContainersStateContainerIdIndexUpgradeAction.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 6b142a62458a..2737c34f3997 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -34,8 +34,8 @@ import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException; import org.apache.hadoop.hdds.scm.container.ContainerReplica; -import org.apache.hadoop.hdds.scm.container.replication.MonitoringReplicationQueue; import org.apache.hadoop.hdds.scm.container.replication.ContainerReplicaOp; +import org.apache.hadoop.hdds.scm.container.replication.MonitoringReplicationQueue; 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; 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 index 9787eaf0a470..ab756461773b 100644 --- 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 @@ -18,8 +18,8 @@ package org.apache.hadoop.ozone.recon.fsck; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java index 669ccc755bbf..b4cde1bb6738 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainersStateContainerIdIndexUpgradeAction.java @@ -19,10 +19,10 @@ 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 static org.jooq.impl.DSL.name; import java.sql.Connection; import java.sql.DatabaseMetaData; From e03690255569c3abc801a1f749027d4665215c3a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Tue, 10 Mar 2026 09:41:27 +0530 Subject: [PATCH 40/43] HDDS-13891. Fixed failed robot tetss. --- .../smoketest/recon/recon-taskstatus.robot | 2 +- .../hadoop/ozone/recon/TestReconTasks.java | 64 +++++++++---------- ...Node.java => TestReconTasksMultiNode.java} | 20 +++--- .../ozone/recon/api/ContainerEndpoint.java | 6 +- .../ozone/recon/fsck/ContainerHealthTask.java | 2 +- .../ContainerHealthSchemaManager.java | 16 ++--- .../recon/scm/ReconContainerManager.java | 2 +- .../ReconStorageContainerManagerFacade.java | 10 +-- 8 files changed, 61 insertions(+), 61 deletions(-) rename hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/{TestReconTasksV2MultiNode.java => TestReconTasksMultiNode.java} (91%) diff --git a/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot b/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot index 49a0df2ffe6b..d3473b9aff0e 100644 --- a/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot +++ b/hadoop-ozone/dist/src/main/smoketest/recon/recon-taskstatus.robot @@ -28,7 +28,7 @@ Suite Setup Get Security Enabled From Config ${BASE_URL} http://recon:9888 ${TASK_STATUS_ENDPOINT} ${BASE_URL}/api/v1/task/status ${TRIGGER_SYNC_ENDPOINT} ${BASE_URL}/api/v1/triggerdbsync/om -${TASK_NAME_1} ContainerHealthTaskV2 +${TASK_NAME_1} ContainerHealthTask ${TASK_NAME_2} OmDeltaRequest ${BUCKET} testbucket ${VOLUME} testvolume diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java index 21f25edd4486..9ad018c0ed60 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasks.java @@ -70,22 +70,22 @@ * table except the dead {@code ALL_REPLICAS_BAD} state):

      *
        *
      • {@code UNDER_REPLICATED}: RF3 CLOSED container loses one replica (node down) - * → {@link #testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure}
      • + * → {@link #testContainerHealthTaskDetectsUnderReplicatedAfterNodeFailure} *
      • {@code EMPTY_MISSING}: RF1 OPEN container's only replica lost (node down, * container has no OM-tracked keys) → - * {@link #testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost}
      • + * {@link #testContainerHealthTaskDetectsEmptyMissingWhenAllReplicasLost} *
      • {@code MISSING}: RF1 CLOSED container with OM-tracked keys loses its only * replica (metadata manipulation) → - * {@link #testContainerHealthTaskV2DetectsMissingForContainerWithKeys}
      • + * {@link #testContainerHealthTaskDetectsMissingForContainerWithKeys} *
      • {@code OVER_REPLICATED}: RF1 CLOSED container gains a phantom extra replica * (metadata injection) → - * {@link #testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize}
      • + * {@link #testContainerHealthTaskDetectsOverReplicatedAndNegativeSize} *
      • {@code NEGATIVE_SIZE}: Co-detected alongside {@code OVER_REPLICATED} when * the container's {@code usedBytes} is negative → - * {@link #testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize}
      • + * {@link #testContainerHealthTaskDetectsOverReplicatedAndNegativeSize} *
      • {@code REPLICA_MISMATCH}: RF3 CLOSED container where one replica reports a * different data checksum (metadata injection) → - * {@link #testContainerHealthTaskV2DetectsReplicaMismatch}
      • + * {@link #testContainerHealthTaskDetectsReplicaMismatch} *
      * *

      States NOT covered:

      @@ -209,7 +209,7 @@ public void testSyncSCMContainerInfo() throws Exception { * MISSING/EMPTY_MISSING, regardless of key count.

      */ @Test - public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() + public void testContainerHealthTaskDetectsUnderReplicatedAfterNodeFailure() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) @@ -285,15 +285,15 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); List underReplicated = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); List missing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); List emptyMissing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); return containsContainerId(underReplicated, containerID) @@ -307,7 +307,7 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); List underReplicated = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); return !containsContainerId(underReplicated, containerID); @@ -315,14 +315,14 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() // After recovery: our container must not appear in any unhealthy state. List missingAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); assertFalse(containsContainerId(missingAfterRecovery, containerID), "Container should not be MISSING after node recovery"); List emptyMissingAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); assertFalse(containsContainerId(emptyMissingAfterRecovery, containerID), @@ -351,7 +351,7 @@ public void testContainerHealthTaskV2DetectsUnderReplicatedAfterNodeFailure() * replicas for the health scan to classify as EMPTY_MISSING.

      */ @Test - public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() + public void testContainerHealthTaskDetectsEmptyMissingWhenAllReplicasLost() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) @@ -387,15 +387,15 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); List emptyMissing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); List missing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); List underReplicated = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); // EMPTY_MISSING must be set; MISSING and UNDER_REPLICATED must NOT be set @@ -412,7 +412,7 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() LambdaTestUtils.await(STATE_TRANSITION_TIMEOUT_MS, POLL_INTERVAL_MS, () -> { forceContainerHealthScan(reconScm); List emptyMissing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); return !containsContainerId(emptyMissing, containerID); @@ -420,14 +420,14 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() // After recovery: our container must not appear in any unhealthy state. List missingAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); assertFalse(containsContainerId(missingAfterRecovery, containerID), "Container should not be MISSING after node recovery"); List underReplicatedAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); assertFalse(containsContainerId(underReplicatedAfterRecovery, containerID), @@ -453,7 +453,7 @@ public void testContainerHealthTaskV2DetectsEmptyMissingWhenAllReplicasLost() * by re-adding the replica and running another health scan.

      */ @Test - public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() + public void testContainerHealthTaskDetectsMissingForContainerWithKeys() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) @@ -513,11 +513,11 @@ public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() forceContainerHealthScan(reconScm); List missing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); List emptyMissing = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.EMPTY_MISSING, 0L, 0L, 1000); @@ -531,7 +531,7 @@ public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() forceContainerHealthScan(reconScm); List missingAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.MISSING, 0L, 0L, 1000); assertFalse(containsContainerId(missingAfterRecovery, containerID), @@ -558,7 +558,7 @@ public void testContainerHealthTaskV2DetectsMissingForContainerWithKeys() * {@code usedBytes} to a non-negative value.

      */ @Test - public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() + public void testContainerHealthTaskDetectsOverReplicatedAndNegativeSize() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) @@ -628,11 +628,11 @@ public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() forceContainerHealthScan(reconScm); List overReplicated = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, 0L, 0L, 1000); List negativeSize = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.NEGATIVE_SIZE, 0L, 0L, 1000); @@ -647,11 +647,11 @@ public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() forceContainerHealthScan(reconScm); List overReplicatedAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, 0L, 0L, 1000); List negativeSizeAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.NEGATIVE_SIZE, 0L, 0L, 1000); @@ -681,7 +681,7 @@ public void testContainerHealthTaskV2DetectsOverReplicatedAndNegativeSize() * were excluded and could linger indefinitely after a mismatch was resolved).

      */ @Test - public void testContainerHealthTaskV2DetectsReplicaMismatch() throws Exception { + public void testContainerHealthTaskDetectsReplicaMismatch() throws Exception { ReconStorageContainerManagerFacade reconScm = (ReconStorageContainerManagerFacade) recon.getReconServer().getReconStorageContainerManager(); @@ -741,7 +741,7 @@ public void testContainerHealthTaskV2DetectsReplicaMismatch() throws Exception { forceContainerHealthScan(reconScm); List replicaMismatch = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.REPLICA_MISMATCH, 0L, 0L, 1000); assertTrue(containsContainerId(replicaMismatch, containerID), @@ -752,7 +752,7 @@ public void testContainerHealthTaskV2DetectsReplicaMismatch() throws Exception { forceContainerHealthScan(reconScm); List replicaMismatchAfterRecovery = - reconCm.getContainerSchemaManagerV2().getUnhealthyContainers( + reconCm.getContainerSchemaManager().getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.REPLICA_MISMATCH, 0L, 0L, 1000); assertFalse(containsContainerId(replicaMismatchAfterRecovery, containerID), diff --git a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksMultiNode.java similarity index 91% rename from hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java rename to hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksMultiNode.java index 44540a9b1761..8276ff805f2f 100644 --- a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksV2MultiNode.java +++ b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconTasksMultiNode.java @@ -45,7 +45,7 @@ * different cluster configurations (3 datanodes) and would conflict * with the @BeforeEach/@AfterEach setup in that class. */ -public class TestReconTasksV2MultiNode { +public class TestReconTasksMultiNode { private static MiniOzoneCluster cluster; private static ReconContainerManager reconContainerManager; @@ -81,7 +81,7 @@ public static void setupCluster() throws Exception { @BeforeEach public void cleanupBeforeEach() throws Exception { // Ensure each test starts from a clean unhealthy-container table. - reconContainerManager.getContainerSchemaManagerV2().clearAllUnhealthyContainerRecords(); + reconContainerManager.getContainerSchemaManager().clearAllUnhealthyContainerRecords(); // Ensure Recon has initialized pipeline state before assertions. LambdaTestUtils.await(60000, 300, () -> (!reconPipelineManager.getPipelines().isEmpty())); @@ -111,7 +111,7 @@ public static void shutdownCluster() { * 4. Datanodes are decommissioned * * The detection logic is tested end-to-end in: - * - TestReconTasks.testContainerHealthTaskV2WithSCMSync() - which proves + * - TestReconTasks.testContainerHealthTaskWithSCMSync() - which proves * Recon's RM logic works for MISSING containers (similar detection logic) * * Full end-to-end test for UNDER_REPLICATED would require: @@ -120,16 +120,16 @@ public static void shutdownCluster() { * 3. Shut down 1 datanode * 4. Wait for SCM to mark datanode as dead (stale/dead intervals) * 5. Wait for ContainerHealthTask to run (task interval) - * 6. Verify UNDER_REPLICATED state in V2 table with correct replica counts + * 6. Verify UNDER_REPLICATED state in table with correct replica counts * 7. Restart datanode and verify container becomes healthy */ @Test - public void testContainerHealthTaskV2UnderReplicated() throws Exception { + public void testContainerHealthTaskUnderReplicated() throws Exception { cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.THREE, 60000); // Verify the query mechanism for UNDER_REPLICATED state works List underReplicatedContainers = - reconContainerManager.getContainerSchemaManagerV2() + reconContainerManager.getContainerSchemaManager() .getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.UNDER_REPLICATED, 0L, 0L, 1000); @@ -145,14 +145,14 @@ public void testContainerHealthTaskV2UnderReplicated() throws Exception { * 2. Allocate a container with replication factor 1 * 3. Write data to the container * 4. Manually add the container to additional datanodes to create over-replication - * 5. Verify ContainerHealthTask detects OVER_REPLICATED state in V2 table + * 5. Verify ContainerHealthTask detects OVER_REPLICATED state in UNHEALTHY_CONTAINERS table * * Note: Creating over-replication scenarios is complex in integration tests * as it requires manipulating the container replica state artificially. * This test demonstrates the detection capability when over-replication occurs. */ @Test - public void testContainerHealthTaskV2OverReplicated() throws Exception { + public void testContainerHealthTaskOverReplicated() throws Exception { cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.ONE, 60000); // Note: Creating over-replication in integration tests is challenging @@ -163,12 +163,12 @@ public void testContainerHealthTaskV2OverReplicated() throws Exception { // 3. Manual intervention or bugs cause duplicate replicas // // For now, this test verifies the detection mechanism exists. - // If over-replication is detected in the future, the V2 table + // If over-replication is detected in the future, the UNHEALTHY_CONTAINERS table // should contain the record with proper replica counts. // For now, just verify that the query mechanism works List overReplicatedContainers = - reconContainerManager.getContainerSchemaManagerV2() + reconContainerManager.getContainerSchemaManager() .getUnhealthyContainers( ContainerSchemaDefinition.UnHealthyContainerStates.OVER_REPLICATED, 0L, 0L, 1000); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index 1a20e02f7efc..ea7588d778af 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -398,7 +398,7 @@ public Response getUnhealthyContainers( } /** - * V2 implementation - reads from UNHEALTHY_CONTAINERS table. + * New implementation - reads from UNHEALTHY_CONTAINERS table. */ private Response getUnhealthyContainersFromSchema( String state, @@ -416,14 +416,14 @@ private Response getUnhealthyContainersFromSchema( v2State = ContainerSchemaDefinition.UnHealthyContainerStates.valueOf(state); } - // Get summary from V2 table and convert to V1 format + // Get summary from UNHEALTHY_CONTAINERS table and convert to V1 format List v2Summary = containerHealthSchemaManager.getUnhealthyContainersSummary(); for (ContainerHealthSchemaManager.UnhealthyContainersSummary s : v2Summary) { summary.add(new UnhealthyContainersSummary(s.getContainerState(), s.getCount())); } - // Get containers from V2 table + // Get containers from UNHEALTHY_CONTAINERS table List v2Containers = containerHealthSchemaManager.getUnhealthyContainers(v2State, minContainerId, maxContainerId, limit); diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java index 7b761068d748..d4a1af51ad8e 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ContainerHealthTask.java @@ -28,7 +28,7 @@ import org.slf4j.LoggerFactory; /** - * V2 implementation of Container Health Task using Local ReplicationManager. + * New implementation of Container Health Task using Local ReplicationManager. * *

      Solution:

      *
        diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java index d3c9c7d7195d..d6e13fab08f4 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java @@ -82,7 +82,7 @@ public ContainerHealthSchemaManager( } /** - * Insert or update unhealthy container records in V2 table using TRUE batch insert. + * Insert or update unhealthy container records in UNHEALTHY_CONTAINERS table using TRUE batch insert. * Uses JOOQ's batch API for optimal performance (single SQL statement for all records). * Falls back to individual insert-or-update if batch insert fails (e.g., duplicate keys). */ @@ -302,7 +302,7 @@ public Map getExistingInStateSinceByContainerIds( } /** - * Get summary of unhealthy containers grouped by state from V2 table. + * Get summary of unhealthy containers grouped by state from UNHEALTHY_CONTAINERS table. */ public List getUnhealthyContainersSummary() { DSLContext dslContext = containerSchemaDefinition.getDSLContext(); @@ -316,13 +316,13 @@ public List getUnhealthyContainersSummary() { .groupBy(UNHEALTHY_CONTAINERS.CONTAINER_STATE) .fetchInto(UnhealthyContainersSummary.class); } catch (Exception e) { - LOG.error("Failed to get summary from V2 table", e); + LOG.error("Failed to get summary from UNHEALTHY_CONTAINERS table", e); return result; } } /** - * Get unhealthy containers from V2 table. + * Get unhealthy containers from UNHEALTHY_CONTAINERS table. */ public List getUnhealthyContainers( UnHealthyContainerStates state, long minContainerId, long maxContainerId, int limit) { @@ -382,22 +382,22 @@ public List getUnhealthyContainers( record.getReason())) .collect(Collectors.toList()); } catch (Exception e) { - LOG.error("Failed to query V2 table", e); + LOG.error("Failed to query UNHEALTHY_CONTAINERS table", e); return new ArrayList<>(); } } /** - * Clear all records from V2 table (for testing). + * Clear all records from UNHEALTHY_CONTAINERS table (for testing). */ @VisibleForTesting public void clearAllUnhealthyContainerRecords() { DSLContext dslContext = containerSchemaDefinition.getDSLContext(); try { dslContext.deleteFrom(UNHEALTHY_CONTAINERS).execute(); - LOG.info("Cleared all V2 unhealthy container records"); + LOG.info("Cleared all UNHEALTHY_CONTAINERS table's unhealthy container records"); } catch (Exception e) { - LOG.error("Failed to clear V2 unhealthy container records", e); + LOG.error("Failed to clear UNHEALTHY_CONTAINERS table's unhealthy container records", e); } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java index 42564658ba8a..586aad5fd68f 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java @@ -341,7 +341,7 @@ public void removeContainerReplica(ContainerID containerID, } @VisibleForTesting - public ContainerHealthSchemaManager getContainerSchemaManagerV2() { + public ContainerHealthSchemaManager getContainerSchemaManager() { return containerHealthSchemaManager; } 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 79ac3ad95bcd..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 @@ -155,7 +155,7 @@ public class ReconStorageContainerManagerFacade private final SCMNodeDetails reconNodeDetails; private final SCMHAManager scmhaManager; private final SequenceIdGenerator sequenceIdGen; - private final ReconScmTask containerHealthTaskV2; + private final ReconScmTask containerHealthTask; private final DataSource dataSource; private final ContainerHealthSchemaManager containerHealthSchemaManager; @@ -272,7 +272,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, // Create ContainerHealthTask (always runs, writes to UNHEALTHY_CONTAINERS) LOG.info("Creating ContainerHealthTask"); - containerHealthTaskV2 = new ContainerHealthTask( + containerHealthTask = new ContainerHealthTask( reconTaskConfig, taskStatusUpdaterManager, this // ReconStorageContainerManagerFacade - provides access to ReconReplicationManager @@ -312,7 +312,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, new ReconStaleNodeHandler(nodeManager, pipelineManager, pipelineSyncTask); DeadNodeHandler deadNodeHandler = new ReconDeadNodeHandler(nodeManager, pipelineManager, containerManager, scmServiceProvider, - containerHealthTaskV2, pipelineSyncTask); + containerHealthTask, pipelineSyncTask); ContainerReportHandler containerReportHandler = new ReconContainerReportHandler(nodeManager, containerManager); @@ -380,7 +380,7 @@ public ReconStorageContainerManagerFacade(OzoneConfiguration conf, eventQueue.addHandler(SCMEvents.CLOSE_CONTAINER, closeContainerHandler); eventQueue.addHandler(SCMEvents.NEW_NODE, newNodeHandler); reconScmTasks.add(pipelineSyncTask); - reconScmTasks.add(containerHealthTaskV2); + reconScmTasks.add(containerHealthTask); reconScmTasks.add(containerSizeCountTask); reconSafeModeMgrTask = new ReconSafeModeMgrTask( containerManager, nodeManager, safeModeManager, @@ -761,7 +761,7 @@ public ContainerSizeCountTask getContainerSizeCountTask() { @VisibleForTesting public ReconScmTask getContainerHealthTask() { - return containerHealthTaskV2; + return containerHealthTask; } @VisibleForTesting From 72a1b67999eac4724d75b3cd6b450ecfe8f240f7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 16 Mar 2026 17:58:56 +0530 Subject: [PATCH 41/43] HDDS-13891. Fixed review comments. --- .../recon/fsck/ReconReplicationManager.java | 47 ++++++------------- .../ContainerHealthSchemaManager.java | 40 ++++++++++++++++ 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 2737c34f3997..0557ad68440e 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -386,33 +386,32 @@ private void storeHealthStatesToDatabase( ContainerID containerId = container.containerID(); try { if (missingContainers.contains(containerId)) { - handleMissingContainer(containerId, currentTime, - existingInStateSinceByContainerAndState, recordsToInsert, chunkStats); + handleMissingContainer(containerId, currentTime, recordsToInsert, + chunkStats); } if (underReplicatedContainers.contains(containerId)) { chunkStats.incrementUnderRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.UNDER_REPLICATED, - existingInStateSinceByContainerAndState, recordsToInsert, + recordsToInsert, negativeSizeRecorded, chunkStats); } if (overReplicatedContainers.contains(containerId)) { chunkStats.incrementOverRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.OVER_REPLICATED, - existingInStateSinceByContainerAndState, recordsToInsert, + recordsToInsert, negativeSizeRecorded, chunkStats); } if (misReplicatedContainers.contains(containerId)) { chunkStats.incrementMisRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.MIS_REPLICATED, - existingInStateSinceByContainerAndState, recordsToInsert, + recordsToInsert, negativeSizeRecorded, chunkStats); } if (replicaMismatchContainers.contains(containerId)) { - processReplicaMismatchContainer(containerId, currentTime, - existingInStateSinceByContainerAndState, recordsToInsert); + processReplicaMismatchContainer(containerId, currentTime, recordsToInsert); totalReplicaMismatchCount++; } } catch (ContainerNotFoundException e) { @@ -421,6 +420,8 @@ private void storeHealthStatesToDatabase( } } + recordsToInsert = healthSchemaManager.applyExistingInStateSince( + recordsToInsert, chunkContainerIds); totalStats.add(chunkStats); persistUnhealthyRecords(existingContainerIdsToDelete, recordsToInsert); } @@ -436,7 +437,6 @@ private void storeHealthStatesToDatabase( private void handleMissingContainer( ContainerID containerId, long currentTime, - Map existingInStateSinceByContainerAndState, List recordsToInsert, ProcessingStats stats) throws ContainerNotFoundException { ContainerInfo container = containerManager.getContainer(containerId); @@ -445,9 +445,7 @@ private void handleMissingContainer( stats.incrementEmptyMissingCount(); recordsToInsert.add(createRecord(container, UnHealthyContainerStates.EMPTY_MISSING, - resolveInStateSince(container.getContainerID(), - UnHealthyContainerStates.EMPTY_MISSING, currentTime, - existingInStateSinceByContainerAndState), + currentTime, expected, 0, "Container has no replicas and no keys")); return; @@ -456,9 +454,7 @@ private void handleMissingContainer( stats.incrementMissingCount(); recordsToInsert.add(createRecord(container, UnHealthyContainerStates.MISSING, - resolveInStateSince(container.getContainerID(), - UnHealthyContainerStates.MISSING, currentTime, - existingInStateSinceByContainerAndState), + currentTime, expected, 0, "No replicas available")); } @@ -467,7 +463,6 @@ private void handleReplicaStateContainer( ContainerID containerId, long currentTime, UnHealthyContainerStates targetState, - Map existingInStateSinceByContainerAndState, List recordsToInsert, Set negativeSizeRecorded, ProcessingStats stats) throws ContainerNotFoundException { @@ -476,17 +471,15 @@ private void handleReplicaStateContainer( int expected = container.getReplicationConfig().getRequiredNodes(); int actual = replicas.size(); recordsToInsert.add(createRecord(container, targetState, - resolveInStateSince(container.getContainerID(), targetState, - currentTime, existingInStateSinceByContainerAndState), + currentTime, expected, actual, reasonForState(targetState))); addNegativeSizeRecordIfNeeded(container, currentTime, actual, recordsToInsert, - existingInStateSinceByContainerAndState, negativeSizeRecorded, stats); + negativeSizeRecorded, stats); } private void processReplicaMismatchContainer( ContainerID containerId, long currentTime, - Map existingInStateSinceByContainerAndState, List recordsToInsert) throws ContainerNotFoundException { ContainerInfo container = containerManager.getContainer(containerId); Set replicas = containerManager.getContainerReplicas(containerId); @@ -494,9 +487,7 @@ private void processReplicaMismatchContainer( int actual = replicas.size(); recordsToInsert.add(createRecord(container, UnHealthyContainerStates.REPLICA_MISMATCH, - resolveInStateSince(container.getContainerID(), - UnHealthyContainerStates.REPLICA_MISMATCH, currentTime, - existingInStateSinceByContainerAndState), + currentTime, expected, actual, "Data checksum mismatch across replicas")); } @@ -532,7 +523,6 @@ private void addNegativeSizeRecordIfNeeded( long currentTime, int actualReplicaCount, List recordsToInsert, - Map existingInStateSinceByContainerAndState, Set negativeSizeRecorded, ProcessingStats stats) { if (isNegativeSize(container) @@ -540,22 +530,13 @@ private void addNegativeSizeRecordIfNeeded( int expected = container.getReplicationConfig().getRequiredNodes(); recordsToInsert.add(createRecord(container, UnHealthyContainerStates.NEGATIVE_SIZE, - resolveInStateSince(container.getContainerID(), - UnHealthyContainerStates.NEGATIVE_SIZE, currentTime, - existingInStateSinceByContainerAndState), + currentTime, expected, actualReplicaCount, "Container reports negative usedBytes")); stats.incrementNegativeSizeCount(); } } - private long resolveInStateSince(long containerId, UnHealthyContainerStates state, - long currentTime, Map existingInStateSinceByContainerAndState) { - Long inStateSince = existingInStateSinceByContainerAndState.get( - new ContainerStateKey(containerId, state.toString())); - return inStateSince == null ? currentTime : inStateSince; - } - private String reasonForState(UnHealthyContainerStates state) { switch (state) { case UNDER_REPLICATED: diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java index d6e13fab08f4..73a03bee1814 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java @@ -301,6 +301,46 @@ public Map getExistingInStateSinceByContainerIds( 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. */ From 775e282016856d8b4cf1fb97ddfe9517ecb7d153 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Mon, 23 Mar 2026 19:31:28 +0530 Subject: [PATCH 42/43] HDDS-13891. Fixed review comments. --- .../ozone/recon/api/ContainerEndpoint.java | 15 +- .../recon/fsck/ReconReplicationManager.java | 67 ++----- .../fsck/ReconReplicationManagerReport.java | 102 +--------- .../ContainerHealthSchemaManager.java | 64 +----- .../fsck/TestReconReplicationManager.java | 4 +- ...stUnhealthyContainersDerbyPerformance.java | 186 +++++++++++------- 6 files changed, 159 insertions(+), 279 deletions(-) diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java index ea7588d778af..4cf6ca85f6f7 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/ContainerEndpoint.java @@ -409,25 +409,24 @@ private Response getUnhealthyContainersFromSchema( List summary = new ArrayList<>(); try { - ContainerSchemaDefinition.UnHealthyContainerStates v2State = null; + ContainerSchemaDefinition.UnHealthyContainerStates containerState = null; if (state != null) { - // Convert V1 state string to V2 enum - v2State = ContainerSchemaDefinition.UnHealthyContainerStates.valueOf(state); + containerState = ContainerSchemaDefinition.UnHealthyContainerStates.valueOf(state); } // Get summary from UNHEALTHY_CONTAINERS table and convert to V1 format - List v2Summary = + List unhealthyContainersSummary = containerHealthSchemaManager.getUnhealthyContainersSummary(); - for (ContainerHealthSchemaManager.UnhealthyContainersSummary s : v2Summary) { + for (ContainerHealthSchemaManager.UnhealthyContainersSummary s : unhealthyContainersSummary) { summary.add(new UnhealthyContainersSummary(s.getContainerState(), s.getCount())); } // Get containers from UNHEALTHY_CONTAINERS table - List v2Containers = - containerHealthSchemaManager.getUnhealthyContainers(v2State, minContainerId, maxContainerId, limit); + List unhealthyContainers = + containerHealthSchemaManager.getUnhealthyContainers(containerState, minContainerId, maxContainerId, limit); - unhealthyMeta = v2Containers.stream() + unhealthyMeta = unhealthyContainers.stream() .map(this::toUnhealthyMetadata) .collect(Collectors.toList()); } catch (UncheckedIOException ex) { diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java index 0557ad68440e..af52521465ba 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManager.java @@ -298,6 +298,7 @@ public synchronized void processAll() { final List containers = containerManager.getContainers(); LOG.info("Processing {} containers", containers.size()); + final int logEvery = Math.max(1, containers.size() / 100); // Process each container (reuses inherited processContainer and health check chain) int processedCount = 0; @@ -320,7 +321,7 @@ public synchronized void processAll() { processedCount++; - if (processedCount % 10000 == 0) { + if (processedCount % logEvery == 0 || processedCount == containers.size()) { LOG.info("Processed {}/{} containers", processedCount, containers.size()); } } catch (ContainerNotFoundException e) { @@ -351,21 +352,6 @@ private void storeHealthStatesToDatabase( long currentTime = System.currentTimeMillis(); ProcessingStats totalStats = new ProcessingStats(); int totalReplicaMismatchCount = 0; - Set missingContainers = unionAsIdSet(report, - ContainerHealthState.MISSING, - ContainerHealthState.QUASI_CLOSED_STUCK_MISSING, - ContainerHealthState.MISSING_UNDER_REPLICATED); - Set underReplicatedContainers = unionAsIdSet(report, - ContainerHealthState.UNDER_REPLICATED, - ContainerHealthState.UNHEALTHY_UNDER_REPLICATED, - ContainerHealthState.QUASI_CLOSED_STUCK_UNDER_REPLICATED, - ContainerHealthState.MISSING_UNDER_REPLICATED); - Set overReplicatedContainers = unionAsIdSet(report, - ContainerHealthState.OVER_REPLICATED, - ContainerHealthState.UNHEALTHY_OVER_REPLICATED, - ContainerHealthState.QUASI_CLOSED_STUCK_OVER_REPLICATED); - Set misReplicatedContainers = - asIdSet(report, ContainerHealthState.MIS_REPLICATED); logUnmappedScmStates(report); Set replicaMismatchContainers = new HashSet<>(report.getReplicaMismatchContainers()); @@ -385,25 +371,33 @@ private void storeHealthStatesToDatabase( ContainerInfo container = allContainers.get(i); ContainerID containerId = container.containerID(); try { - if (missingContainers.contains(containerId)) { + ContainerHealthState healthState = container.getHealthState(); + if (healthState == ContainerHealthState.MISSING + || healthState == ContainerHealthState.QUASI_CLOSED_STUCK_MISSING + || healthState == ContainerHealthState.MISSING_UNDER_REPLICATED) { handleMissingContainer(containerId, currentTime, recordsToInsert, chunkStats); } - if (underReplicatedContainers.contains(containerId)) { + if (healthState == ContainerHealthState.UNDER_REPLICATED + || healthState == ContainerHealthState.UNHEALTHY_UNDER_REPLICATED + || healthState == ContainerHealthState.QUASI_CLOSED_STUCK_UNDER_REPLICATED + || healthState == ContainerHealthState.MISSING_UNDER_REPLICATED) { chunkStats.incrementUnderRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.UNDER_REPLICATED, recordsToInsert, negativeSizeRecorded, chunkStats); } - if (overReplicatedContainers.contains(containerId)) { + if (healthState == ContainerHealthState.OVER_REPLICATED + || healthState == ContainerHealthState.UNHEALTHY_OVER_REPLICATED + || healthState == ContainerHealthState.QUASI_CLOSED_STUCK_OVER_REPLICATED) { chunkStats.incrementOverRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.OVER_REPLICATED, recordsToInsert, negativeSizeRecorded, chunkStats); } - if (misReplicatedContainers.contains(containerId)) { + if (healthState == ContainerHealthState.MIS_REPLICATED) { chunkStats.incrementMisRepCount(); handleReplicaStateContainer(containerId, currentTime, UnHealthyContainerStates.MIS_REPLICATED, @@ -550,40 +544,13 @@ private String reasonForState(UnHealthyContainerStates state) { } } - private Set asIdSet(ReconReplicationManagerReport report, - ContainerHealthState state) { - List containers = report.getAllContainers(state); - if (containers.isEmpty()) { - return Collections.emptySet(); - } - return new HashSet<>(containers); - } - - private Set unionAsIdSet(ReconReplicationManagerReport report, - ContainerHealthState... states) { - Set result = null; - for (ContainerHealthState state : states) { - List containers = report.getAllContainers(state); - if (containers.isEmpty()) { - continue; - } - if (result == null) { - result = new HashSet<>(); - } - result.addAll(containers); - } - return result == null ? Collections.emptySet() : result; - } - private void logUnmappedScmStates(ReconReplicationManagerReport report) { - for (Map.Entry> entry : - report.getAllContainersByState().entrySet()) { - ContainerHealthState state = entry.getKey(); + for (ContainerHealthState state : ContainerHealthState.values()) { if (isMappedScmState(state)) { continue; } - int count = entry.getValue().size(); - if (count > 0) { + long count = report.getStat(state); + if (count > 0L) { LOG.warn("SCM state {} has {} containers but is not mapped to " + "UNHEALTHY_CONTAINERS allowed states; skipping persistence " + "for this state in this run", diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java index 1c235703a195..c80d2b81e6b6 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/fsck/ReconReplicationManagerReport.java @@ -19,106 +19,31 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -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.ReplicationManagerReport; /** - * Extended ReplicationManagerReport that captures ALL container health states, - * not just the first 100 samples per state. + * Recon-specific report extension. * - *

        SCM's standard ReplicationManagerReport uses sampling (SAMPLE_LIMIT = 100) - * to limit memory usage. This is appropriate for SCM which only needs samples - * for debugging/UI display.

        - * - *

        Recon, however, needs to track per-container health states for ALL containers - * to populate its UNHEALTHY_CONTAINERS table. This extended report removes - * the sampling limitation while maintaining backward compatibility by still - * calling the parent's incrementAndSample() method.

        + *

        Recon persists container health using each container's final + * {@code ContainerInfo#healthState}. This report keeps aggregate counters from + * the base class and tracks Recon-only {@code REPLICA_MISMATCH} containers.

        * *

        REPLICA_MISMATCH Handling: Since SCM's HealthState enum doesn't include * REPLICA_MISMATCH (it's a Recon-specific check for data checksum mismatches), * we track it separately in replicaMismatchContainers.

        - * - *

        Memory Impact: For a cluster with 100K containers and 5% unhealthy rate, - * this adds approximately 620KB of memory during report generation (5K containers - * × 124 bytes per container). Even in worst case (100% unhealthy), memory usage - * is only ~14MB, which is negligible for Recon.

        */ public class ReconReplicationManagerReport extends ReplicationManagerReport { - // Captures ALL containers per health state (no SAMPLE_LIMIT restriction) - private final Map> allContainersByState = - new HashMap<>(); - // Captures containers with REPLICA_MISMATCH (Recon-specific, not in SCM's HealthState) private final List replicaMismatchContainers = new ArrayList<>(); public ReconReplicationManagerReport() { - // Recon keeps a full per-state list in allContainersByState below. - // Disable base sampling map to avoid duplicate tracking. + // Disable base sampling list allocation; counters are still maintained. super(0); } - /** - * Override to capture ALL containers, not just first 100 samples. - * Still calls parent method to maintain aggregate counts and samples - * for backward compatibility. - * - * @param stat The health state to increment - * @param container The container ID to record - */ - @Override - public void incrementAndSample(ContainerHealthState stat, ContainerInfo container) { - // Call parent to maintain aggregate counts. - super.incrementAndSample(stat, container); - - // Capture ALL containers for Recon (no SAMPLE_LIMIT restriction) - allContainersByState - .computeIfAbsent(stat, k -> new ArrayList<>()) - .add(container.containerID()); - } - - /** - * Get ALL containers with the specified health state. - * Unlike getSample() which returns max 100 containers, this returns - * all containers that were recorded for the given state. - * - * @param stat The health state to query - * @return List of all container IDs with the specified health state, - * or empty list if none - */ - public List getAllContainers(ContainerHealthState stat) { - return allContainersByState.getOrDefault(stat, Collections.emptyList()); - } - - /** - * Get all captured containers grouped by health state. - * This provides a complete view of all unhealthy containers in the cluster. - * - * @return Immutable map of HealthState to list of container IDs - */ - public Map> getAllContainersByState() { - return Collections.unmodifiableMap(allContainersByState); - } - - /** - * Get count of ALL captured containers for a health state. - * This may differ from getStat() if containers were added after the - * initial increment. - * - * @param stat The health state to query - * @return Number of containers captured for this state - */ - public int getAllContainersCount(ContainerHealthState stat) { - return allContainersByState.getOrDefault(stat, Collections.emptyList()).size(); - } - /** * Add a container to the REPLICA_MISMATCH list. * This is a Recon-specific health state not tracked by SCM. @@ -138,21 +63,4 @@ public List getReplicaMismatchContainers() { return Collections.unmodifiableList(replicaMismatchContainers); } - /** - * Get count of containers with REPLICA_MISMATCH. - * - * @return Number of containers with replica checksum mismatches - */ - public int getReplicaMismatchCount() { - return replicaMismatchContainers.size(); - } - - /** - * Iterate through all unhealthy containers captured from SCM health states. - */ - public void forEachContainerByState( - BiConsumer consumer) { - allContainersByState.forEach( - (state, containers) -> containers.forEach(container -> consumer.accept(state, container))); - } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java index 73a03bee1814..ac1e91350cc6 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/persistence/ContainerHealthSchemaManager.java @@ -24,7 +24,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; -import java.sql.Connection; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -35,15 +34,12 @@ import java.util.stream.Stream; 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.apache.ozone.recon.schema.generated.tables.pojos.UnhealthyContainers; import org.apache.ozone.recon.schema.generated.tables.records.UnhealthyContainersRecord; import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.OrderField; import org.jooq.Record; import org.jooq.SelectQuery; -import org.jooq.exception.DataAccessException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,22 +66,24 @@ public class ContainerHealthSchemaManager { */ static final int MAX_DELETE_CHUNK_SIZE = 1_000; - private final UnhealthyContainersDao unhealthyContainersDao; private final ContainerSchemaDefinition containerSchemaDefinition; @Inject public ContainerHealthSchemaManager( - ContainerSchemaDefinition containerSchemaDefinition, - UnhealthyContainersDao unhealthyContainersDao) { - this.unhealthyContainersDao = unhealthyContainersDao; + ContainerSchemaDefinition containerSchemaDefinition) { this.containerSchemaDefinition = containerSchemaDefinition; } /** - * Insert or update unhealthy container records in UNHEALTHY_CONTAINERS table using TRUE batch insert. - * Uses JOOQ's batch API for optimal performance (single SQL statement for all records). - * Falls back to individual insert-or-update if batch insert fails (e.g., duplicate keys). + * Insert unhealthy container records in UNHEALTHY_CONTAINERS table using + * true batch insert. + * + *

        In the health-task flow, inserts are preceded by delete in the same + * transaction via {@link #replaceUnhealthyContainerRecordsAtomically(List, List)}. + * Therefore duplicate-key fallback is not expected and this method fails fast + * on any insert error.

        */ + @VisibleForTesting public void insertUnhealthyContainerRecords(List recs) { if (recs == null || recs.isEmpty()) { return; @@ -104,11 +102,6 @@ public void insertUnhealthyContainerRecords(List recs) LOG.debug("Batch inserted {} unhealthy container records", recs.size()); - } catch (DataAccessException e) { - // Batch insert failed (likely duplicate key) - fall back to insert-or-update per record - LOG.warn("Batch insert failed, falling back to individual insert-or-update for {} records", - recs.size(), e); - fallbackInsertOrUpdate(recs); } catch (Exception e) { LOG.error("Failed to batch insert records into {}", UNHEALTHY_CONTAINERS_TABLE_NAME, e); throw new RuntimeException("Recon failed to insert " + recs.size() + @@ -131,34 +124,6 @@ private void batchInsertInChunks(DSLContext dslContext, } } - private void fallbackInsertOrUpdate(List recs) { - try (Connection connection = containerSchemaDefinition.getDataSource().getConnection()) { - connection.setAutoCommit(false); - try { - for (UnhealthyContainerRecord rec : recs) { - UnhealthyContainers jooqRec = toJooqPojo(rec); - try { - unhealthyContainersDao.insert(jooqRec); - } catch (DataAccessException insertEx) { - // Duplicate key - update existing record - unhealthyContainersDao.update(jooqRec); - } - } - connection.commit(); - } catch (Exception innerEx) { - connection.rollback(); - LOG.error("Transaction rolled back during fallback insert", innerEx); - throw innerEx; - } finally { - connection.setAutoCommit(true); - } - } catch (Exception fallbackEx) { - LOG.error("Failed to insert {} records even with fallback", recs.size(), fallbackEx); - throw new RuntimeException("Recon failed to insert " + recs.size() + - " unhealthy container records.", fallbackEx); - } - } - private UnhealthyContainersRecord toJooqRecord(DSLContext txContext, UnhealthyContainerRecord rec) { UnhealthyContainersRecord record = txContext.newRecord(UNHEALTHY_CONTAINERS); @@ -172,17 +137,6 @@ private UnhealthyContainersRecord toJooqRecord(DSLContext txContext, return record; } - private UnhealthyContainers toJooqPojo(UnhealthyContainerRecord rec) { - return new UnhealthyContainers( - rec.getContainerId(), - rec.getContainerState(), - rec.getInStateSince(), - rec.getExpectedReplicaCount(), - rec.getActualReplicaCount(), - rec.getReplicaDelta(), - rec.getReason()); - } - /** * Batch delete all health states for multiple containers. * This deletes all states generated from SCM/Recon health scans: 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 index ab756461773b..9aed13b8534d 100644 --- 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 @@ -86,7 +86,7 @@ public TestReconReplicationManager() { public void setUp() throws Exception { dao = getDao(UnhealthyContainersDao.class); schemaManagerV2 = new ContainerHealthSchemaManager( - getSchemaDefinition(ContainerSchemaDefinition.class), dao); + getSchemaDefinition(ContainerSchemaDefinition.class)); containerManager = mock(ContainerManager.class); PlacementPolicy placementPolicy = mock(PlacementPolicy.class); @@ -463,6 +463,7 @@ protected boolean processContainer(ContainerInfo containerInfo, stateByContainer.get(containerInfo.getContainerID()); if (state != null) { reconReport.incrementAndSample(state, containerInfo); + containerInfo.setHealthState(state); return true; } return false; @@ -478,6 +479,7 @@ protected boolean processContainer(ContainerInfo containerInfo, stateByContainer.get(containerInfo.getContainerID()); if (state != null) { reconReport.incrementAndSample(state, containerInfo); + containerInfo.setHealthState(state); return true; } return false; 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 index 1c810fde7c1a..5cc90e88409f 100644 --- 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 @@ -99,7 +99,8 @@ * 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 (100 K × 5 states = 500 K rows) via a single + * container IDs list covering all rows + * (200 K × 5 states = 1 M rows) via a single * IN-clause DELETE.
      • * * @@ -111,9 +112,9 @@ * 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 the 1 M-row dataset is - * inserted exactly once in {@code @BeforeAll} and shared across all - * {@code @Test} methods in the class.
      • + *
      • 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) @@ -162,14 +163,13 @@ public class TestUnhealthyContainersDerbyPerformance { * call in the delete test. * *

      {@code batchDeleteSCMStatesForContainers} now handles internal - * chunking at {@link ContainerHealthSchemaManager#MAX_DELETE_CHUNK_SIZE} - * ({@value ContainerHealthSchemaManager#MAX_DELETE_CHUNK_SIZE} IDs per - * SQL statement) to stay within Derby's 64 KB generated-bytecode limit + * 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; // matches MAX_DELETE_CHUNK_SIZE + private static final int DELETE_CHUNK_SIZE = 1_000; /** * Number of records returned per page in the paginated-read tests. @@ -202,8 +202,10 @@ public class TestUnhealthyContainersDerbyPerformance { */ private static final long MAX_PAGINATED_READ_SECONDS = 60; - /** Maximum acceptable time to batch-delete 500 K rows. */ - private static final long MAX_DELETE_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) @@ -218,9 +220,8 @@ public class TestUnhealthyContainersDerbyPerformance { // ----------------------------------------------------------------------- /** - * Initialises the embedded Derby database, creates the Recon schema, and - * inserts {@value #TOTAL_RECORDS} records. This runs exactly once for the - * entire test class. + * 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 @@ -238,7 +239,7 @@ public class TestUnhealthyContainersDerbyPerformance { *

    */ @BeforeAll - public void setUpDatabaseAndInsertData(@TempDir Path tempDir) throws Exception { + 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); @@ -272,21 +273,25 @@ protected void configure() { dao = injector.getInstance(UnhealthyContainersDao.class); schemaDefinition = injector.getInstance(ContainerSchemaDefinition.class); - schemaManager = new ContainerHealthSchemaManager(schemaDefinition, dao); + schemaManager = new ContainerHealthSchemaManager(schemaDefinition); + } - // ----- Insert 1 M records in small per-transaction chunks ----- - // - // Why chunked? insertUnhealthyContainerRecords wraps its entire input in - // a single Derby transaction. Passing all 1 M records at once forces Derby - // to buffer the full WAL before committing, which exhausts its log and - // causes the call to hang. Committing every CONTAINERS_PER_TX containers - // (= 10 K rows) keeps each transaction small and lets Derby flush the log. + // ----------------------------------------------------------------------- + // 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("Starting bulk INSERT: {} records ({} containers/tx, {} transactions)", + LOG.info("--- Test 1: Batch INSERT {} records ({} containers/tx, {} transactions) ---", TOTAL_RECORDS, CONTAINERS_PER_TX, txCount); long now = System.currentTimeMillis(); - long insertStart = System.nanoTime(); + 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); @@ -294,29 +299,24 @@ protected void configure() { schemaManager.insertUnhealthyContainerRecords(chunk); } - long insertElapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - insertStart); - double insertThroughput = (double) TOTAL_RECORDS / (insertElapsedMs / 1000.0); - LOG.info("INSERT complete: {} records in {} ms ({} rec/sec, {} tx)", - TOTAL_RECORDS, insertElapsedMs, String.format("%.0f", insertThroughput), txCount); + 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(insertElapsedMs <= TimeUnit.SECONDS.toMillis(MAX_INSERT_SECONDS), + assertTrue(elapsedMs <= TimeUnit.SECONDS.toMillis(MAX_INSERT_SECONDS), String.format("INSERT took %d ms, exceeded %d s threshold", - insertElapsedMs, MAX_INSERT_SECONDS)); + elapsedMs, MAX_INSERT_SECONDS)); } // ----------------------------------------------------------------------- - // Test 1 — Verify the inserted row count + // Test 2 — Verify the inserted row count // ----------------------------------------------------------------------- - /** - * Verifies that all {@value #TOTAL_RECORDS} rows are present using a - * COUNT(*) over the full table. This is the baseline correctness check - * for every subsequent read test. - */ @Test - @Order(1) + @Order(2) public void testTotalInsertedRecordCountIsOneMillion() { - LOG.info("--- Test 1: Verify total row count = {} ---", TOTAL_RECORDS); + LOG.info("--- Test 2: Verify total row count = {} ---", TOTAL_RECORDS); long countStart = System.nanoTime(); long totalCount = dao.count(); @@ -340,9 +340,9 @@ public void testTotalInsertedRecordCountIsOneMillion() { *

    Each state must have exactly {@value #CONTAINER_ID_RANGE} records.

    */ @Test - @Order(2) + @Order(3) public void testCountByStatePerformanceUsesIndex() { - LOG.info("--- Test 2: COUNT(*) by state (index-covered, {} records each) ---", + LOG.info("--- Test 3: COUNT(*) by state (index-covered, {} records each) ---", CONTAINER_ID_RANGE); DSLContext dsl = schemaDefinition.getDSLContext(); @@ -380,9 +380,9 @@ public void testCountByStatePerformanceUsesIndex() { * {@value #CONTAINER_ID_RANGE} records.

    */ @Test - @Order(3) + @Order(4) public void testGroupBySummaryQueryPerformance() { - LOG.info("--- Test 3: GROUP BY summary over {} rows ---", TOTAL_RECORDS); + LOG.info("--- Test 4: GROUP BY summary over {} rows ---", TOTAL_RECORDS); long start = System.nanoTime(); List summary = @@ -425,10 +425,10 @@ public void testGroupBySummaryQueryPerformance() { * */ @Test - @Order(4) + @Order(5) public void testPaginatedReadByStatePerformance() { UnHealthyContainerStates targetState = UnHealthyContainerStates.UNDER_REPLICATED; - LOG.info("--- Test 4: Paginated read of {} ({} records, page size {}) ---", + LOG.info("--- Test 5: Paginated read of {} ({} records, page size {}) ---", targetState, CONTAINER_ID_RANGE, READ_PAGE_SIZE); int totalRead = 0; @@ -491,9 +491,9 @@ public void testPaginatedReadByStatePerformance() { * This measures aggregate read throughput across the entire dataset. */ @Test - @Order(5) + @Order(6) public void testFullDatasetReadThroughputAllStates() { - LOG.info("--- Test 5: Full {} M record read (all states, paged) ---", + LOG.info("--- Test 6: Full {} M record read (all states, paged) ---", TOTAL_RECORDS / 1_000_000); long totalStart = System.nanoTime(); @@ -541,40 +541,92 @@ public void testFullDatasetReadThroughputAllStates() { } // ----------------------------------------------------------------------- - // Test 6 — Batch DELETE performance + // Test 7 — Atomic replace (delete + insert) performance for 1M records // ----------------------------------------------------------------------- /** - * Deletes records for the first half of container IDs (1 – 100,000) across - * all five states by passing the complete 100 K ID list in one call to + * 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 {@link ContainerHealthSchemaManager#MAX_DELETE_CHUNK_SIZE} + * 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 100 statements of + * 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: 100 K × 5 states = 500 K rows deleted, 500 K remain.

    + *

    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(6) - public void testBatchDeletePerformanceHalfTheContainers() { - int deleteCount = CONTAINER_ID_RANGE / 2; // 100 000 container IDs - int expectedDeleted = deleteCount * STATE_COUNT; // 500 000 rows + @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 / ContainerHealthSchemaManager.MAX_DELETE_CHUNK_SIZE); + (double) deleteCount / DELETE_CHUNK_SIZE); - LOG.info("--- Test 6: Batch DELETE — {} IDs × {} states = {} rows " + LOG.info("--- Test 8: Batch DELETE — {} IDs × {} states = {} rows " + "({} internal SQL statements of {} IDs) ---", deleteCount, STATE_COUNT, expectedDeleted, - internalChunks, ContainerHealthSchemaManager.MAX_DELETE_CHUNK_SIZE); + internalChunks, DELETE_CHUNK_SIZE); long start = System.nanoTime(); @@ -607,19 +659,17 @@ public void testBatchDeletePerformanceHalfTheContainers() { } // ----------------------------------------------------------------------- - // Test 7 — Re-read counts after partial delete + // Test 9 — Re-read counts after full delete // ----------------------------------------------------------------------- /** - * After the deletion in Test 6, verifies that each state has exactly - * {@code CONTAINER_ID_RANGE / 2} records (100 K), confirming that the - * index-covered COUNT query remains accurate after a large delete. + * After full delete, verifies that each state has 0 records. */ @Test - @Order(7) - public void testCountByStateAfterPartialDelete() { - int expectedPerState = CONTAINER_ID_RANGE / 2; - LOG.info("--- Test 7: COUNT by state after 50%% delete (expected {} each) ---", + @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(); @@ -636,7 +686,7 @@ public void testCountByStateAfterPartialDelete() { LOG.info(" COUNT({}) = {} rows in {} ms", state, stateCount, elapsedMs); assertEquals(expectedPerState, stateCount, - "After partial delete, state " + state + "After full delete, state " + state + " should have exactly " + expectedPerState + " records"); } } From 548b0f70018f33b44623ecbaa56007898b8f587d Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Tue, 24 Mar 2026 12:34:40 +0530 Subject: [PATCH 43/43] HDDS-13891. Fixed failed test. --- .../ozone/recon/fsck/TestReconReplicationManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index 9aed13b8534d..64867279169c 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -33,6 +34,7 @@ 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; @@ -409,6 +411,8 @@ 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)); @@ -416,6 +420,12 @@ private ContainerInfo mockContainerInfo(long containerId, long 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; }