This is an automated email from the ASF dual-hosted git repository.
spacemonkd 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 ed9e5ff1c19 HDDS-15272. Add error handling for SCM failure in cluster
capacity (#10411)
ed9e5ff1c19 is described below
commit ed9e5ff1c19ae0b8cae2185edf333f72a1c32748
Author: Abhishek Pal <[email protected]>
AuthorDate: Wed Jun 3 13:24:24 2026 +0530
HDDS-15272. Add error handling for SCM failure in cluster capacity (#10411)
---
.../src/__tests__/capacity/Capacity.test.tsx | 53 ++++++++++++++++++++++
.../src/v2/pages/capacity/capacity.less | 18 ++++++++
.../src/v2/pages/capacity/capacity.tsx | 17 ++++++-
.../pages/capacity/components/CapacityDetail.tsx | 49 ++++++++++++++------
.../capacity/components/CapacityDetailError.tsx | 39 ++++++++++++++++
5 files changed, 159 insertions(+), 17 deletions(-)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
index 94109adb274..d6f878aaad0 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
@@ -18,9 +18,11 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
+import { rest } from 'msw';
import Capacity from '@/v2/pages/capacity/capacity';
import { capacityServer } from '@tests/mocks/capacityMocks/capacityServer';
+import * as mockResponses from
'@tests/mocks/capacityMocks/capacityResponseMocks';
vi.mock('@/components/autoReloadPanel/autoReloadPanel', () => ({
default: () => <div data-testid="auto-reload-panel" />,
@@ -92,4 +94,55 @@ describe('Capacity Page', () => {
);
expect(datanodeCard).toHaveTextContent(/FREE SPACE\s*3\s*KB/i);
});
+
+ test('shows scm-only error state when SCM pending deletion returns sentinel
failure values', async () => {
+ capacityServer.use(
+ rest.get('api/v1/pendingDeletion', (req, res, ctx) => {
+ const component = req.url.searchParams.get('component');
+ switch (component) {
+ case 'scm':
+ return res(
+ ctx.status(200),
+ ctx.json({
+ totalBlocksize: -1,
+ totalReplicatedBlockSize: -1,
+ totalBlocksCount: -1
+ })
+ );
+ case 'om':
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.OmPendingDeletion)
+ );
+ case 'dn':
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.DnPendingDeletion)
+ );
+ default:
+ return res(
+ ctx.status(400),
+ ctx.json({ message: 'Unsupported pending deletion component.' })
+ );
+ }
+ })
+ );
+
+ render(<Capacity />);
+
+ const pendingDeletionTitle = await screen.findByText('Pending Deletion');
+ const pendingDeletionCard = pendingDeletionTitle.closest('.ant-card');
+ expect(pendingDeletionCard).not.toBeNull();
+ if (!pendingDeletionCard) {
+ return;
+ }
+
+ await waitFor(() =>
+ expect(pendingDeletionCard).toHaveTextContent(/OZONE MANAGER\s*2\s*KB/i)
+ );
+ expect(pendingDeletionCard).toHaveTextContent(/DATANODES\s*3\s*KB/i);
+ expect(pendingDeletionCard).toHaveTextContent(/STORAGE CONTAINER
MANAGER\s*N\/A/i);
+ expect(await
screen.findByTestId('pending-deletion-scm-error')).toBeInTheDocument();
+ await waitFor(() =>
expect(screen.getAllByTestId('echart')).toHaveLength(4));
+ });
});
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
index 5a1e9b0443f..a04f3482ded 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
@@ -92,3 +92,21 @@
color: #5a656d;
margin-left: 15px;
}
+
+.capacity-detail-error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ margin: 14px auto;
+ color: #5a656d;
+}
+
+.capacity-detail-error-icon {
+ color: #f47b2d;
+ font-size: 24px;
+}
+
+.capacity-detail-error-message {
+ font-size: 13px;
+}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
index 307f5d1177a..b2e8e0522a0 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
@@ -303,6 +303,16 @@ const Capacity: React.FC<object> = () => {
</span>
);
+ const hasSCMPendingDeletionError = (
+ scmPendingDeletes.data.totalBlocksize < 0
+ || scmPendingDeletes.data.totalReplicatedBlockSize < 0
+ || scmPendingDeletes.data.totalBlocksCount < 0
+ );
+
+ const scmReplicatedPendingDeletionSize = hasSCMPendingDeletionError
+ ? 0
+ : scmPendingDeletes.data.totalReplicatedBlockSize;
+
return (
<>
<div className='page-header-v2'>
@@ -381,7 +391,7 @@ const Capacity: React.FC<object> = () => {
),
value: (
omPendingDeletes.data.totalSize
- + scmPendingDeletes.data.totalReplicatedBlockSize
+ + scmReplicatedPendingDeletionSize
+ (dnPendingDeletes.data.totalPendingDeletionSize ?? 0)
),
color: "#10073b"
@@ -406,7 +416,10 @@ const Capacity: React.FC<object> = () => {
}]
}, {
title: 'STORAGE CONTAINER MANAGER',
- size: scmPendingDeletes.data.totalReplicatedBlockSize,
+ size: hasSCMPendingDeletionError ? 0 :
scmPendingDeletes.data.totalReplicatedBlockSize,
+ hasError: hasSCMPendingDeletionError,
+ errorMessage: 'SCM pending deletion details are currently
unavailable.',
+ errorTestId: 'pending-deletion-scm-error',
breakdown: [{
label: 'BLOCKS',
value: scmPendingDeletes.data.totalReplicatedBlockSize,
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
index 612f9f986fe..f17c52b6004 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
@@ -19,6 +19,7 @@
import { EChart } from '@/components/eChart/eChart';
import { GraphLegendIcon } from '@/utils/themeIcons';
import { cardHeadStyle, statisticValueStyle } from
'@/v2/pages/capacity/constants/styles.constants';
+import CapacityDetailError from
'@/v2/pages/capacity/components/CapacityDetailError';
import { Segment } from '@/v2/types/capacity.types';
import { DownloadOutlined } from '@ant-design/icons';
import { Card, Divider, Row, Select, Spin, Statistic } from 'antd';
@@ -30,6 +31,9 @@ type DataDetailItem = {
size: number;
breakdown: Segment[];
loading?: boolean;
+ hasError?: boolean;
+ errorMessage?: string;
+ errorTestId?: string;
}
type CapacityDetailProps = {
@@ -159,25 +163,40 @@ const CapacityDetail: React.FC<CapacityDetailProps> = (
<div key={`data-detail-${data.title}-${idx}`}
className='data-detail-item'>
<Statistic
title={data.title}
- value={size[0]}
- suffix={size[1]}
+ value={data.hasError ? 'N/A' : size[0]}
+ suffix={data.hasError ? undefined : size[1]}
valueStyle={statisticValueStyle}
className='data-detail-statistic'
loading={data.loading}
/>
- {!data.loading && <Row
className='data-detail-breakdown-container'>
- {data.breakdown.map((item, idx) => (
- <div
key={`data-defailt-breakdown-${item.label}-${idx}`}
className='data-detail-breakdown-item'>
- <GraphLegendIcon color={item.color} height={12} />
- <span
className="data-detail-breakdown-label">{item.label}</span>
- <span
className="data-detail-breakdown-value">{filesize(item.value, {round:
1})}</span>
- </div>
- ))}
- <EChart
- option={getEchartOptions(data.title, data)}
- style={{ height: '40px', width: '100%', margin: '10px
0px' }} />
- {idx < dataDetails.length - 1 && <Divider />}
- </Row>}
+ {!data.loading
+ && (
+ data.hasError
+ ? (
+ <>
+ <CapacityDetailError
+ message={data.errorMessage}
+ testId={data.errorTestId}
+ />
+ {idx < dataDetails.length - 1 && <Divider />}
+ </>
+ )
+ : (
+ <Row className='data-detail-breakdown-container'>
+ {data.breakdown.map((item, idx) => (
+ <div
key={`data-defailt-breakdown-${item.label}-${idx}`}
className='data-detail-breakdown-item'>
+ <GraphLegendIcon color={item.color}
height={12} />
+ <span
className="data-detail-breakdown-label">{item.label}</span>
+ <span
className="data-detail-breakdown-value">{filesize(item.value, {round:
1})}</span>
+ </div>
+ ))}
+ <EChart
+ option={getEchartOptions(data.title, data)}
+ style={{ height: '40px', width: '100%',
margin: '10px 0px' }} />
+ {idx < dataDetails.length - 1 && <Divider />}
+ </Row>
+ )
+ )}
</div>
)
})}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetailError.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetailError.tsx
new file mode 100644
index 00000000000..5f02fc5afe3
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetailError.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+import { DisconnectOutlined } from '@ant-design/icons';
+import React from 'react';
+
+type CapacityDetailErrorProps = {
+ message?: string;
+ testId?: string;
+}
+
+const CapacityDetailError: React.FC<CapacityDetailErrorProps> = ({
+ message = 'Pending deletion details are currently unavailable.',
+ testId
+}) => {
+ return (
+ <div className='capacity-detail-error' data-testid={testId}>
+ <DisconnectOutlined className='capacity-detail-error-icon' />
+ <span className='capacity-detail-error-message'>{message}</span>
+ </div>
+ );
+};
+
+export default CapacityDetailError;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]