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

ArafatKhan2198 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 6ee2b423fb4 HDDS-14927. Add Quasi-Closed Container Tracking in Recon. 
(#10198).
6ee2b423fb4 is described below

commit 6ee2b423fb4bb79b3de933be825d68effe31f155
Author: Arafat2198 <[email protected]>
AuthorDate: Tue May 12 10:41:38 2026 +0530

    HDDS-14927. Add Quasi-Closed Container Tracking in Recon. (#10198).
---
 .../TestReconQuasiClosedContainerEndpoint.java     | 242 +++++++++++++++++++++
 .../hadoop/ozone/recon/api/ContainerEndpoint.java  |  62 ++++++
 .../api/types/QuasiClosedContainerMetadata.java    | 124 +++++++++++
 .../api/types/QuasiClosedContainersResponse.java   |  82 +++++++
 .../src/v2/components/tables/containersTable.tsx   |  11 +-
 .../src/v2/pages/containers/containers.tsx         | 165 +++++++++++++-
 .../src/v2/types/container.types.ts                |  22 +-
 .../ozone/recon/api/TestContainerEndpoint.java     | 203 +++++++++++++++++
 8 files changed, 899 insertions(+), 12 deletions(-)

diff --git 
a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconQuasiClosedContainerEndpoint.java
 
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconQuasiClosedContainerEndpoint.java
new file mode 100644
index 00000000000..c70151b9946
--- /dev/null
+++ 
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconQuasiClosedContainerEndpoint.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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+import javax.ws.rs.core.Response;
+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.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import 
org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.recon.api.ContainerEndpoint;
+import org.apache.hadoop.ozone.recon.api.types.QuasiClosedContainerMetadata;
+import org.apache.hadoop.ozone.recon.api.types.QuasiClosedContainersResponse;
+import org.apache.hadoop.ozone.recon.scm.ReconContainerManager;
+import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
+import org.apache.ozone.test.LambdaTestUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Integration tests for the GET /containers/quasiClosed endpoint.
+ *
+ * The cluster is started once for the entire test class 
(@TestInstance.PER_CLASS)
+ * so the expensive MiniOzoneCluster boot only happens once instead of once 
per test.
+ *
+ * Each test allocates containers using unique IDs from CONTAINER_ID_SEQ and 
uses
+ * those IDs as pagination cursors so tests don't interfere with each other.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestReconQuasiClosedContainerEndpoint {
+
+  private static final int PIPELINE_READY_TIMEOUT_MS = 30000;
+  private static final int POLL_INTERVAL_MS = 500;
+
+  /**
+   * Monotonically increasing ID counter. Each test records its start ID and
+   * uses (startId - 1) as the minContainerId cursor so it only sees its own
+   * containers when paginating.
+   */
+  private final AtomicLong containerIdSeq = new AtomicLong(10000L);
+
+  private MiniOzoneCluster cluster; // NOPMD - shared across @BeforeAll and 
@AfterAll
+  private ReconService recon; // NOPMD
+  private ContainerEndpoint containerEndpoint;
+  private ReconContainerManager reconContainerManager;
+  private ReconStorageContainerManagerFacade reconScm;
+
+  @BeforeAll
+  public void init() throws Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    recon = new ReconService(conf);
+    cluster = MiniOzoneCluster.newBuilder(conf)
+        .setNumDatanodes(3)
+        .addService(recon)
+        .build();
+    cluster.waitForClusterToBeReady();
+    cluster.waitForPipelineTobeReady(HddsProtos.ReplicationFactor.THREE, 
30000);
+
+    reconScm = (ReconStorageContainerManagerFacade)
+        recon.getReconServer().getReconStorageContainerManager();
+
+    // Wait for Recon's pipeline manager to be populated from SCM.
+    LambdaTestUtils.await(PIPELINE_READY_TIMEOUT_MS, POLL_INTERVAL_MS,
+        () -> !reconScm.getPipelineManager().getPipelines(
+            
RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE))
+            .isEmpty());
+
+    reconContainerManager = (ReconContainerManager) 
reconScm.getContainerManager();
+
+    containerEndpoint = new ContainerEndpoint(
+        reconScm,
+        null,  // ContainerHealthSchemaManager — not needed
+        null,  // ReconNamespaceSummaryManager — not needed
+        null,  // ReconContainerMetadataManager — not needed
+        null,  // ReconOMMetadataManager — not needed
+        null); // ExportJobManager — not needed
+  }
+
+  @AfterAll
+  public void shutdown() {
+    IOUtils.closeQuietly(cluster);
+  }
+
+  /**
+   * Injects a container with the next available ID directly into Recon's
+   * in-memory state — no RPC sync needed. Returns the assigned ID.
+   */
+  private long createQuasiClosedContainer() throws Exception {
+    long id = containerIdSeq.getAndIncrement();
+    Pipeline pipeline = reconScm.getPipelineManager()
+        .getPipelines(
+            
RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE))
+        .get(0);
+
+    ContainerInfo containerInfo = new ContainerInfo.Builder()
+        .setContainerID(id)
+        .setNumberOfKeys(5)
+        .setPipelineID(pipeline.getId())
+        .setReplicationConfig(
+            
RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE))
+        .setOwner("test")
+        .setState(HddsProtos.LifeCycleState.OPEN)
+        .build();
+
+    reconContainerManager.addNewContainer(
+        new ContainerWithPipeline(containerInfo, pipeline));
+    reconContainerManager.updateContainerState(
+        ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.FINALIZE);
+    reconContainerManager.updateContainerState(
+        ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.QUASI_CLOSE);
+    return id;
+  }
+
+  @Test
+  public void testBasicQuasiClosedList() throws Exception {
+    long startId = containerIdSeq.get();
+    long id1 = createQuasiClosedContainer();
+    long id2 = createQuasiClosedContainer();
+
+    // Use (startId - 1) so we only see containers created in this test.
+    Response response = containerEndpoint.getQuasiClosedContainers(1000, 
startId - 1);
+    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse) response.getEntity();
+    assertNotNull(result);
+
+    List<Long> returnedIds = result.getContainers().stream()
+        .map(QuasiClosedContainerMetadata::getContainerID)
+        .collect(Collectors.toList());
+    assertTrue(returnedIds.contains(id1));
+    assertTrue(returnedIds.contains(id2));
+
+    result.getContainers().forEach(c -> {
+      assertEquals(3L, c.getExpectedReplicaCount());
+      assertTrue(c.getStateEnterTime() >= 0);
+      assertNotNull(c.getPipelineID());
+    });
+  }
+
+  @Test
+  public void testPagination() throws Exception {
+    final int totalContainers = 25;
+    final int pageSize = 7;
+
+    long startId = containerIdSeq.get();
+    for (int i = 0; i < totalContainers; i++) {
+      createQuasiClosedContainer();
+    }
+    long endId = containerIdSeq.get() - 1;
+
+    // Walk pages using the cursor, collecting only IDs in our range [startId, 
endId].
+    List<Long> allReturnedIds = new ArrayList<>();
+    long cursor = startId - 1;
+    int pagesVisited = 0;
+
+    while (true) {
+      QuasiClosedContainersResponse page =
+          (QuasiClosedContainersResponse)
+              containerEndpoint.getQuasiClosedContainers(pageSize, 
cursor).getEntity();
+
+      List<Long> pageIds = page.getContainers().stream()
+          .map(QuasiClosedContainerMetadata::getContainerID)
+          .filter(id -> id >= startId && id <= endId)
+          .collect(Collectors.toList());
+
+      if (pageIds.isEmpty()) {
+        break;
+      }
+
+      // No ID from this page should have been seen before.
+      for (Long id : pageIds) {
+        assertTrue(!allReturnedIds.contains(id),
+            "Duplicate container ID across pages: " + id);
+      }
+
+      allReturnedIds.addAll(pageIds);
+      cursor = page.getLastKey();
+      pagesVisited++;
+    }
+
+    assertEquals(totalContainers, allReturnedIds.size(),
+        "All created containers must be returned across pages");
+    // 25 containers / pageSize 7 = ceil(25/7) = 4 pages
+    assertEquals(4, pagesVisited);
+  }
+
+  @Test
+  public void testLimitZeroReturnsCountOnly() throws Exception {
+    createQuasiClosedContainer();
+    createQuasiClosedContainer();
+
+    // limit=0 must return empty containers but a non-zero total count.
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse)
+            containerEndpoint.getQuasiClosedContainers(0, 0L).getEntity();
+
+    assertTrue(result.getContainers() == null || 
result.getContainers().isEmpty(),
+        "limit=0 must return empty container list");
+    assertTrue(result.getQuasiClosedCount() >= 2,
+        "quasiClosedCount must reflect all quasi-closed containers");
+  }
+
+  @Test
+  public void testInvalidInputsReturnBadRequest() {
+    assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+        containerEndpoint.getQuasiClosedContainers(10, -1L).getStatus(),
+        "Negative minContainerId must return 400");
+    assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+        containerEndpoint.getQuasiClosedContainers(-1, 0L).getStatus(),
+        "Negative limit must return 400");
+  }
+}
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 3c97c0a791f..3eea69aa536 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
@@ -80,6 +80,8 @@
 import org.apache.hadoop.ozone.recon.api.types.KeysResponse;
 import org.apache.hadoop.ozone.recon.api.types.MissingContainerMetadata;
 import org.apache.hadoop.ozone.recon.api.types.MissingContainersResponse;
