This is an automated email from the ASF dual-hosted git repository.
priyeshkaratha 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 3b40495c46a HDDS-15782. [Recon] Poll DN pending-deletion scan to
completion when Auto Refresh is off (#10714)
3b40495c46a is described below
commit 3b40495c46a02cc698d53dd17ba3f74df95c0c11
Author: Priyesh Karatha <[email protected]>
AuthorDate: Wed Jul 22 21:50:51 2026 +0530
HDDS-15782. [Recon] Poll DN pending-deletion scan to completion when Auto
Refresh is off (#10714)
---
.../src/__tests__/capacity/Capacity.test.tsx | 144 ++++++++++++++++++++-
.../src/v2/pages/capacity/capacity.tsx | 62 +++++++--
2 files changed, 189 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 037fcd093cd..7e2914e78be 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
@@ -25,7 +25,13 @@ 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" />,
+ default: (props: { onReload?: () => void }) => (
+ <div data-testid="auto-reload-panel">
+ <button data-testid="manual-reload" onClick={() => props.onReload?.()}>
+ reload
+ </button>
+ </div>
+ ),
}));
vi.mock('@/components/eChart/eChart', () => ({
EChart: () => <div data-testid="echart" />,
@@ -49,9 +55,9 @@ describe('Capacity Page', () => {
return;
}
await waitFor(() =>
- expect(ozoneCapacityCard).toHaveTextContent(/TOTAL\s*10\s*KB/i)
+ expect(ozoneCapacityCard).toHaveTextContent(/TOTAL CAPACITY\s*10\s*KB/i)
);
- expect(ozoneCapacityCard).toHaveTextContent(/OZONE USED SPACE\s*4\s*KB/i);
+ expect(ozoneCapacityCard).toHaveTextContent(/USED SPACE\s*4\s*KB/i);
expect(ozoneCapacityCard).toHaveTextContent(/OTHER USED SPACE\s*2\s*KB/i);
expect(ozoneCapacityCard).toHaveTextContent(/CONTAINER
PRE-ALLOCATED\s*1\s*KB/i);
expect(ozoneCapacityCard).toHaveTextContent(/REMAINING SPACE\s*4\s*KB/i);
@@ -63,7 +69,7 @@ describe('Capacity Page', () => {
return;
}
await waitFor(() =>
- expect(ozoneUsedSpaceCard).toHaveTextContent(/PENDING
DELETION\s*6\s*KB/i)
+ expect(ozoneUsedSpaceCard).toHaveTextContent(/PENDING
DELETION\s*7\s*KB/i)
);
});
@@ -80,7 +86,7 @@ describe('Capacity Page', () => {
expect(pendingDeletionCard).toHaveTextContent(/OZONE MANAGER\s*2\s*KB/i)
);
expect(pendingDeletionCard)
- .toHaveTextContent(/STORAGE CONTAINER MANAGER\s*1\s*KB/i);
+ .toHaveTextContent(/STORAGE CONTAINER MANAGER\s*2\s*KB/i);
expect(pendingDeletionCard).toHaveTextContent(/DATANODES\s*3\s*KB/i);
const downloadLink = await screen.findByText('Download Insights');
@@ -307,4 +313,132 @@ describe('Capacity Page', () => {
);
expect(screen.queryByRole('option', { name: 'dn-17'
})).not.toBeInTheDocument();
});
+
+ // Interval constants mirrored from capacity.tsx / autoReload.constants.
+ const PENDING_POLL_INTERVAL = 5 * 1000;
+ const AUTO_RELOAD_INTERVAL = 60 * 1000;
+
+ type EndpointCounts = { storage: number; scm: number; om: number; dn: number
};
+
+ // Installs handlers that count how often each endpoint is hit and serves the
+ // supplied DN scan statuses in order (clamped to the last entry afterwards).
+ const setupCountingHandlers = (dnStatuses: string[]) => {
+ const counts: EndpointCounts = { storage: 0, scm: 0, om: 0, dn: 0 };
+ capacityServer.use(
+ rest.get('api/v1/storageDistribution', (_req, res, ctx) => {
+ counts.storage++;
+ return res(ctx.status(200),
ctx.json(mockResponses.StorageDistribution));
+ }),
+ rest.get('api/v1/pendingDeletion', (req, res, ctx) => {
+ const component = req.url.searchParams.get('component');
+ if (component === 'dn') {
+ const status = dnStatuses[Math.min(counts.dn, dnStatuses.length -
1)];
+ counts.dn++;
+ return res(
+ ctx.status(200),
+ ctx.json({ ...mockResponses.DnPendingDeletion, status })
+ );
+ }
+ if (component === 'scm') {
+ counts.scm++;
+ return res(ctx.status(200),
ctx.json(mockResponses.ScmPendingDeletion));
+ }
+ if (component === 'om') {
+ counts.om++;
+ return res(ctx.status(200),
ctx.json(mockResponses.OmPendingDeletion));
+ }
+ return res(ctx.status(400), ctx.json({ message: 'Unsupported pending
deletion component.' }));
+ })
+ );
+ return counts;
+ };
+
+ test('Auto Refresh off: a manual refresh drives the DN scan, polling only
the DN endpoint until it finishes, then syncs the other endpoints', async () =>
{
+ // Auto Refresh disabled before mount, so useAutoReload never starts its
timer.
+ sessionStorage.setItem('autoReloadEnabled', 'false');
+
+ // Mount read is FINISHED (no scan running). The manual refresh kicks off a
+ // scan: the next reads are IN_PROGRESS, IN_PROGRESS, then FINISHED.
+ const counts = setupCountingHandlers([
+ 'FINISHED', // mount
+ 'IN_PROGRESS', // manual refresh
+ 'IN_PROGRESS', // poll #1
+ 'FINISHED' // poll #2
+ ]);
+
+ vi.useFakeTimers();
+ try {
+ render(<Capacity />);
+
+ // Flush the initial mount fetch: everything fetched exactly once.
+ await vi.advanceTimersByTimeAsync(50);
+ expect(counts).toEqual({ storage: 1, scm: 1, om: 1, dn: 1 });
+
+ // Scan is FINISHED, so nothing is polled while idle.
+ await vi.advanceTimersByTimeAsync(AUTO_RELOAD_INTERVAL * 2);
+ expect(counts).toEqual({ storage: 1, scm: 1, om: 1, dn: 1 });
+
+ // Manual reload -> full refresh of all four endpoints; DN comes back
+ // IN_PROGRESS, which starts the DN-only poll.
+ fireEvent.click(screen.getByTestId('manual-reload'));
+ await vi.advanceTimersByTimeAsync(50);
+ expect(counts).toEqual({ storage: 2, scm: 2, om: 2, dn: 2 });
+
+ // While IN_PROGRESS only the DN endpoint is polled; the others stay put.
+ await vi.advanceTimersByTimeAsync(PENDING_POLL_INTERVAL);
+ expect(counts).toEqual({ storage: 2, scm: 2, om: 2, dn: 3 });
+
+ // Next poll returns FINISHED -> DN polling stops and the other three
+ // endpoints are synced exactly once.
+ await vi.advanceTimersByTimeAsync(PENDING_POLL_INTERVAL);
+ expect(counts).toEqual({ storage: 3, scm: 3, om: 3, dn: 4 });
+
+ // No further polling once finished, and no periodic refresh while off.
+ await vi.advanceTimersByTimeAsync(AUTO_RELOAD_INTERVAL * 2);
+ expect(counts).toEqual({ storage: 3, scm: 3, om: 3, dn: 4 });
+ } finally {
+ vi.useRealTimers();
+ sessionStorage.removeItem('autoReloadEnabled');
+ }
+ });
+
+ test('Auto Refresh on: refreshes all endpoints on the interval, polls only
the DN endpoint while a scan runs, and syncs the others when it finishes',
async () => {
+ // Auto Refresh enabled (default). useAutoReload runs a full refresh every
60s.
+ sessionStorage.setItem('autoReloadEnabled', 'true');
+
+ // A scan is already running at mount: IN_PROGRESS, IN_PROGRESS, then
FINISHED.
+ const counts = setupCountingHandlers([
+ 'IN_PROGRESS', // mount
+ 'IN_PROGRESS', // poll #1
+ 'FINISHED' // poll #2
+ ]);
+
+ vi.useFakeTimers();
+ try {
+ render(<Capacity />);
+
+ // Flush the initial mount fetch: everything fetched once, scan
IN_PROGRESS.
+ await vi.advanceTimersByTimeAsync(50);
+ expect(counts).toEqual({ storage: 1, scm: 1, om: 1, dn: 1 });
+
+ // While IN_PROGRESS only the DN endpoint is polled.
+ await vi.advanceTimersByTimeAsync(PENDING_POLL_INTERVAL);
+ expect(counts).toEqual({ storage: 1, scm: 1, om: 1, dn: 2 });
+
+ // Next poll returns FINISHED -> DN polling stops and the others sync
once.
+ await vi.advanceTimersByTimeAsync(PENDING_POLL_INTERVAL);
+ expect(counts).toEqual({ storage: 2, scm: 2, om: 2, dn: 3 });
+
+ // No DN-only polling once finished.
+ await vi.advanceTimersByTimeAsync(PENDING_POLL_INTERVAL * 4);
+ expect(counts).toEqual({ storage: 2, scm: 2, om: 2, dn: 3 });
+
+ // At the auto-refresh interval, all four endpoints are refreshed
together.
+ await vi.advanceTimersByTimeAsync(AUTO_RELOAD_INTERVAL);
+ expect(counts).toEqual({ storage: 3, scm: 3, om: 3, dn: 4 });
+ } finally {
+ vi.useRealTimers();
+ sessionStorage.removeItem('autoReloadEnabled');
+ }
+ });
});
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 d6695af6be3..152fe43ff30 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
@@ -38,7 +38,6 @@ import { useApiData } from '@/v2/hooks/useAPIData.hook';
import * as CONSTANTS from '@/v2/constants/capacity.constants';
import { UtilizationResponse, SCMPendingDeletion, OMPendingDeletion,
DNPendingDeletion, DataNodeUsage } from '@/v2/types/capacity.types';
import { useAutoReload } from '@/v2/hooks/useAutoReload.hook';
-import { AUTO_RELOAD_INTERVAL_DEFAULT } from
'@/constants/autoReload.constants';
type CapacityState = {
lastUpdated: number;
@@ -202,19 +201,58 @@ const Capacity: React.FC<object> = () => {
}
};
- // Adjust the polling interval based on DN scan status:
- // fast (5s) while a scan is running, normal (60s) once finished.
- // Honors the auto-reload toggle: if polling is OFF, do nothing.
+ // --- DN pending-deletion scan polling
--------------------------------------
+ // The periodic full refresh (all four endpoints) is driven by useAutoReload
+ // when Auto Refresh is enabled, and by the manual reload button otherwise.
+ // On top of that, whenever a DN scan is reported IN_PROGRESS we poll ONLY
the
+ // DN endpoint at a fast cadence until it finishes, then refetch the other
+ // endpoints once so the aggregate pending-deletion numbers reflect the final
+ // DN data. This runs regardless of the Auto Refresh toggle, because a
running
+ // scan must be driven to completion either way.
+
+ // Refetch only the DN endpoint. Kept in a ref so the interval below always
+ // reads the current `loading` value instead of a stale closure (the effect
+ // does not re-run while the status stays IN_PROGRESS).
+ const pollDnScanRef = React.useRef<() => void>(() => {});
+ pollDnScanRef.current = () => {
+ if (!dnPendingDeletes.loading) {
+ dnPendingDeletes.refetch();
+ }
+ };
+
+ // Refetch everything except the DN endpoint, to re-sync the OM / SCM /
storage
+ // numbers with the DN data once the scan has finished.
+ const syncNonDnDataRef = React.useRef<() => void>(() => {});
+ syncNonDnDataRef.current = () => {
+ storageDistribution.refetch();
+ scmPendingDeletes.refetch();
+ omPendingDeletes.refetch();
+ setState({ lastUpdated: Number(moment()) });
+ };
+
+ // True while we are actively driving a DN scan, so that we sync the other
+ // endpoints exactly once when it transitions to FINISHED — and not on the
+ // initial load, where all four endpoints were already fetched together.
+ const isDrivingDnScanRef = React.useRef(false);
+
React.useEffect(() => {
- if (!autoReload.isPolling) {
- return;
+ const scanInProgress = dnPendingDeletes.data.status === "IN_PROGRESS";
+
+ if (scanInProgress) {
+ isDrivingDnScanRef.current = true;
+ const timer = window.setInterval(() => pollDnScanRef.current(),
PENDING_POLL_INTERVAL);
+ return () => clearInterval(timer);
}
- autoReload.startPolling(
- dnPendingDeletes.data.status === "FINISHED"
- ? AUTO_RELOAD_INTERVAL_DEFAULT
- : PENDING_POLL_INTERVAL
- );
- }, [dnPendingDeletes.data.status, autoReload.isPolling]); //
eslint-disable-line react-hooks/exhaustive-deps
+
+ // Reached a terminal state (FINISHED / FAILED). If we were driving a scan,
+ // sync the other endpoints once (only on success) and stop tracking it.
+ if (isDrivingDnScanRef.current) {
+ isDrivingDnScanRef.current = false;
+ if (dnPendingDeletes.data.status === "FINISHED") {
+ syncNonDnDataRef.current();
+ }
+ }
+ }, [dnPendingDeletes.data.status]); // eslint-disable-line
react-hooks/exhaustive-deps
const dnReportStatus = (
(dnPendingDeletes.data.totalNodeQueriesFailed ?? 0) > 0
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]