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 8457a929d fix(dlq): isolate detail resend state across instance 
switches (#4487)
8457a929d is described below

commit 8457a929dbf2829ecfc126bc07e1f15e27d76d8a
Author: zmuxuny <[email protected]>
AuthorDate: Mon Sep 21 12:06:59 2026 +0800

    fix(dlq): isolate detail resend state across instance switches (#4487)
    
    The dead-letter detail drawer shared its in-flight flag and request 
generation
    with the group retry flow, so a resend started under one instance could 
resolve
    after an instance switch and write into the new scope, and the two paths 
could
    not tell each other apart.
    
    Give the detail resend its own request-id and in-flight refs, make the two 
paths
    mutually exclusive, and reset the drawer state (open flag, messages, page,
    selection, loading, error) when the instance changes or the drawer closes.
---
 web/src/pages/instance/__tests__/DLQPage.test.tsx | 96 +++++++++++++++++++++++
 web/src/pages/instance/dlq.tsx                    | 58 +++++++++++---
 2 files changed, 142 insertions(+), 12 deletions(-)

diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx 
b/web/src/pages/instance/__tests__/DLQPage.test.tsx
index 29cf71068..5025b85ba 100644
--- a/web/src/pages/instance/__tests__/DLQPage.test.tsx
+++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx
@@ -259,6 +259,102 @@ describe('DLQ page', () => {
     );
   });
 
+  it('does not let an old-instance detail resend overwrite the new instance 
drawer', async () => {
+    let resolveResend!: (result: DLQResendResult) => void;
+    let resolveSecondDetail!: (page: DLQMessagePage) => void;
+    let firstInstanceDetailCalls = 0;
+    vi.mocked(messageService.listDLQGroups)
+      .mockResolvedValueOnce(pageOf([dlqGroup]))
+      .mockResolvedValueOnce(pageOf([secondDlqGroup]));
+    vi.mocked(messageService.listDLQMessages).mockImplementation((params) => {
+      if (params.instanceId === 'instance-2') {
+        return new Promise<DLQMessagePage>((resolve) => {
+          resolveSecondDetail = resolve;
+        });
+      }
+      firstInstanceDetailCalls += 1;
+      return Promise.resolve({
+        items: [
+          {
+            msgId: firstInstanceDetailCalls === 1 ? 'instance-a-initial' : 
'stale-a-after-resend',
+            topic: 'orders',
+            queueId: 0,
+            offset: firstInstanceDetailCalls,
+            storeTime: 1_700_000_000_000,
+            keys: 'instance-a-key',
+            body: 'a-body',
+            bodyBase64: null,
+            properties: {},
+            propertiesTruncated: false,
+          },
+        ],
+        total: 1,
+        page: 1,
+        size: 20,
+      });
+    });
+    vi.mocked(messageService.resendDLQSelected).mockImplementationOnce(
+      () =>
+        new Promise<DLQResendResult>((resolve) => {
+          resolveResend = resolve;
+        }),
+    );
+    const user = userEvent.setup();
+    renderWithProviders(<DLQPage />);
+
+    const firstGroupRow = (await screen.findByText('cg-order')).closest('tr');
+    if (!firstGroupRow) throw new Error('first DLQ group row not found');
+    await user.click(within(firstGroupRow).getByRole('button', { name: /消息明细/ 
}));
+    const firstMessageRow = (await 
screen.findByText('instance-a-initial')).closest('tr');
+    if (!firstMessageRow) throw new Error('first DLQ message row not found');
+    await user.click(within(firstMessageRow).getByRole('checkbox'));
+    await user.click(screen.getByRole('button', { name: /批量重发选中/ }));
+    expect(messageService.resendDLQSelected).toHaveBeenCalledWith(
+      expect.objectContaining({ instanceId: 'instance-1', groupName: 
'cg-order' }),
+    );
+
+    await user.click(screen.getAllByRole('combobox')[0]);
+    await user.click(
+      await screen.findByText('instance-2', { selector: 
'.ant-select-item-option-content' }),
+    );
+    const secondGroupRow = (await 
screen.findByText('-cg-"payment"')).closest('tr');
+    if (!secondGroupRow) throw new Error('second DLQ group row not found');
+    await user.click(within(secondGroupRow).getByRole('button', { name: /消息明细/ 
}));
+    expect(await screen.findByText('DLQ 消息明细 · 
-cg-"payment"')).toBeInTheDocument();
+    await waitFor(() =>
+      expect(messageService.listDLQMessages).toHaveBeenCalledWith(
+        expect.objectContaining({ instanceId: 'instance-2', groupName: 
'-cg-"payment"' }),
+      ),
+    );
+
+    await act(async () => resolveResend({ matched: 1, resent: 1, failed: 0, 
outcome: 'SUCCESS' }));
+    expect(firstInstanceDetailCalls).toBe(1);
+    await act(async () =>
+      resolveSecondDetail({
+        items: [
+          {
+            msgId: 'instance-b-message',
+            topic: 'payments',
+            queueId: 1,
+            offset: 7,
+            storeTime: 1_700_000_001_000,
+            keys: 'instance-b-key',
+            body: 'b-body',
+            bodyBase64: null,
+            properties: {},
+            propertiesTruncated: false,
+          },
+        ],
+        total: 1,
+        page: 1,
+        size: 20,
+      }),
+    );
+
+    expect(await screen.findByText('instance-b-message')).toBeInTheDocument();
+    expect(screen.queryByText('stale-a-after-resend')).not.toBeInTheDocument();
+  });
+
   it('shows user properties in the DLQ message drawer', async () => {
     vi.mocked(messageService.listDLQMessages).mockResolvedValue({
       items: [
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index e32e06747..26f46df81 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -142,14 +142,17 @@ const DLQPage = () => {
   const [detailResending, setDetailResending] = useState(false);
   const [detailError, setDetailError] = useState<string | null>(null);
   const detailRequestIdRef = useRef(0);
+  const detailResendRequestIdRef = useRef(0);
   const retryRequestIdRef = useRef(0);
   const groupRequestIdRef = useRef(0);
-  const resendInFlightRef = useRef(false);
+  const retryInFlightRef = useRef(false);
+  const detailResendInFlightRef = useRef(false);
 
   useEffect(
     () => () => {
       retryRequestIdRef.current += 1;
       detailRequestIdRef.current += 1;
+      detailResendRequestIdRef.current += 1;
     },
     [],
   );
@@ -168,7 +171,15 @@ const DLQPage = () => {
     setTotal(0);
     setPage(1);
     setSelectedGroupNames([]);
+    setDetailOpen(false);
     setDetailGroup(null);
+    setDetailMessages([]);
+    setDetailTotal(0);
+    setDetailPage(1);
+    setDetailSelectedMsgIds([]);
+    setDetailLoading(false);
+    setDetailResending(false);
+    setDetailError(null);
     setRetryModalOpen(false);
     setRetryGroup(null);
     setRetryTargetTopic('');
@@ -229,7 +240,12 @@ const DLQPage = () => {
   /* ─── Handlers ─── */
   const handleInstanceChange = (instanceId: string) => {
     retryRequestIdRef.current += 1;
+    detailRequestIdRef.current += 1;
+    detailResendRequestIdRef.current += 1;
+    retryInFlightRef.current = false;
+    detailResendInFlightRef.current = false;
     setRetrySubmitting(false);
+    setDetailResending(false);
     selectInstance(instanceId);
   };
 
@@ -247,9 +263,9 @@ const DLQPage = () => {
       return;
     }
     if (!retryGroup || !selectedInstanceId) return;
-    if (resendInFlightRef.current) return;
+    if (retryInFlightRef.current || detailResendInFlightRef.current) return;
 
-    resendInFlightRef.current = true;
+    retryInFlightRef.current = true;
     const requestId = retryRequestIdRef.current + 1;
     retryRequestIdRef.current = requestId;
     const groupName = retryGroup.groupName;
@@ -283,8 +299,8 @@ const DLQPage = () => {
         setRetryError(getErrorMessage(error, DEFAULT_RETRY_ERROR));
       }
     } finally {
-      resendInFlightRef.current = false;
       if (retryRequestIdRef.current === requestId) {
+        retryInFlightRef.current = false;
         setRetrySubmitting(false);
       }
     }
@@ -318,6 +334,10 @@ const DLQPage = () => {
 
   /* ─── DLQ Message Details Drawer ─── */
   const openDetailDrawer = (group: DLQGroup) => {
+    detailRequestIdRef.current += 1;
+    detailResendRequestIdRef.current += 1;
+    detailResendInFlightRef.current = false;
+    setDetailResending(false);
     setDetailGroup(group);
     setDetailOpen(true);
     setDetailPage(1);
@@ -358,16 +378,23 @@ const DLQPage = () => {
 
   const resendSelectedMessages = async (msgIds: string[]) => {
     if (!selectedInstanceId || !detailGroup || msgIds.length === 0) return;
-    if (resendInFlightRef.current) return;
-    resendInFlightRef.current = true;
+    if (retryInFlightRef.current || detailResendInFlightRef.current) return;
+    detailResendInFlightRef.current = true;
+    const requestId = detailResendRequestIdRef.current + 1;
+    detailResendRequestIdRef.current = requestId;
+    const instanceId = selectedInstanceId;
+    const group = detailGroup;
+    const pageToReload = detailPage;
+    const pageSizeToReload = detailPageSize;
     setDetailResending(true);
     setDetailError(null);
     try {
       const result = await resendDLQSelected({
-        instanceId: selectedInstanceId,
-        groupName: detailGroup.groupName,
+        instanceId,
+        groupName: group.groupName,
         msgIds,
       });
+      if (detailResendRequestIdRef.current !== requestId) return;
       if (result.outcome === 'FAILED' && result.failed > 0) {
         message.error(`重发失败:成功 ${result.resent},失败 ${result.failed}`);
       } else if (result.resent > 0 && result.failed > 0) {
@@ -376,12 +403,16 @@ const DLQPage = () => {
         message.success(`重发完成:成功 ${result.resent} 条`);
       }
       setDetailSelectedMsgIds([]);
-      await loadDetailMessages(detailGroup, detailPage, detailPageSize);
+      await loadDetailMessages(group, pageToReload, pageSizeToReload);
     } catch (error) {
-      setDetailError(getErrorMessage(error, '重发死信消息失败,请稍后重试'));
+      if (detailResendRequestIdRef.current === requestId) {
+        setDetailError(getErrorMessage(error, '重发死信消息失败,请稍后重试'));
+      }
     } finally {
-      resendInFlightRef.current = false;
-      setDetailResending(false);
+      if (detailResendRequestIdRef.current === requestId) {
+        detailResendInFlightRef.current = false;
+        setDetailResending(false);
+      }
     }
   };
 
@@ -795,6 +826,9 @@ const DLQPage = () => {
         open={detailOpen}
         onClose={() => {
           detailRequestIdRef.current += 1;
+          detailResendRequestIdRef.current += 1;
+          detailResendInFlightRef.current = false;
+          setDetailResending(false);
           setDetailOpen(false);
           setDetailGroup(null);
           setDetailMessages([]);

Reply via email to