You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by "unbridled-41 (via GitHub)" <gi...@apache.org> on 2026/09/24 11:19:33 UTC

[PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

unbridled-41 opened a new pull request, #5069:
URL: https://github.com/apache/rocketmq-dashboard/pull/5069

   ## Problem
   
   A client that reconnects to a run that is still generating can receive the run's own live frames **interleaved with the replayed ones**, which visibly shuffles the transcript it renders.
   
   `AgentStreamSession` buffers every live frame that arrives while the persisted rows are being replayed and drains that buffer in `finishReplay()`. The drain, however, runs *after* the lock is released:
   
   ```java
   void finishReplay() {
       Deque<LiveFrame> pending;
       synchronized (sendLock) {
           if (state.get() != State.REPLAYING) { return; }
           state.set(State.LIVE);
           pending = new ArrayDeque<>(buffered);
           buffered.clear();
       }
       pending.forEach(frame -> deliver(frame.seq(), frame.event()));   // <- outside the lock
   }
   ```
   
   `deliver` re-acquires `sendLock` per frame, and `sendAgent` serialises *before* it takes the lock, so between two drained frames the lock is free and a frame published by the run's worker thread (`AiEventSink` → `AgentRunRegistry.publish` → `deliver`) is written to the socket immediately — after buffered frame *k* and before buffered frame *k+1*.
   
   The order matters because it is the whole point of the class: the class javadoc (`AgentStreamSession.java:57-62`) states the invariant — "a frame cannot slip in between two replayed rows and reorder the transcript" — and the web client renders arrival order, appending every `text_delta` to the trailing text block (`web/src/pages/ai/render/blocks.ts:140-146`). A delta that overtakes buffered frames is a scrambled answer for anyone who reloaded the page or whose SSE connection dropped mid-run.
   
   ## Evidence
   
   Pre-fix, on base `a562601d`, with the new test in place and only the production file at its base revision:
   
   ```
   $ cd server && mvn -o test -Dtest=AgentStreamSessionReplayOrderTest
   [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
   java.lang.AssertionError:
   Expecting actual:
     "{"type":"text_delta","content":"live-one"}"
   to contain:
     "replay-two"
     at AgentStreamSessionReplayOrderTest.aFramePublishedDuringTheDrainShouldNotOvertakeTheBufferedFrames(AgentStreamSessionReplayOrderTest.java:89)
   ```
   
   The live frame is at index 1, the second buffered frame at index 2 — the order the test asserts is `replay-one, replay-two, live-one`.
   
   The test is not timing-dependent: it parks the drain inside the *serialisation* of its second buffered frame (`ObjectMapper.writeValueAsString` hook) and then publishes a frame from another thread. Serialisation is outside the lock on both sides of the fix, so the pause is precisely the window the defect leaves open.
   
   Reachability: `AiRunService.attach` (`AiRunService.java:268-288`) — the reconnect path of `GET /api/ai/runs/{runId}/stream` — calls `finishReplay()` after `registry.attach(...)`, so the worker can publish from the moment the observer is registered, i.e. exactly while the buffer is being drained.
   
   ## Root cause
   
   `finishReplay()` released `sendLock` between the state flip and the drain, so the drain was no longer atomic with respect to live frames.
   
   ## Fix
   
   Drain the copied buffer while still holding `sendLock`. `deliver` is reentrant on the same monitor, the dedup predicate is unchanged, and the lock already serialises every write to the socket (heartbeat, replayed frame, live frame), so nothing else can now be written until the buffer is empty. The javadoc records why the lock is held across the drain, since that is the non-obvious part.
   
   ## Score
   
   `PRIORITY = 70` (impact 25: a visibly shuffled answer on every reconnect that lands inside the drain, which is the documented failure mode of the whole buffering design; scope 15: the AI streaming surface, every reconnecting client; reproducibility 15: the regression test decides the race deterministically; maintenance value 15: the class documents the invariant it was breaking).
   `FIX_CONFIDENCE = 85`.
   
   ## Tests
   
   ```
   $ cd server && mvn -o test -Dtest=AgentStreamSessionReplayOrderTest          # post-fix
   Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
   BUILD SUCCESS
   
   $ cd server && mvn -o test -Dtest='org.apache.rocketmq.studio.ops.ai.**.*Test'
   Tests run: 924, Failures: 0, Errors: 0, Skipped: 0
   BUILD SUCCESS
   ```
   
   The pre-fix run of the same test (only `AgentStreamSession.java` reverted to `a562601d`, test kept) is the failure quoted under Evidence. Checkstyle runs as part of the `validate` phase of both commands.
   
   ## Risk
   
   Low, and bounded to the replay path: the drain now happens under the same monitor that already guards every socket write, so a concurrent publish waits for the drain instead of interleaving with it. That is the intended serialisation; the only cost is a longer lock hold for the length of one buffer (a replay-length burst of socket writes, previously interleaved with live ones anyway). No API, payload or dedup behaviour changes. `sendAgent` can still detach mid-drain on an IO failure, which is how the pre-existing code behaves too.
   
   ## Dedup
   
   - `gh search prs --repo apache/rocketmq-dashboard "AgentStreamSession"` → only #4948 (drain a backlog longer than one timeline page), which changes the *replay read* in `AiRunService`/`replayInto` and does not touch `finishReplay` or `AgentStreamSession.java` at all — the two changes sit in different files and can merge in either order.
   - `gh search issues "AgentStreamSession"` → only #4963 (the same #4948 defect).
   - `gh search prs/issues "finishReplay"`, `"replay buffered frames"`, `"reconnect transcript order"` → no results.
   - `git log --oneline a562601d -- .../AgentStreamSession.java` → `5b761df8` (introduction) only.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

Posted by "unbridled-41 (via GitHub)" <gi...@apache.org>.
unbridled-41 commented on PR #5069:
URL: https://github.com/apache/rocketmq-dashboard/pull/5069#issuecomment-5929062970

   Thanks for the review — the blocking item is fixed in f2a4b658: the case is now `aFramePublishedDuringTheDrainShouldNotOvertakeTheBufferedFramesTest` (`AgentStreamSessionReplayOrderTest.java:56`), matching the `AgentRunRegistryTest` convention; `mvn -o test -Dtest=AgentStreamSessionReplayOrderTest` passes (1/1).
   
   On the optional item: agreed the `RecordingEmitter` overlaps `AiRunTestSupport.RecordingSseEmitter`, and per your note I have left it in place for now — the two record at different granularity (raw event set vs. filtered JSON frames) and this case needs to count JSON frames only. If you would still prefer the shared `awaitPayload(String)` helper, happy to do that consolidation as a separate change.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

Posted by "unbridled-41 (via GitHub)" <gi...@apache.org>.
unbridled-41 commented on PR #5069:
URL: https://github.com/apache/rocketmq-dashboard/pull/5069#issuecomment-6267545510

   Thanks for the review. This is now superseded by #5758, merged into `rocketmq-studio` on 2026-10-19: it fixes the same `finishReplay()` ordering race by draining the buffer while holding `sendLock`, and carries its own deterministic regression test. Keeping this branch open would submit a third copy of the same production change, so I am closing it as superseded. The test-name blocker from the review was fixed in f2a4b658 and is preserved in the closed branch for reference.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

Posted by "unbridled-41 (via GitHub)" <gi...@apache.org>.
unbridled-41 closed pull request #5069: fix(ai): keep a reconnecting run's buffered frames ahead of live ones
URL: https://github.com/apache/rocketmq-dashboard/pull/5069


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

Posted by "unbridled-41 (via GitHub)" <gi...@apache.org>.
unbridled-41 commented on PR #5069:
URL: https://github.com/apache/rocketmq-dashboard/pull/5069#issuecomment-6086398906

   ### CI status of this PR
   
   `Backend Tests (Java 21)` — **pass**; the change-specific commands and their results are in the Tests section of the description.
   
   The four remaining red jobs are red for every branch, including `rocketmq-studio` itself:
   
   - `Backend Build (Java 21)` — the `binary-license-gate` antrun step fails on a list of third-party jars (`spring-ai-*`, `swagger-ui`, `tencentcloud-sdk-*`, `zstd-jni`, ... : "missing complete upstream LICENSE/COPYING; a POM name or link is not a substitute"). This PR touches no build configuration; tracked by #5341.
   - `Frontend Build (Node 20)` / `Frontend Docker Build` — the frontend licence manifest check fails the same way on branches that do not touch `web/` at all; tracked by #5346.
   - `rmqctl Build (Go)` — `cmd` `TestMessageQueryCompletenessWarningTest` yaml subtests: `got {"items":[{"size":"3",...}]}, want {"size":3,...}`, i.e. the YAML renderer writes numbers as quoted strings where the test expects integer literals. Reproduced locally with `cd rmqctl && go test ./cmd/` from a checkout whose `rmqctl/` tree is byte-identical to `rocketmq-studio`'s, and owned by open PR #5850 ("keep yaml numbers unquoted").


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

Posted by "unbridled-41 (via GitHub)" <gi...@apache.org>.
unbridled-41 commented on PR #5069:
URL: https://github.com/apache/rocketmq-dashboard/pull/5069#issuecomment-5813353420

   ### Verification pass (evidence audit, 2026-09-24)
   
   Fresh checkout of this branch (`git worktree add --detach /tmp/aud-a fork/fix/ai-replay-drain-order`, empty `server/target`); every `file:line` re-read against both the base `a562601d` and the branch; every command re-executed there. The pre-fix claim was re-demonstrated by writing the base content of the production file back with `git show a562601d:<path> > <path>` (which does not touch the index) and keeping the new test.
   
   **Citations — all hold**
   
   | Claim | base `a562601d` | branch |
   |---|---|---|
   | the invariant the fix restores | `AgentStreamSession.java:57-62` — the `Interleaving` bullet, verbatim "a frame cannot slip in between two replayed rows and reorder the transcript" | unchanged |
   | the defect | `:169-180` — state flip and buffer copy under `sendLock`, `pending.forEach(...)` outside it | drains inside the block; only the lock scope moved |
   | the premise the test's hook relies on | `sendAgent` serialises at `:301`, takes `sendLock` at `:308` | unchanged |
   | the reachable path | `AiRunService.java:268-288` (`attach`: replay read → watermark → `registry.attach` → `finishReplay`) | unchanged |
   | why arrival order is the rendered order | `web/src/pages/ai/render/blocks.ts:140-146` appends a `text_delta` to the trailing text block | unchanged |
   | the quoted failure location | — | `AgentStreamSessionReplayOrderTest.java:89` is exactly `assertThat(frames.get(1)).contains("replay-two")` |
   
   **Pre-fix reproduction** (same checkout, command as written):
   
   ```
   $ cd server && mvn -o test -Dtest=AgentStreamSessionReplayOrderTest
   java.lang.AssertionError:
   Expecting actual:
     "{"type":"text_delta","content":"live-one"}"
   to contain:
     "replay-two"
   	at …AgentStreamSessionReplayOrderTest.aFramePublishedDuringTheDrainShouldNotOvertakeTheBufferedFrames(AgentStreamSessionReplayOrderTest.java:89)
   [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
   ```
   
   **Post-fix** (production file restored): `Tests run: 1, Failures: 0, Errors: 0, Skipped: 0` — BUILD SUCCESS. Package run `-Dtest='org.apache.rocketmq.studio.ops.ai.**.*Test'`: `Tests run: 924, Failures: 0, Errors: 0, Skipped: 0`.
   
   **Coverage of the changed behaviour.** The test asserts all three consequences of getting the drain right: every buffered frame is still written (`hasSize(3)`), in buffered order (`frames.get(0)`, `frames.get(1)`), and the frame published during the drain is delivered *after* them (`frames.get(2)`) — neither reordering nor a silently dropped frame passes. The adjacent properties the fix could have broken are already pinned and still pass on the branch: dedup of a frame buffered during a replay (`AgentRunRegistryTest.aFrameBufferedDuringAReplayShouldStillBeDeduplicatedTest`) and "live frame after replayed frame" (`AgentRunRegistryTest.framesArrivingDuringAReplayShouldBeBufferedUntilTheReplayIsSentTest`). Neither existing test can catch this defect: both publish the live frame *before* `finishReplay()` in a single thread (no `Thread`/latch anywhere in `AgentRunRegistryTest`), so no frame is ever published while the drain is running — which is why the new test drives a two-frame 
 buffer and publishes from another thread mid-drain.
   
   **Commit hygiene**: `git show --stat 83d280df` = the production file + the new test, nothing else.
   
   No description defect, no test gap and no code defect found in this pass; nothing was added.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] fix(ai): keep a reconnecting run's buffered frames ahead of live ones [rocketmq-dashboard]

Posted by "unbridled-41 (via GitHub)" <gi...@apache.org>.
unbridled-41 commented on PR #5069:
URL: https://github.com/apache/rocketmq-dashboard/pull/5069#issuecomment-5965575665

   ### CI status of this PR
   
   `Backend Tests (Java 21)` — **pass**; the change-specific command and its result are recorded in the Tests section above.
   
   The remaining red jobs are red for every branch, including `rocketmq-studio` itself, with the same logs:
   
   - `Backend Build (Java 21)` — fails inside the `binary-license-gate` antrun step (`exec returned: 1`); it also fails on a docs-only PR of this batch, and is addressed independently by #5341.
   - `Frontend Build (Node 20)` — fails after a successful build in the license manifest check (`license gate: build artifact verification failed: assets/index-<hash>.js`); identical on trunk and on a docs-only PR of this batch (run 37088829422), addressed independently by #5346.
   - `Frontend Docker Build` — depends on the frontend build above.
   
   This diff touches `server`, so none of the three can be caused by it.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@rocketmq.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org