Skip to content

Commit 58620e4

Browse files
committed
fix(server): deliver awaited final events to gracefully closed child queues
A blocking message/send closes the caller's child queue as soon as the agent returns, while the final event can still be in the MainEventBus. Since dabca6c, distributeToChildren skipped every closed child, so the final event was lost and the EventConsumer waited out its 3-second grace period for it. Deliver to a closed child that is still awaiting a final event; a gracefully closed child still drains its queue. Fixes #1226
1 parent 7a1dfcc commit 58620e4

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

‎server-common/src/main/java/org/a2aproject/sdk/server/events/EventQueue.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ void distributeToChildren(EventQueueItem item) {
776776
// a narrow residual race remains for graceful close but is harmless for
777777
// non-final events (they'd land in the draining queue) and impossible for
778778
// final events (submittedFinalEvents prevents close while one is in flight).
779-
if (child.isClosed()) {
779+
if (child.isClosed() && !child.isAwaitingFinalEvent()) {
780780
LOGGER.debug("MainQueue[{}]: Skipping closed child queue for event {}",
781781
taskId, item.getEvent().getClass().getSimpleName());
782782
return;

‎server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,6 +1696,64 @@ public void cancel(RequestContext context, AgentEmitter emitter) {
16961696
assertEquals(TaskState.TASK_STATE_FAILED, storedTask.status().state());
16971697
}
16981698

1699+
/**
1700+
* A blocking call returns to the handler on the agent's first event, and the handler closes the
1701+
* caller's queue as soon as the agent returns. A final event the event bus distributes after that
1702+
* must still reach the queue, which was told to expect it; otherwise the consumer waits out its
1703+
* grace period for it and the call fails or returns late.
1704+
*/
1705+
@Test
1706+
void blockingSendStillReturnsCompletedTaskWhenFinalEventArrivesLate() throws Exception {
1707+
CountDownLatch agentReturned = new CountDownLatch(1);
1708+
holdEventsAfterWorkingUntilAgentReturned(agentReturned);
1709+
agentExecutorExecute = (context, emitter) -> {
1710+
emitter.startWork();
1711+
emitter.complete();
1712+
agentReturned.countDown();
1713+
};
1714+
1715+
EventKind result = requestHandler.onMessageSend(blockingParams("msg-blocking-final-event"), NULL_CONTEXT);
1716+
1717+
Task task = assertInstanceOf(Task.class, result);
1718+
assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state());
1719+
}
1720+
1721+
private void holdEventsAfterWorkingUntilAgentReturned(CountDownLatch agentReturned) {
1722+
mainEventBusProcessor.setCallback(new MainEventBusProcessorCallback() {
1723+
@Override
1724+
public void onEventProcessed(String taskId, Event event) {
1725+
if (event instanceof TaskStatusUpdateEvent statusUpdate
1726+
&& statusUpdate.status().state() == TaskState.TASK_STATE_WORKING) {
1727+
try {
1728+
// Hold the agent's next event in the bus until the handler has closed the queue
1729+
assertTrue(agentReturned.await(5, TimeUnit.SECONDS));
1730+
Thread.sleep(200);
1731+
} catch (InterruptedException e) {
1732+
Thread.currentThread().interrupt();
1733+
}
1734+
}
1735+
}
1736+
1737+
@Override
1738+
public void onTaskFinalized(String taskId) {
1739+
}
1740+
});
1741+
}
1742+
1743+
private static MessageSendParams blockingParams(String messageId) {
1744+
return MessageSendParams.builder()
1745+
.message(Message.builder()
1746+
.messageId(messageId)
1747+
.role(Message.Role.ROLE_USER)
1748+
.parts(new TextPart("hello"))
1749+
.build())
1750+
.configuration(MessageSendConfiguration.builder()
1751+
.returnImmediately(false)
1752+
.acceptedOutputModes(List.of())
1753+
.build())
1754+
.build();
1755+
}
1756+
16991757
@Test
17001758
void interruptedTerminalEnqueueStillReportsAgentFailure() throws Exception {
17011759
assertInterruptedTerminalEnqueueReportsError(

0 commit comments

Comments
 (0)