3535import org .a2aproject .sdk .server .events .InMemoryQueueManager ;
3636import org .a2aproject .sdk .server .events .MainEventBus ;
3737import org .a2aproject .sdk .server .events .MainEventBusProcessor ;
38+ import org .a2aproject .sdk .server .events .MainEventBusProcessorCallback ;
3839import org .a2aproject .sdk .server .tasks .AgentEmitter ;
3940import org .a2aproject .sdk .server .tasks .InMemoryPushNotificationConfigStore ;
4041import org .a2aproject .sdk .server .tasks .InMemoryTaskStore ;
4142import org .a2aproject .sdk .server .tasks .PushNotificationConfigStore ;
4243import org .a2aproject .sdk .server .tasks .PushNotificationSender ;
4344import org .a2aproject .sdk .server .tasks .TaskStore ;
45+ import org .a2aproject .sdk .server .tasks .TaskStateProvider ;
4446import org .a2aproject .sdk .spec .A2AError ;
4547import org .a2aproject .sdk .spec .CancelTaskParams ;
4648import 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