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 4cf6ca85f6f7..4080c857750b 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 @@ -72,6 +72,7 @@ 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.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; @@ -447,6 +448,11 @@ private Response getUnhealthyContainersFromSchema( for (UnhealthyContainersSummary s : summary) { response.setSummaryCount(s.getContainerState(), s.getCount()); } + + // Also include the quasi-closed count in the summary for the frontend Highlights tab + long quasiClosedCount = containerManager.getContainerStateCount(HddsProtos.LifeCycleState.QUASI_CLOSED); + response.setQuasiClosedCount(quasiClosedCount); + return Response.ok(response).build(); } @@ -812,4 +818,54 @@ 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 prevKey the containerID after which results are returned. + * @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_PREVKEY) long prevKey) { + + List containers = containerManager.getContainers( + ContainerID.valueOf(prevKey + 1), limit, HddsProtos.LifeCycleState.QUASI_CLOSED); + + List metaList = containers.stream() + .map(ci -> { + long containerID = ci.getContainerID(); + int requiredNodes = 0; + try { + requiredNodes = ci.getReplicationConfig().getRequiredNodes(); + } catch (Exception e) { + LOG.warn("Could not get required nodes for container {}", containerID, e); + } + List replicas = containerManager.getLatestContainerHistory(containerID, requiredNodes); + + UnhealthyContainerMetadata metadata = new UnhealthyContainerMetadata( + containerID, + "QUASI_CLOSED", + ci.getStateEnterTime() != null ? ci.getStateEnterTime().toEpochMilli() : 0L, + requiredNodes, + replicas.size(), + replicas.size() - requiredNodes, + "", + ci.getNumberOfKeys(), + ci.getPipelineID() != null ? ci.getPipelineID().getId() : null, + replicas + ); + return metadata; + }) + .collect(Collectors.toList()); + + long firstKey = metaList.isEmpty() ? prevKey : metaList.get(0).getContainerID(); + long lastKey = metaList.isEmpty() ? prevKey : metaList.get(metaList.size() - 1).getContainerID(); + long total = containerManager.getContainerStateCount(HddsProtos.LifeCycleState.QUASI_CLOSED); + + return Response.ok(new QuasiClosedContainersResponse(total, firstKey, lastKey, metaList)).build(); + } } 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 000000000000..23d6c81c66e6 --- /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; + +/** + * Class that represents the API Response structure for Quasi-Closed Containers. + */ +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 containers; + + public QuasiClosedContainersResponse() { + } + + public QuasiClosedContainersResponse(long quasiClosedCount, long firstKey, long lastKey, + List 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 getContainers() { + return containers; + } + + public void setContainers(List containers) { + this.containers = containers; + } +} 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..b88978bb80af 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 @@ -75,6 +75,22 @@ public UnhealthyContainerMetadata(UnhealthyContainers rec, this.keys = keyCount; } + public UnhealthyContainerMetadata(long containerID, String containerState, + long unhealthySince, long expectedReplicaCount, long actualReplicaCount, + long replicaDeltaCount, String reason, long keys, UUID pipelineID, + List replicas) { + this.containerID = containerID; + this.containerState = containerState; + this.unhealthySince = unhealthySince; + this.expectedReplicaCount = expectedReplicaCount; + this.actualReplicaCount = actualReplicaCount; + this.replicaDeltaCount = replicaDeltaCount; + this.reason = reason; + this.keys = keys; + this.pipelineID = pipelineID; + this.replicas = replicas; + } + // Default constructor, used by jackson lib for object deserialization. public UnhealthyContainerMetadata() { } 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..59bde745ca4e 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 @@ -55,6 +55,12 @@ public class UnhealthyContainersResponse { @JsonProperty("replicaMismatchCount") private long replicaMismatchCount = 0; + /** + * Total count of quasi-closed containers. + */ + @JsonProperty("quasiClosedCount") + private long quasiClosedCount = 0; + /** * The smallest container ID in the current response batch. * Used for pagination to determine the lower bound for the next page. @@ -125,6 +131,14 @@ public long getReplicaMismatchCount() { return replicaMismatchCount; } + public long getQuasiClosedCount() { + return quasiClosedCount; + } + + public void setQuasiClosedCount(long quasiClosedCount) { + this.quasiClosedCount = quasiClosedCount; + } + public long getLastKey() { return lastKey; } 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 1b76c0371aad..92b521259be0 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 = ({ 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 2b1ca3d24994..fb4da91d11f1 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 @@ -50,6 +50,7 @@ const TAB_STATE_MAP: Record = { '3': 'OVER_REPLICATED', '4': 'MIS_REPLICATED', '5': 'REPLICA_MISMATCH', + '6': 'QUASI_CLOSED', }; const SearchableColumnOpts = [{ @@ -85,6 +86,7 @@ const Containers: React.FC<{}> = () => { overReplicatedCount: 0, misReplicatedCount: 0, replicaMismatchCount: 0, + quasiClosedCount: 0, }); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [tabStates, setTabStates] = useState>({ @@ -93,6 +95,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({}); const [selectedColumns, setSelectedColumns] = useState(defaultColumns); @@ -140,8 +143,12 @@ const Containers: React.FC<{}> = () => { })); try { + const endpoint = tabKey === '6' + ? `/api/v1/containers/quasiClosed` + : `/api/v1/containers/unhealthy/${containerStateName}`; + const response = await fetchData( - `/api/v1/containers/unhealthy/${containerStateName}`, + endpoint, 'GET', { limit: fetchSize, minContainerId } ); @@ -176,11 +183,12 @@ const Containers: React.FC<{}> = () => { // Summary counts are returned by every tab endpoint. 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, + quasiClosedCount: response.quasiClosedCount ?? prev.quasiClosedCount, lastUpdated: Number(moment()), })); } catch (error) { @@ -247,6 +255,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); @@ -260,6 +269,7 @@ const Containers: React.FC<{}> = () => { '3': { ...DEFAULT_TAB_STATE }, '4': { ...DEFAULT_TAB_STATE }, '5': { ...DEFAULT_TAB_STATE }, + '6': { ...DEFAULT_TAB_STATE }, }); fetchTabData(selectedTab, 0, pageSize); clusterState.refetch(); @@ -276,6 +286,7 @@ const Containers: React.FC<{}> = () => { overReplicatedCount, misReplicatedCount, replicaMismatchCount, + quasiClosedCount, } = state; const currentTabState = tabStates[selectedTab]; @@ -310,6 +321,10 @@ const Containers: React.FC<{}> = () => { Mismatched Replicas
{replicaMismatchCount ?? 'N/A'} +
+ Quasi Closed
+ {quasiClosedCount ?? 'N/A'} +
); @@ -363,10 +378,10 @@ const Containers: React.FC<{}> = () => { handleTabChange(activeKey)}> - {(['1','2','3','4','5'] as const).map((key) => ( + {(['1','2','3','4','5','6'] as const).map((key) => ( + tab={['Missing','Under-Replicated','Over-Replicated','Mis-Replicated','Mismatched Replicas','Quasi Closed'][Number(key)-1]}> = () => { hasPrevPage={tabStates[key].pageHistory.length > 0} pageSize={pageSize} onPageSizeChange={handlePageSizeChange} + sinceColumnTitle={key === '6' ? 'State Enter Time' : 'Unhealthy Since'} /> ))} 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 3b51f36af184..38342f50e61c 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 @@ -72,6 +72,8 @@ export type ContainersPaginationResponse = { overReplicatedCount: number; misReplicatedCount: number; replicaMismatchCount: number; + quasiClosedCount?: number; + totalCount?: number; } export type TabPaginationState = { @@ -98,6 +100,7 @@ export type ContainerTableProps = { hasPrevPage: boolean; pageSize: number; onPageSizeChange: (newSize: number) => void; + sinceColumnTitle?: string; } @@ -121,4 +124,5 @@ export type ContainerState = { overReplicatedCount: number; misReplicatedCount: number; replicaMismatchCount: number; + quasiClosedCount: 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 cc2a02d0da36..46c8d22d2e59 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,7 @@ 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.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 +1955,172 @@ 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 containers = + new ArrayList<>(result.getContainers()); + assertEquals(3, containers.size()); + containers.forEach(c -> { + assertEquals("QUASI_CLOSED", c.getContainerState()); + // 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()); + + UnhealthyContainerMetadata meta = result.getContainers().get(0); + assertEquals(210L, meta.getContainerID()); + assertEquals(2L, meta.getActualReplicaCount()); + assertEquals(2, meta.getReplicas().size()); + + Set 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 page1Ids = page1.getContainers().stream() + .map(UnhealthyContainerMetadata::getContainerID) + .collect(Collectors.toSet()); + Set page2Ids = page2.getContainers().stream() + .map(UnhealthyContainerMetadata::getContainerID) + .collect(Collectors.toSet()); + assertTrue(Collections.disjoint(page1Ids, page2Ids)); + } + + @Test + public void testGetQuasiClosedCountInUnhealthySummary() 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 unhealthy summary endpoint must include the quasi-closed count + // so the Highlights card is populated on first page load (tab 1 fetch). + Response response = containerEndpoint.getUnhealthyContainers(1000, 0, 0); + UnhealthyContainersResponse summary = + (UnhealthyContainersResponse) response.getEntity(); + + assertEquals(2L, summary.getQuasiClosedCount()); + } }