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 99ed333c4 fix(ai): re-read the run row before a stop writes the 
terminal state (#6384)
99ed333c4 is described below

commit 99ed333c465cc1b2aa4edde3b4ce583d0d0a6898
Author: 0 <[email protected]>
AuthorDate: Thu Oct 15 18:09:03 2026 +0800

    fix(ai): re-read the run row before a stop writes the terminal state (#6384)
    
    * fix(ai): re-read the run row before a stop writes the terminal state
    
    stop() read the run once at the top, and the registry lookup that came
    back empty - the branch that means nobody owns the run - could not tell
    an orphaned row from a worker that finalised between the read and the
    lookup. Writing STOPPED on that stale read duplicated the run_status
    event and flipped a run the user watched complete to STOPPED.
    
    The row is re-read before the terminal write; an already-terminal run
    makes the stop the no-op it always promised to be.
    
    * fix(ai): let the stop path fall through to the shared terminal re-read
---
 .../studio/ops/ai/conversation/AiRunService.java     | 18 ++++++++++++++----
 .../studio/ops/ai/conversation/AiRunServiceTest.java | 20 ++++++++++++++++++++
 2 files changed, 34 insertions(+), 4 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunService.java
index 63f346c59..5ac501dc0 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunService.java
@@ -402,11 +402,21 @@ public class AiRunService {
             // worker is still flushing its buffered text.
             log.info("agent run {} is already stopping", run.getId());
         } else {
-            // The row says active but nothing in this process owns it, so no 
worker will ever write the
-            // terminal state. Writing it here is what keeps the stop button 
from spinning until the
-            // orphan sweep gets around to it.
+            // The row said active but the worker may have finalised between 
the read at the top of
+            // this method and the registry lookup, whose empty result is what 
reached this branch.
+            // Re-read before writing anything: a terminal write over a 
finished run would duplicate
+            // the run_status event and flip a run the user watched complete 
to STOPPED.
+            RmqAiRun current = runRepository.findById(runId).orElse(run);
+            RunStatus currentStatus = statusOf(current.getStatus());
+            if (currentStatus == null || currentStatus.isTerminal()) {
+                log.debug("stop of run {} found it already terminal ({})", 
run.getId(), current.getStatus());
+                return current;
+            }
+            // The row really is active with no owner in this process, so no 
worker will ever write
+            // the terminal state. Writing it here is what keeps the stop 
button from spinning until
+            // the orphan sweep gets around to it.
             log.warn("run {} is non-terminal with no owner in this process; 
stopping it directly", run.getId());
-            runExecutor.terminate(detachedContext(run), RunStatus.STOPPED, 
StopReason.USER_STOP, null, null);
+            runExecutor.terminate(detachedContext(current), RunStatus.STOPPED, 
StopReason.USER_STOP, null, null);
         }
         return runRepository.findById(runId).orElse(run);
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunServiceTest.java
index 7a73a69ce..e2a84ce3e 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunServiceTest.java
@@ -458,6 +458,26 @@ class AiRunServiceTest {
         assertThat(runUpdates).isEmpty();
     }
 
+    @Test
+    void aStopThatLosesTheFinalisationRaceMustNotWriteASecondTerminalTest() {
+        // The run row is read once at the top of stop(); the worker can 
finalise between that read
+        // and the registry lookup, whose empty result then looks like "nobody 
owns this run".
+        // Writing the terminal state on that stale read would duplicate the 
run_status event and
+        // flip a run the user watched complete to STOPPED, so the row is 
re-read first.
+        RmqAiRun running = AiRunTestSupport.run(RUN_ID, CONVERSATION_ID, 1, 
RunStatus.RUNNING);
+        RmqAiRun finished = AiRunTestSupport.run(RUN_ID, CONVERSATION_ID, 1, 
RunStatus.COMPLETED);
+        when(runRepository.findById(RUN_ID))
+                .thenReturn(Optional.of(running))
+                .thenReturn(Optional.of(finished));
+        
when(runRepository.findActiveByConversationId(CONVERSATION_ID)).thenReturn(Optional.of(running));
+
+        RmqAiRun stopped = service.stop(RUN_ID);
+
+        assertThat(stopped.getStatus()).isEqualTo(RunStatus.COMPLETED.name());
+        assertThat(runUpdates).isEmpty();
+        assertThat(inserted).isEmpty();
+    }
+
     @Test
     void stopOfARunNobodyOwnsAnyMoreShouldStillWriteTheTerminalStateTest() {
         // The row says active, but nothing in this process owns it: the 
worker is gone and will never

Reply via email to