Skip to content

fix: avoid unread main queue copies in default in-memory manager (0.3.x) - #1276

Merged
kabir merged 3 commits into
a2aproject:0.3.xfrom
kabir:codex/03x-inmemory-queue-fanout
Nov 3, 2026
Merged

kabir merged 3 commits into
a2aproject:0.3.xfrom
kabir:codex/03x-inmemory-queue-fanout

Conversation

@kabir

@kabir kabir commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Description

The default InMemoryQueueManager in the legacy 0.3.x line always returns a ChildQueue from createOrTap(). MainQueue nevertheless retains a second copy of every event and acquires a separate semaphore permit. Consuming all child events does not release those permits, so event 1001 blocks with the default capacity even with fast consumers. Increasing the main capacity only moves the failure boundary.

This is a maintenance-branch fix for the behavior reproduced with 0.3.3.Final. It is not a claim about the newer main-branch event-bus implementation or the root cause of every production SSE timeout.

Change

Add a package-private dispatch-only builder mode and enable it only in InMemoryQueueManager's default factory. In this mode, MainQueue distributes events to children and invokes its hook without retaining an unused main copy or acquiring a permit for that copy. Child queues still enforce bounded backpressure.

No bookkeeping dequeue is performed, so enqueue does not falsely signal awaitQueuePollerStart(). Existing close callbacks and task-state handling remain in use. Directly built queues, explicitly added queues, custom factories, and the replicated queue manager's custom factory retain their previous behavior.

The class Javadoc documents that default-manager consumers use the returned child and that tap does not replay history. With no subscribers, the default dispatch-only main queue retains no events; later subscribers receive future events.

Tests

Five queue-only regression tests cover:

  • 10000 events delivered in order to two subscribers without reading MainQueue;
  • enqueue alone does not signal polling start, while a real child dequeue does;
  • a slow subscriber still blocks production until it consumes;
  • an active task can resubscribe after a period without subscribers, and final close removes its queue;
  • direct queues and custom factories still retain main-queue events.

Validation:

  • Unmodified 0.3.3.Final fails the 10000-event regression with a timeout in semaphore acquisition.
  • On this 0.3.x branch: mvn -B -ntp -pl server-common -am test — BUILD SUCCESS, 411 tests, 0 failures, 0 errors, 0 skipped (265 in server-common).
  • Queue-focused run: 40 tests, 0 failures/errors/skips.
  • git diff --check passes.
  • Java 21 runtime, project compiler release 17. The isolated 0.3.3 comparison used cached Jackson 2.17.2; the repository run used its unmodified dependency configuration.

Related: agentscope-ai/agentscope-java#2507.
Supersedes the application-level workaround proposed in agentscope-ai/agentscope-java#3941, whose enqueue-hook dequeue triggered polling-start signals prematurely. AgentScope 2.0.x would still require a released SDK fix; this PR alone does not change its published dependency.

This is #1256 @Evader-lgb rebased on the latest 0.3.x branch

@kabir
kabir changed the base branch from main to 0.3.x November 3, 2026 10:57
@kabir kabir closed this Nov 3, 2026
@kabir kabir reopened this Nov 3, 2026
@kabir
kabir marked this pull request as draft November 3, 2026 10:58
@kabir
kabir marked this pull request as ready for review November 3, 2026 11:05
@kabir
kabir merged commit 462a7bc into a2aproject:0.3.x Nov 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants