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 >
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]