Skip to content

Commit 4db8bd1

Browse files
committed
test(server): cover terminal enqueue backpressure
This fixes #1183
1 parent 85290b2 commit 4db8bd1

1 file changed

Lines changed: 112 additions & 7 deletions

File tree

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

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@
3535
import org.a2aproject.sdk.server.events.InMemoryQueueManager;
3636
import org.a2aproject.sdk.server.events.MainEventBus;
3737
import org.a2aproject.sdk.server.events.MainEventBusProcessor;
38+
import org.a2aproject.sdk.server.events.MainEventBusProcessorCallback;
3839
import org.a2aproject.sdk.server.tasks.AgentEmitter;
3940
import org.a2aproject.sdk.server.tasks.InMemoryPushNotificationConfigStore;
4041
import org.a2aproject.sdk.server.tasks.InMemoryTaskStore;
4142
import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore;
4243
import org.a2aproject.sdk.server.tasks.PushNotificationSender;
4344
import org.a2aproject.sdk.server.tasks.TaskStore;
45+
import org.a2aproject.sdk.server.tasks.TaskStateProvider;
4446
import org.a2aproject.sdk.spec.A2AError;
4547
import org.a2aproject.sdk.spec.CancelTaskParams;
4648
import org.a2aproject.sdk.spec.Event;
@@ -1695,10 +1697,69 @@ public void cancel(RequestContext context, AgentEmitter emitter) {
16951697

16961698
@Test
16971699
void interruptedTerminalEnqueueStillReportsAgentFailure() throws Exception {
1700+
EventQueueUtil.stop(mainEventBusProcessor);
1701+
queueManager = new InMemoryQueueManager((TaskStateProvider) taskStore, mainEventBus) {
1702+
@Override
1703+
public EventQueue.EventQueueBuilder createBaseEventQueueBuilder(String taskId) {
1704+
return super.createBaseEventQueueBuilder(taskId).queueSize(1);
1705+
}
1706+
};
1707+
mainEventBusProcessor = new MainEventBusProcessor(mainEventBus, taskStore,
1708+
NOOP_PUSHNOTIFICATION_SENDER, queueManager);
1709+
EventQueueUtil.start(mainEventBusProcessor);
1710+
1711+
CountDownLatch workingEventProcessing = new CountDownLatch(1);
1712+
CountDownLatch releaseWorkingEvent = new CountDownLatch(1);
1713+
CountDownLatch fallbackErrorProcessed = new CountDownLatch(1);
1714+
mainEventBusProcessor.setCallback(new MainEventBusProcessorCallback() {
1715+
@Override
1716+
public void onEventProcessed(String taskId, Event event) {
1717+
if (event instanceof TaskStatusUpdateEvent statusUpdate
1718+
&& statusUpdate.status().state() == TaskState.TASK_STATE_WORKING) {
1719+
workingEventProcessing.countDown();
1720+
try {
1721+
releaseWorkingEvent.await();
1722+
} catch (InterruptedException e) {
1723+
Thread.currentThread().interrupt();
1724+
}
1725+
} else if (event instanceof InternalError) {
1726+
fallbackErrorProcessed.countDown();
1727+
}
1728+
}
1729+
1730+
@Override
1731+
public void onTaskFinalized(String taskId) {
1732+
}
1733+
});
1734+
1735+
CountDownLatch agentFinished = new CountDownLatch(1);
1736+
AtomicReference<Thread> agentThread = new AtomicReference<>();
1737+
AtomicReference<Boolean> interruptStatusAfterAgent = new AtomicReference<>(false);
1738+
Executor recordingExecutor = command -> internalExecutor.execute(() -> {
1739+
try {
1740+
command.run();
1741+
} finally {
1742+
interruptStatusAfterAgent.set(Thread.currentThread().isInterrupted());
1743+
agentFinished.countDown();
1744+
}
1745+
});
1746+
requestHandler = DefaultRequestHandler.builder()
1747+
.agentExecutor(executor)
1748+
.taskStore(taskStore)
1749+
.queueManager(queueManager)
1750+
.pushConfigStore(pushConfigStore)
1751+
.pushNotificationsEnabled(true)
1752+
.mainEventBusProcessor(mainEventBusProcessor)
1753+
.executor(recordingExecutor)
1754+
.eventConsumerExecutor(internalExecutor)
1755+
.authorizationRequired(false)
1756+
.build();
1757+
16981758
CountDownLatch executorRan = new CountDownLatch(1);
16991759
agentExecutorExecute = (context, emitter) -> {
1760+
agentThread.set(Thread.currentThread());
17001761
executorRan.countDown();
1701-
Thread.currentThread().interrupt();
1762+
emitter.startWork();
17021763
emitter.complete();
17031764
};
17041765

@@ -1708,15 +1769,59 @@ void interruptedTerminalEnqueueStillReportsAgentFailure() throws Exception {
17081769
.role(Message.Role.ROLE_USER)
17091770
.parts(new TextPart("hello"))
17101771
.build())
1711-
.configuration(DEFAULT_CONFIG)
1772+
.configuration(MessageSendConfiguration.builder()
1773+
.returnImmediately(false)
1774+
.acceptedOutputModes(List.of())
1775+
.build())
17121776
.build();
17131777

1714-
InternalError failure = assertThrows(InternalError.class,
1715-
() -> requestHandler.onMessageSend(params, NULL_CONTEXT));
1778+
ExecutorService controller = Executors.newSingleThreadExecutor();
1779+
try {
1780+
Future<Boolean> interruptWasRestored = controller.submit(() -> {
1781+
assertTrue(workingEventProcessing.await(5, TimeUnit.SECONDS),
1782+
"Processor should hold the queue capacity after the WORKING event");
1783+
assertTrue(executorRan.await(5, TimeUnit.SECONDS), "Executor should have run");
1784+
Thread worker = agentThread.get();
1785+
assertNotNull(worker);
1786+
assertTrue(awaitSemaphoreAcquire(worker, false),
1787+
"Terminal enqueue should wait while the queue is full");
1788+
1789+
worker.interrupt();
1790+
1791+
assertTrue(awaitSemaphoreAcquire(worker, false),
1792+
"Fallback enqueue should wait with the interrupt temporarily cleared");
1793+
releaseWorkingEvent.countDown();
1794+
assertTrue(agentFinished.await(5, TimeUnit.SECONDS), "Agent failure reporting should finish");
1795+
return interruptStatusAfterAgent.get();
1796+
});
17161797

1717-
assertTrue(executorRan.await(5, TimeUnit.SECONDS), "Executor should have run");
1718-
assertEquals("Agent execution failed: Unable to acquire the semaphore to enqueue the event",
1719-
failure.getMessage());
1798+
assertThrows(InternalError.class,
1799+
() -> requestHandler.onMessageSend(params, NULL_CONTEXT));
1800+
1801+
assertTrue(fallbackErrorProcessed.await(5, TimeUnit.SECONDS),
1802+
"The fallback InternalError should be processed by the event bus");
1803+
assertTrue(interruptWasRestored.get(5, TimeUnit.SECONDS),
1804+
"The agent worker's interrupt status should be restored after reporting the error");
1805+
} finally {
1806+
releaseWorkingEvent.countDown();
1807+
controller.shutdownNow();
1808+
}
1809+
}
1810+
1811+
private static boolean awaitSemaphoreAcquire(Thread thread, boolean interrupted) throws InterruptedException {
1812+
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
1813+
do {
1814+
if (thread.getState() == Thread.State.WAITING && thread.isInterrupted() == interrupted) {
1815+
for (StackTraceElement frame : thread.getStackTrace()) {
1816+
if (frame.getClassName().equals("java.util.concurrent.Semaphore")
1817+
&& frame.getMethodName().equals("acquire")) {
1818+
return true;
1819+
}
1820+
}
1821+
}
1822+
TimeUnit.MILLISECONDS.sleep(10);
1823+
} while (System.nanoTime() < deadline);
1824+
return false;
17201825
}
17211826

17221827
private DefaultRequestHandler buildHandlerWithRouter(AgentExecutorRouter router) {

0 commit comments

Comments
 (0)