+import org.apache.hadoop.ozone.recon.api.types.QuasiClosedContainerMetadata;
+import org.apache.hadoop.ozone.recon.api.types.QuasiClosedContainersResponse;
 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;
@@ -976,4 +978,64 @@ public Response getOmContainersDeletedInSCM(
     response.put("containerDiscrepancyInfo", containerDiscrepancyInfoList);
     return Response.ok(response).build();
   }
+
+  /**
+   * Return all containers in QUASI_CLOSED state.
+   *
+   * @param limit          max no. of containers to get.
+   * @param minContainerId cursor — return containers with ID &gt; 
minContainerId.
+   * @return {@link Response}
+   */
+  @GET
+  @Path("/quasiClosed")
+  public Response getQuasiClosedContainers(
+      @DefaultValue(DEFAULT_FETCH_COUNT) @QueryParam(RECON_QUERY_LIMIT) int 
limit,
+      @DefaultValue(PREV_CONTAINER_ID_DEFAULT_VALUE)
+      @QueryParam(RECON_QUERY_MIN_CONTAINER_ID) long minContainerId) {
+
+    if (minContainerId < 0) {
+      return Response.status(Response.Status.BAD_REQUEST)
+          .entity("minContainerId must be >= 0").build();
+    }
+    if (limit < 0) {
+      return Response.status(Response.Status.BAD_REQUEST)
+          .entity("limit must be >= 0").build();
+    }
+
+    List<ContainerInfo> containers = containerManager.getContainers(
+        ContainerID.valueOf(minContainerId + 1), limit, 
HddsProtos.LifeCycleState.QUASI_CLOSED);
+
+    List<QuasiClosedContainerMetadata> metaList = containers.stream()
+        .map(this::toQuasiClosedMetadata)
+        .collect(Collectors.toList());
+
+    long firstKey = metaList.isEmpty() ? minContainerId : 
metaList.get(0).getContainerID();
+    long lastKey  = metaList.isEmpty() ? minContainerId : 
metaList.get(metaList.size() - 1).getContainerID();
+    int total     = 
containerManager.getContainerStateCount(HddsProtos.LifeCycleState.QUASI_CLOSED);
+
+    return Response.ok(new QuasiClosedContainersResponse(total, firstKey, 
lastKey, metaList)).build();
+  }
+
+  private QuasiClosedContainerMetadata toQuasiClosedMetadata(ContainerInfo ci) 
{
+    try {
+      long containerID = ci.getContainerID();
+      int requiredNodes = ci.getReplicationConfig().getRequiredNodes();
+      List<ContainerHistory> replicas =
+          containerManager.getLatestContainerHistory(containerID, 
requiredNodes);
+      long stateEnterTime = ci.getStateEnterTime() != null
+          ? ci.getStateEnterTime().toEpochMilli() : 0L;
+      String pipelineID = ci.getPipelineID() != null
+          ? ci.getPipelineID().getId().toString() : null;
+      return new QuasiClosedContainerMetadata(
+          containerID,
+          pipelineID,
+          ci.getNumberOfKeys(),
+          stateEnterTime,
+          requiredNodes,
+          replicas.size(),
+          replicas);
+    } catch (Exception e) {
+      throw new WebApplicationException(e, 
Response.Status.INTERNAL_SERVER_ERROR);
+    }
+  }
 }
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/QuasiClosedContainerMetadata.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/QuasiClosedContainerMetadata.java
new file mode 100644
index 00000000000..72a8dda29a4
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/QuasiClosedContainerMetadata.java
@@ -0,0 +1,124 @@
+/*
+ * 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.api.types;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import org.apache.hadoop.ozone.recon.persistence.ContainerHistory;
+
+/**
+ * JSON response DTO for a single QUASI_CLOSED container.
+ * Uses semantically correct field names (stateEnterTime, actualReplicaCount)
+ * instead of reusing the unhealthy-container vocabulary.
+ */
+public class QuasiClosedContainerMetadata {
+
+  @JsonProperty("containerID")
+  private long containerID;
+
+  @JsonProperty("pipelineID")
+  private String pipelineID;
+
+  @JsonProperty("keys")
+  private long keys;
+
+  /** Epoch millis when the container entered QUASI_CLOSED state per SCM. */
+  @JsonProperty("stateEnterTime")
+  private long stateEnterTime;
+
+  @JsonProperty("expectedReplicaCount")
+  private long expectedReplicaCount;
+
+  @JsonProperty("actualReplicaCount")
+  private long actualReplicaCount;
+
+  @JsonProperty("replicas")
+  private List<ContainerHistory> replicas;
+
+  public QuasiClosedContainerMetadata() {
+  }
+
+  public QuasiClosedContainerMetadata(
+      long containerID, String pipelineID, long keys,
+      long stateEnterTime, long expectedReplicaCount,
+      long actualReplicaCount, List<ContainerHistory> replicas) {
+    this.containerID = containerID;
+    this.pipelineID = pipelineID;
+    this.keys = keys;
+    this.stateEnterTime = stateEnterTime;
+    this.expectedReplicaCount = expectedReplicaCount;
+    this.actualReplicaCount = actualReplicaCount;
+    this.replicas = replicas;
+  }
+
+  public long getContainerID() {
+    return containerID;
+  }
+
+  public void setContainerID(long containerID) {
+    this.containerID = containerID;
+  }
+
+  public String getPipelineID() {
+    return pipelineID;
+  }
+
+  public void setPipelineID(String pipelineID) {
+    this.pipelineID = pipelineID;
+  }
+
+  public long getKeys() {
+    return keys;
+  }
+
+  public void setKeys(long keys) {
+    this.keys = keys;
+  }
+
+  public long getStateEnterTime() {
+    return stateEnterTime;
+  }
+
+  public void setStateEnterTime(long stateEnterTime) {
+    this.stateEnterTime = stateEnterTime;
+  }
+
+  public long getExpectedReplicaCount() {
+    return expectedReplicaCount;
+  }
+
+  public void setExpectedReplicaCount(long expectedReplicaCount) {
+    this.expectedReplicaCount = expectedReplicaCount;
+  }
+
+  public long getActualReplicaCount() {
+    return actualReplicaCount;
+  }
+
+  public void setActualReplicaCount(long actualReplicaCount) {
+    this.actualReplicaCount = actualReplicaCount;
+  }
+
+  public List<ContainerHistory> getReplicas() {
+    return replicas;
+  }
+
+  public void setReplicas(List<ContainerHistory> replicas) {
+    this.replicas = replicas;
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/QuasiClosedContainersResponse.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/QuasiClosedContainersResponse.java
new file mode 100644
index 00000000000..76d31d97236
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/types/QuasiClosedContainersResponse.java
@@ -0,0 +1,82 @@
+/*
+ * 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.api.types;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * API response wrapper for the quasi-closed containers endpoint.
+ */
+public class QuasiClosedContainersResponse {
+
+  @JsonProperty("quasiClosedCount")
+  private long quasiClosedCount = 0;
+
+  @JsonProperty("firstKey")
+  private long firstKey = 0;
+
+  @JsonProperty("lastKey")
+  private long lastKey = 0;
+
+  @JsonProperty("containers")
+  private List<QuasiClosedContainerMetadata> containers;
+
+  public QuasiClosedContainersResponse() {
+  }
+
+  public QuasiClosedContainersResponse(long quasiClosedCount, long firstKey, 
long lastKey,
+      List<QuasiClosedContainerMetadata> containers) {
+    this.quasiClosedCount = quasiClosedCount;
+    this.firstKey = firstKey;
+    this.lastKey = lastKey;
+    this.containers = containers;
+  }
+
+  public long getQuasiClosedCount() {
+    return quasiClosedCount;
+  }
+
+  public void setQuasiClosedCount(long quasiClosedCount) {
+    this.quasiClosedCount = quasiClosedCount;
+  }
+
+  public long getFirstKey() {
+    return firstKey;
+  }
+
+  public void setFirstKey(long firstKey) {
+    this.firstKey = firstKey;
+  }
+
+  public long getLastKey() {
+    return lastKey;
+  }
+
+  public void setLastKey(long lastKey) {
+    this.lastKey = lastKey;
+  }
+
+  public List<QuasiClosedContainerMetadata> getContainers() {
+    return containers;
+  }
+
+  public void setContainers(List<QuasiClosedContainerMetadata> containers) {
+    this.containers = containers;
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
index 1b76c0371aa..92b521259be 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
@@ -192,14 +192,23 @@ const ContainerTable: React.FC<ContainerTableProps> = ({
   hasPrevPage,
   pageSize,
   onPageSizeChange,
+  sinceColumnTitle = 'Unhealthy Since',
 }) => {
 
 
   function filterSelectedColumns() {
     const columnKeys = selectedColumns.map((column) => column.value);
-    return COLUMNS.filter(
+    const filteredColumns = COLUMNS.filter(
       (column) => columnKeys.indexOf(column.key as string) >= 0
     );
+    
+    // Override the title for the unhealthySince column if needed
+    return filteredColumns.map(col => {
+      if (col.key === 'unhealthySince') {
+        return { ...col, title: sinceColumnTitle };
+      }
+      return col;
+    });
   }
 
   async function loadRowData(containerID: number) {
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
index 6c920dfba8d..75496c1a916 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
@@ -45,10 +45,13 @@ import { useAutoReload } from 
"@/v2/hooks/useAutoReload.hook";
 import * as CONSTANTS from '@/v2/constants/overview.constants';
 
 import {
+  Container,
   ContainersPaginationResponse,
   ContainerState,
   ExpandedRow,
   ExportJob,
+  QuasiClosedContainer,
+  QuasiClosedContainersResponse,
   TabPaginationState,
 } from "@/v2/types/container.types";
 import { ClusterStateResponse } from "@/v2/types/overview.types";
@@ -64,6 +67,8 @@ const TAB_STATE_MAP: Record<string, string> = {
   '3': 'OVER_REPLICATED',
   '4': 'MIS_REPLICATED',
   '5': 'REPLICA_MISMATCH',
+  // '6' (Quasi Closed) intentionally absent — it uses /quasiClosed, not 
/unhealthy/:state
+  // '7' (Export) intentionally absent — it has no container data to fetch
 };
 
 const EXPORT_STATE_OPTIONS = [
@@ -99,6 +104,26 @@ const DEFAULT_TAB_STATE: TabPaginationState = {
 
 const POLL_INTERVAL_MS = 3000;
 
+/**
+ * Maps a QuasiClosedContainer (from the /quasiClosed API) to the shared
+ * Container type so it can be displayed in the existing ContainerTable.
+ * Explicit field mapping ensures TypeScript catches any upstream renames.
+ */
+function toContainer(qc: QuasiClosedContainer): Container {
+  return {
+    containerID: qc.containerID,
+    pipelineID: qc.pipelineID,
+    keys: qc.keys,
+    containerState: 'QUASI_CLOSED',
+    unhealthySince: qc.stateEnterTime,
+    expectedReplicaCount: qc.expectedReplicaCount,
+    actualReplicaCount: qc.actualReplicaCount,
+    replicaDeltaCount: qc.actualReplicaCount - qc.expectedReplicaCount,
+    reason: '',
+    replicas: qc.replicas,
+  };
+}
+
 const Containers: React.FC<{}> = () => {
   const [state, setState] = useState<ContainerState>({
     lastUpdated: 0,
@@ -109,6 +134,7 @@ const Containers: React.FC<{}> = () => {
     overReplicatedCount: 0,
     misReplicatedCount: 0,
     replicaMismatchCount: 0,
+    quasiClosedCount: 0,
   });
   const [pageSize, setPageSize] = useState<number>(DEFAULT_PAGE_SIZE);
   const [tabStates, setTabStates] = useState<Record<string, 
TabPaginationState>>({
@@ -117,6 +143,7 @@ const Containers: React.FC<{}> = () => {
     '3': { ...DEFAULT_TAB_STATE },
     '4': { ...DEFAULT_TAB_STATE },
     '5': { ...DEFAULT_TAB_STATE },
+    '6': { ...DEFAULT_TAB_STATE },
   });
   const [expandedRow, setExpandedRow] = useState<ExpandedRow>({});
   const [selectedColumns, setSelectedColumns] = 
useState<Option[]>(defaultColumns);
@@ -179,7 +206,7 @@ const Containers: React.FC<{}> = () => {
 
   // Start polling when Export tab is active; stop when leaving if no active 
jobs.
   useEffect(() => {
-    if (selectedTab === '6') {
+    if (selectedTab === '7') {
       startPolling();
     } else {
       const hasActive = exportJobs.some(
@@ -272,15 +299,79 @@ const Containers: React.FC<{}> = () => {
   };
 
   // ── Container data fetching ───────────────────────────────────────────────
+
+  // Fetches the quasi-closed count independently to populate Highlights on 
page load.
+  const fetchQuasiClosedCount = async () => {
+    try {
+      const response = await fetchData<QuasiClosedContainersResponse>(
+        '/api/v1/containers/quasiClosed',
+        'GET',
+        { limit: 0, minContainerId: 0 }
+      );
+      setState(prev => ({
+        ...prev,
+        quasiClosedCount: response.quasiClosedCount ?? prev.quasiClosedCount,
+      }));
+    } catch (_) {
+      // Non-critical: count stays 0 until the tab is opened.
+    }
+  };
+
   const fetchTabData = async (
     tabKey: string,
     minContainerId: number,
     currentPageSize: number
   ) => {
-    const containerStateName = TAB_STATE_MAP[tabKey];
-    if (!containerStateName) return; // skip Export tab (key='6') or unknown 
keys
     const fetchSize = currentPageSize + 1;
 
+    if (tabKey === '6') {
+      // Quasi-closed uses its own dedicated in-memory endpoint, not 
/unhealthy/:state.
+      setTabStates(prev => ({
+        ...prev,
+        [tabKey]: { ...prev[tabKey], loading: true },
+      }));
+      try {
+        const response = await fetchData<QuasiClosedContainersResponse>(
+          '/api/v1/containers/quasiClosed',
+          'GET',
+          { limit: fetchSize, minContainerId }
+        );
+        const allContainers = response.containers ?? [];
+        const hasNextPage = allContainers.length > currentPageSize;
+        const pageContainers = allContainers.slice(0, currentPageSize);
+        const mapped: Container[] = pageContainers.map(toContainer);
+        const lastKey = mapped.length > 0 ? Math.max(...mapped.map(c => 
c.containerID)) : 0;
+        const firstKey = mapped.length > 0 ? Math.min(...mapped.map(c => 
c.containerID)) : 0;
+        setTabStates(prev => ({
+          ...prev,
+          [tabKey]: {
+            ...prev[tabKey],
+            data: mapped,
+            loading: false,
+            firstKey,
+            lastKey,
+            currentMinContainerId: minContainerId,
+            hasNextPage,
+          },
+        }));
+        setState(prev => ({
+          ...prev,
+          quasiClosedCount: response.quasiClosedCount ?? prev.quasiClosedCount,
+          lastUpdated: Number(moment()),
+        }));
+      } catch (error) {
+        setTabStates(prev => ({
+          ...prev,
+          [tabKey]: { ...prev[tabKey], loading: false },
+        }));
+        showDataFetchError(error);
+      }
+      return;
+    }
+
+    const containerStateName = TAB_STATE_MAP[tabKey];
+    if (!containerStateName) return; // skips tab '7' (Export) and any unknown 
keys
+
     setTabStates(prev => ({
       ...prev,
       [tabKey]: { ...prev[tabKey], loading: true },
@@ -318,11 +409,11 @@ const Containers: React.FC<{}> = () => {
 
       setState(prev => ({
         ...prev,
-        missingCount: response.missingCount ?? 0,
-        underReplicatedCount: response.underReplicatedCount ?? 0,
-        overReplicatedCount: response.overReplicatedCount ?? 0,
-        misReplicatedCount: response.misReplicatedCount ?? 0,
-        replicaMismatchCount: response.replicaMismatchCount ?? 0,
+        missingCount: response.missingCount ?? prev.missingCount,
+        underReplicatedCount: response.underReplicatedCount ?? 
prev.underReplicatedCount,
+        overReplicatedCount: response.overReplicatedCount ?? 
prev.overReplicatedCount,
+        misReplicatedCount: response.misReplicatedCount ?? 
prev.misReplicatedCount,
+        replicaMismatchCount: response.replicaMismatchCount ?? 
prev.replicaMismatchCount,
         lastUpdated: Number(moment()),
       }));
     } catch (error) {
@@ -336,6 +427,7 @@ const Containers: React.FC<{}> = () => {
 
   useEffect(() => {
     fetchTabData('1', 0, DEFAULT_PAGE_SIZE);
+    fetchQuasiClosedCount();
   }, []); // eslint-disable-line react-hooks/exhaustive-deps
 
   function handleColumnChange(selected: ValueType<Option, true>) {
@@ -344,7 +436,7 @@ const Containers: React.FC<{}> = () => {
 
   function handleTabChange(key: string) {
     setSelectedTab(key);
-    if (key !== '6' && tabStates[key]?.data.length === 0 && 
!tabStates[key]?.loading) {
+    if (key !== '7' && tabStates[key]?.data.length === 0 && 
!tabStates[key]?.loading) {
       fetchTabData(key, 0, pageSize);
     }
   }
@@ -382,6 +474,7 @@ const Containers: React.FC<{}> = () => {
       '3': { ...DEFAULT_TAB_STATE },
       '4': { ...DEFAULT_TAB_STATE },
       '5': { ...DEFAULT_TAB_STATE },
+      '6': { ...DEFAULT_TAB_STATE },
     };
     setTabStates(reset);
     fetchTabData(selectedTab, 0, newSize);
@@ -394,8 +487,10 @@ const Containers: React.FC<{}> = () => {
       '3': { ...DEFAULT_TAB_STATE },
       '4': { ...DEFAULT_TAB_STATE },
       '5': { ...DEFAULT_TAB_STATE },
+      '6': { ...DEFAULT_TAB_STATE },
     });
     fetchTabData(selectedTab, 0, pageSize);
+    fetchQuasiClosedCount();
     clusterState.refetch();
   };
 
@@ -410,6 +505,7 @@ const Containers: React.FC<{}> = () => {
     overReplicatedCount,
     misReplicatedCount,
     replicaMismatchCount,
+    quasiClosedCount,
   } = state;
 
   const currentTabState = tabStates[selectedTab] ?? DEFAULT_TAB_STATE;
@@ -606,6 +702,10 @@ const Containers: React.FC<{}> = () => {
         Mismatched Replicas <br/>
         <span className='highlight-content-value'>{replicaMismatchCount ?? 
'N/A'}</span>
       </div>
+      <div className='highlight-content'>
+        Quasi Closed <br/>
+        <span className='highlight-content-value'>{quasiClosedCount ?? 
'N/A'}</span>
+      </div>
     </div>
   );
 
@@ -681,9 +781,54 @@ const Containers: React.FC<{}> = () => {
               </Tabs.TabPane>
             ))}
 
+            {/* ── Quasi-Closed tab ──────────────────────────────────────── 
*/}
+            <Tabs.TabPane key='6' tab='Quasi Closed'>
+              <div className='table-header-section'>
+                <div className='table-filter-section'>
+                  <MultiSelect
+                    options={columnOptions}
+                    defaultValue={selectedColumns}
+                    selected={selectedColumns}
+                    placeholder='Columns'
+                    onChange={handleColumnChange}
+                    fixedColumn='containerID'
+                    onTagClose={() => {}}
+                    columnLength={columnOptions.length} />
+                </div>
+                <Search
+                  disabled={tabStates['6'].data.length === 0}
+                  searchOptions={SearchableColumnOpts}
+                  searchInput={searchTerm}
+                  searchColumn={searchColumn}
+                  onSearchChange={
+                    (e: React.ChangeEvent<HTMLInputElement>) => 
setSearchTerm(e.target.value)
+                  }
+                  onChange={(value) => {
+                    setSearchTerm('');
+                    setSearchColumn(value as 'containerID' | 'pipelineID');
+                  }} />
+              </div>
+              <ContainerTable
+                data={tabStates['6'].data}
+                loading={tabStates['6'].loading}
+                searchColumn={searchColumn}
+                searchTerm={debouncedSearch}
+                selectedColumns={selectedColumns}
+                expandedRow={expandedRow}
+                expandedRowSetter={setExpandedRow}
+                onNextPage={() => handleNextPage('6')}
+                onPrevPage={() => handlePrevPage('6')}
+                hasNextPage={tabStates['6'].hasNextPage}
+                hasPrevPage={tabStates['6'].pageHistory.length > 0}
+                pageSize={pageSize}
+                onPageSizeChange={handlePageSizeChange}
+                sinceColumnTitle='State Enter Time'
+              />
+            </Tabs.TabPane>
+
             {/* ── Export tab ────────────────────────────────────────────── 
*/}
             <Tabs.TabPane
-              key='6'
+              key='7'
               tab={
                 <span>
                   <ExportOutlined />
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
index f7461c913f2..3351d70d26f 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
@@ -74,6 +74,23 @@ export type ContainersPaginationResponse = {
   replicaMismatchCount: number;
 }
 
+export type QuasiClosedContainer = {
+  containerID: number;
+  pipelineID: string;
+  keys: number;
+  stateEnterTime: number;
+  expectedReplicaCount: number;
+  actualReplicaCount: number;
+  replicas: ContainerReplica[];
+}
+
+export type QuasiClosedContainersResponse = {
+  quasiClosedCount: number;
+  firstKey: number;
+  lastKey: number;
+  containers: QuasiClosedContainer[];
+}
+
 export type TabPaginationState = {
   data: Container[];
   loading: boolean;
@@ -98,6 +115,7 @@ export type ContainerTableProps = {
   hasPrevPage: boolean;
   pageSize: number;
   onPageSizeChange: (newSize: number) => void;
+  sinceColumnTitle?: string;
 }
 
 
@@ -121,8 +139,10 @@ export type ContainerState = {
   overReplicatedCount: number;
   misReplicatedCount: number;
   replicaMismatchCount: number;
+  quasiClosedCount: number;
 }
 
+
 export type ExportJobStatus = 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED';
 
 export type ExportJob = {
@@ -140,4 +160,4 @@ export type ExportJob = {
   completedAt: number;
   downloadCount: number;
   downloadsRemaining: number;
-}
\ No newline at end of file
+}
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 cc2a02d0da3..aabf2a45a63 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
@@ -95,6 +95,8 @@
 import org.apache.hadoop.ozone.recon.api.types.KeysResponse;
 import org.apache.hadoop.ozone.recon.api.types.MissingContainerMetadata;
 import org.apache.hadoop.ozone.recon.api.types.MissingContainersResponse;
+import org.apache.hadoop.ozone.recon.api.types.QuasiClosedContainerMetadata;
+import org.apache.hadoop.ozone.recon.api.types.QuasiClosedContainersResponse;
 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;
@@ -1954,4 +1956,205 @@ public void testDuplicateFSOKeysForContainerEndpoint() 
throws Exception {
       }
     }
   }
+
+  @Test
+  public void testGetQuasiClosedContainersEmpty() throws Exception {
+    // No QUASI_CLOSED containers exist — endpoint must return an empty list 
with zero counts.
+    Response response = containerEndpoint.getQuasiClosedContainers(1000, 0L);
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse) response.getEntity();
+
+    assertNotNull(result);
+    assertTrue(result.getContainers() == null || 
result.getContainers().isEmpty());
+    assertEquals(0L, result.getQuasiClosedCount());
+    assertEquals(0L, result.getFirstKey());
+    assertEquals(0L, result.getLastKey());
+  }
+
+  @Test
+  public void testGetQuasiClosedContainersBasic() throws Exception {
+    // Add 3 containers and transition them to QUASI_CLOSED.
+    reconContainerManager.addNewContainer(
+        getTestContainer(HddsProtos.LifeCycleState.OPEN, 200L));
+    reconContainerManager.addNewContainer(
+        getTestContainer(HddsProtos.LifeCycleState.OPEN, 201L));
+    reconContainerManager.addNewContainer(
+        getTestContainer(HddsProtos.LifeCycleState.OPEN, 202L));
+
+    for (long id = 200L; id <= 202L; id++) {
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.FINALIZE);
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.QUASI_CLOSE);
+    }
+    assertContainerCount(HddsProtos.LifeCycleState.QUASI_CLOSED, 3);
+
+    Response response = containerEndpoint.getQuasiClosedContainers(1000, 0L);
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse) response.getEntity();
+
+    assertNotNull(result);
+    assertEquals(3L, result.getQuasiClosedCount());
+    assertEquals(200L, result.getFirstKey());
+    assertEquals(202L, result.getLastKey());
+
+    List<QuasiClosedContainerMetadata> containers =
+        new ArrayList<>(result.getContainers());
+    assertEquals(3, containers.size());
+    containers.forEach(c -> {
+      // StandaloneReplicationConfig.ONE → requiredNodes = 1
+      assertEquals(1L, c.getExpectedReplicaCount());
+    });
+  }
+
+  @Test
+  public void testGetQuasiClosedContainersWithReplicas() throws Exception {
+    // Use RATIS/THREE so requiredNodes=3, which is > number of replicas we 
add.
+    // getLatestContainerHistory uses requiredNodes as its limit, so if we used
+    // StandaloneReplicationConfig.ONE (requiredNodes=1) only 1 replica would 
come back.
+    Pipeline localPipeline = getRandomPipeline();
+    reconPipelineManager.addPipeline(localPipeline);
+    ContainerInfo containerInfo = new ContainerInfo.Builder()
+        .setContainerID(210L)
+        .setNumberOfKeys(10)
+        .setPipelineID(localPipeline.getId())
+        
.setReplicationConfig(RatisReplicationConfig.getInstance(ReplicationFactor.THREE))
+        .setOwner("test")
+        .setState(HddsProtos.LifeCycleState.OPEN)
+        .build();
+    reconContainerManager.addNewContainer(
+        new ContainerWithPipeline(containerInfo, localPipeline));
+    reconContainerManager.updateContainerState(
+        ContainerID.valueOf(210L), HddsProtos.LifeCycleEvent.FINALIZE);
+    reconContainerManager.updateContainerState(
+        ContainerID.valueOf(210L), HddsProtos.LifeCycleEvent.QUASI_CLOSE);
+
+    // Register 2 datanodes and upsert replica history for container 210.
+    UUID dn1 = newDatanode("qc-host1", "10.0.0.1");
+    UUID dn2 = newDatanode("qc-host2", "10.0.0.2");
+    reconContainerManager.upsertContainerHistory(
+        210L, dn1, 1L, 2L, "QUASI_CLOSED", ContainerChecksums.of(1111L, 0L));
+    reconContainerManager.upsertContainerHistory(
+        210L, dn2, 3L, 4L, "QUASI_CLOSED", ContainerChecksums.of(1111L, 0L));
+
+    Response response = containerEndpoint.getQuasiClosedContainers(1000, 0L);
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse) response.getEntity();
+
+    assertNotNull(result);
+    assertEquals(1, result.getContainers().size());
+
+    QuasiClosedContainerMetadata meta = result.getContainers().get(0);
+    assertEquals(210L, meta.getContainerID());
+    assertEquals(2L, meta.getActualReplicaCount());
+    assertEquals(2, meta.getReplicas().size());
+
+    Set<String> returnedHosts = meta.getReplicas().stream()
+        .map(ContainerHistory::getDatanodeHost)
+        .collect(Collectors.toSet());
+    assertThat(returnedHosts).contains("qc-host1", "qc-host2");
+  }
+
+  @Test
+  public void testGetQuasiClosedContainersPagination() throws Exception {
+    // Add 6 containers (IDs 300–305) in QUASI_CLOSED state.
+    for (long id = 300L; id <= 305L; id++) {
+      reconContainerManager.addNewContainer(
+          getTestContainer(HddsProtos.LifeCycleState.OPEN, id));
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.FINALIZE);
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.QUASI_CLOSE);
+    }
+    assertContainerCount(HddsProtos.LifeCycleState.QUASI_CLOSED, 6);
+
+    // Page 1: fetch first 3.
+    Response page1Response = containerEndpoint.getQuasiClosedContainers(3, 0L);
+    QuasiClosedContainersResponse page1 =
+        (QuasiClosedContainersResponse) page1Response.getEntity();
+
+    assertEquals(3, page1.getContainers().size());
+    assertEquals(6L, page1.getQuasiClosedCount());
+    assertEquals(300L, page1.getFirstKey());
+    assertEquals(302L, page1.getLastKey());
+
+    // Page 2: use lastKey from page 1 as prevKey cursor.
+    Response page2Response =
+        containerEndpoint.getQuasiClosedContainers(3, page1.getLastKey());
+    QuasiClosedContainersResponse page2 =
+        (QuasiClosedContainersResponse) page2Response.getEntity();
+
+    assertEquals(3, page2.getContainers().size());
+    assertEquals(6L, page2.getQuasiClosedCount());
+    assertEquals(303L, page2.getFirstKey());
+    assertEquals(305L, page2.getLastKey());
+
+    // IDs must not overlap between pages.
+    Set<Long> page1Ids = page1.getContainers().stream()
+        .map(QuasiClosedContainerMetadata::getContainerID)
+        .collect(Collectors.toSet());
+    Set<Long> page2Ids = page2.getContainers().stream()
+        .map(QuasiClosedContainerMetadata::getContainerID)
+        .collect(Collectors.toSet());
+    assertTrue(Collections.disjoint(page1Ids, page2Ids));
+  }
+
+  @Test
+  public void testGetQuasiClosedContainersLimitZeroReturnsCountOnly() throws 
Exception {
+    // Add 3 containers in QUASI_CLOSED state.
+    for (long id = 500L; id <= 502L; id++) {
+      reconContainerManager.addNewContainer(
+          getTestContainer(HddsProtos.LifeCycleState.OPEN, id));
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.FINALIZE);
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.QUASI_CLOSE);
+    }
+    assertContainerCount(HddsProtos.LifeCycleState.QUASI_CLOSED, 3);
+
+    // limit=0 must return an empty container list but still populate 
quasiClosedCount.
+    Response response = containerEndpoint.getQuasiClosedContainers(0, 0L);
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse) response.getEntity();
+
+    assertNotNull(result);
+    assertTrue(result.getContainers() == null || 
result.getContainers().isEmpty());
+    assertEquals(3, result.getQuasiClosedCount());
+  }
+
+  @Test
+  public void testGetQuasiClosedContainersInvalidInputsReturnBadRequest() {
+    // Negative minContainerId must return 400.
+    assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+        containerEndpoint.getQuasiClosedContainers(10, -1L).getStatus());
+
+    // Negative limit must return 400.
+    assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+        containerEndpoint.getQuasiClosedContainers(-1, 0L).getStatus());
+  }
+
+  @Test
+  public void testGetQuasiClosedCountViaDedicatedEndpoint() throws Exception {
+    // Add 2 containers and move them to QUASI_CLOSED.
+    reconContainerManager.addNewContainer(
+        getTestContainer(HddsProtos.LifeCycleState.OPEN, 400L));
+    reconContainerManager.addNewContainer(
+        getTestContainer(HddsProtos.LifeCycleState.OPEN, 401L));
+
+    for (long id = 400L; id <= 401L; id++) {
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.FINALIZE);
+      reconContainerManager.updateContainerState(
+          ContainerID.valueOf(id), HddsProtos.LifeCycleEvent.QUASI_CLOSE);
+    }
+    assertContainerCount(HddsProtos.LifeCycleState.QUASI_CLOSED, 2);
+
+    // The quasi-closed count is fetched independently via the dedicated 
endpoint.
+    // The unhealthy endpoint no longer carries this count.
+    Response response = containerEndpoint.getQuasiClosedContainers(1, 0L);
+    QuasiClosedContainersResponse result =
+        (QuasiClosedContainersResponse) response.getEntity();
+
+    assertEquals(2L, result.getQuasiClosedCount());
+  }
 }


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


Reply via email to