This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 971a4d17d fix(consumer): keep a failed progress read out of the empty
and healthy states (#4604)
971a4d17d is described below
commit 971a4d17d3973efba0be96d25bbcd5d7f121185f
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Sep 21 21:04:56 2026 +0800
fix(consumer): keep a failed progress read out of the empty and healthy
states (#4604)
`loadProgress` in `web/src/pages/instance/consumer.tsx` toasted on failure
but never recorded it, and the silent 2s refresh did not even toast. The empty
array it left in `progressByGroup` then fed two renderers: the progress table,
whose `locale.emptyText` states that the group is offline and shows no queue
progress, with every total at zero; and `analyzeConsumerGroupHealth`, which
with no queue lags produces no progress issues and reduces to a healthy verdict
with no deductions. One [...]
A new `progressErrorByGroup` map, symmetric with the existing
`subscriptionErrorByGroup`, is set on failure and cleared on success. The
progress table's empty text becomes "queue progress unavailable" and gains a
warning Alert, and the diagnostics tab warns that its verdict was computed
without queue progress. Health scoring itself is unchanged, which is how a
failed subscription read is treated today; downgrading the verdict would mean
adding an issue code to `consumerGroupDiagnostics`.
Maintainer edits on top of the contribution: merged after #4546 added a
request-ownership guard to `loadProgress`. The conflict was resolved so the new
`progressErrorByGroup` flag is written only by the request that still owns the
diagnostic context, and a stale failed read can no longer mark a group as
failed after a newer read succeeded.
Fixes #4600
---
.../pages/instance/__tests__/ConsumerPage.test.tsx | 39 ++++++++++++++++++++++
web/src/pages/instance/consumer.tsx | 39 ++++++++++++++++++++--
2 files changed, 75 insertions(+), 3 deletions(-)
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 7ffd8d61d..f77e4477c 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -662,6 +662,45 @@ describe('Consumer page', () => {
await waitFor(() =>
expect(screen.getAllByText('remote-topic').length).toBeGreaterThan(0));
});
+ it('does not report a failed progress load as an offline consumer group',
async () => {
+ vi.mocked(consumerService.getConsumerSubscriptions).mockResolvedValue([
+ {
+ topic: 'remote-topic',
+ expression: '*',
+ type: 'TAG',
+ filterMode: 'TAG',
+ consistency: 'consistent',
+ },
+ ]);
+ vi.mocked(consumerService.getConsumerProgress).mockRejectedValue(
+ new Error('broker unreachable'),
+ );
+ const user = userEvent.setup();
+ renderWithProviders(<ConsumerPage />);
+
+ await user.click(await screen.findByRole('button', { name: /详情/ }));
+ await waitFor(() =>
+
expect(consumerService.getConsumerProgress).toHaveBeenCalledWith('remote-cg',
'instance-1'),
+ );
+
+ // The empty-state text claims the group is offline; the read simply
failed.
+ await user.click(await screen.findByRole('tab', { name: /消费进度/ }));
+ const progressPanel = await screen.findByRole('tabpanel', { name: /消费进度/
});
+ await waitFor(() =>
+ expect(
+ within(progressPanel).queryByText('消费组不在线,暂无队列进度数据'),
+ ).not.toBeInTheDocument(),
+ );
+ expect(within(progressPanel).getByText('队列进度暂不可用')).toBeInTheDocument();
+ expect(
+ within(progressPanel).getByText('消费进度加载失败,无法判断消费组是否在线'),
+ ).toBeInTheDocument();
+
+ await user.click(await screen.findByRole('tab', { name: /健康诊断/ }));
+ const panel = await screen.findByRole('tabpanel', { name: /健康诊断/ });
+ await waitFor(() =>
expect(within(panel).queryByText(/消费进度加载失败/)).toBeInTheDocument());
+ });
+
it('shows group health diagnostics from subscriptions, progress and
clients', async () => {
const riskyGroup: ConsumerGroup = {
...group,
diff --git a/web/src/pages/instance/consumer.tsx
b/web/src/pages/instance/consumer.tsx
index 2927d3c01..02d1ade30 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -304,6 +304,7 @@ const ConsumerPageContent = ({
);
const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(false);
const [progressByGroup, setProgressByGroup] = useState<Record<string,
QueueProgress[]>>({});
+ const [progressErrorByGroup, setProgressErrorByGroup] =
useState<Record<string, boolean>>({});
const [stackModalOpen, setStackModalOpen] = useState(false);
const [stackLoading, setStackLoading] = useState(false);
const [selectedStack, setSelectedStack] = useState<ConsumerStackTrace |
null>(null);
@@ -445,10 +446,16 @@ const ConsumerPageContent = ({
const progress = await getConsumerProgress(groupName,
selectedInstanceId || undefined);
if (progressRequestIdRef.current[cacheKey] === requestId) {
setProgressByGroup((prev) => ({ ...prev, [cacheKey]: progress }));
+ setProgressErrorByGroup((prev) => ({ ...prev, [cacheKey]: false }));
}
} catch {
- if (progressRequestIdRef.current[cacheKey] === requestId && !silent) {
- message.error(t('consumer.fetchProgressFailed', { name: groupName
}));
+ // A failed read is not an empty result: the progress tab and the
health
+ // diagnosis must not present it as "the group is offline". A stale
request
+ // must not write the flag either, or it could mark a group as failed
after
+ // a newer read already succeeded.
+ if (progressRequestIdRef.current[cacheKey] === requestId) {
+ setProgressErrorByGroup((prev) => ({ ...prev, [cacheKey]: true }));
+ if (!silent) message.error(t('consumer.fetchProgressFailed', { name:
groupName }));
}
}
},
@@ -650,6 +657,7 @@ const ConsumerPageContent = ({
() => (selectedGroupName ? (progressByGroup[selectedDiagnosticKey] ?? [])
: []),
[progressByGroup, selectedDiagnosticKey, selectedGroupName],
);
+ const selectedProgressFailed =
Boolean(progressErrorByGroup[selectedDiagnosticKey]);
const progressTopicOptions = useMemo(
() => Array.from(new Set(selectedProgress.map((q) =>
q.topic).filter(Boolean))).sort(),
[selectedProgress],
@@ -679,6 +687,9 @@ const ConsumerPageContent = ({
: null,
[selectedGroup, selectedProgress, selectedSubscriptions],
);
+ // A failed progress read leaves the queues unknown; the diagnosis stays
useful
+ // for the loaded data but must never read as "everything is healthy".
+ const selectedGroupHealthIsPartial = selectedProgressFailed &&
selectedSubscriptions.length > 0;
const handlePreviewResetOffset = async () => {
if (!resetGroup || !resetTopic) {
@@ -1917,6 +1928,14 @@ const ConsumerPageContent = ({
/>
)}
+ {selectedGroupHealthIsPartial && (
+ <Alert
+ type="warning"
+ showIcon
+ message="消费进度加载失败,诊断未包含队列进度。"
+ />
+ )}
+
<Row gutter={16}>
<Col span={6}>
<Card size="small" style={{ borderRadius: 8 }}>
@@ -2039,6 +2058,15 @@ const ConsumerPageContent = ({
),
children: (
<div>
+ {selectedProgressFailed && (
+ <Alert
+ type="warning"
+ showIcon
+ style={{ marginBottom: 12 }}
+ message="消费进度加载失败,无法判断消费组是否在线"
+ description="队列进度与堆积统计暂不可用,请稍后重试。"
+ />
+ )}
{progressTopicOptions.length > 0 && (
<Flex align="center" gap={8} style={{ marginBottom: 12
}}>
<Text type="secondary">Topic 筛选:</Text>
@@ -2106,7 +2134,12 @@ const ConsumerPageContent = ({
size="small"
tableLayout="fixed"
scroll={{ x: tableScrollX(queueColumns), y: 380 }}
- locale={{ emptyText: '消费组不在线,暂无队列进度数据' }}
+ locale={{
+ emptyText: selectedProgressFailed
+ ? // A failed read must not assert that the group is
offline.
+ '队列进度暂不可用'
+ : '消费组不在线,暂无队列进度数据',
+ }}
/>
</div>
),