Skip to content

Commit 306ea56

Browse files
committed
Use callback rather than polling where possible in tests
1 parent fc23321 commit 306ea56

File tree

5 files changed

+247
-127
lines changed

5 files changed

+247
-127
lines changed

extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,45 @@ private TaskStatusUpdateEvent createEventForTask(String taskId) {
8383
@AfterEach
8484
void tearDown() {
8585
if (mainEventBusProcessor != null) {
86+
mainEventBusProcessor.setCallback(null); // Clear any test callbacks
8687
EventQueueUtil.stop(mainEventBusProcessor);
8788
}
8889
mainEventBusProcessor = null;
8990
mainEventBus = null;
9091
queueManager = null;
9192
}
9293

94+
/**
95+
* Helper to wait for MainEventBusProcessor to process an event.
96+
* Replaces polling patterns with deterministic callback-based waiting.
97+
*
98+
* @param action the action that triggers event processing
99+
* @throws InterruptedException if waiting is interrupted
100+
* @throws AssertionError if processing doesn't complete within timeout
101+
*/
102+
private void waitForEventProcessing(Runnable action) throws InterruptedException {
103+
CountDownLatch processingLatch = new CountDownLatch(1);
104+
mainEventBusProcessor.setCallback(new io.a2a.server.events.MainEventBusProcessorCallback() {
105+
@Override
106+
public void onEventProcessed(String taskId, io.a2a.spec.Event event) {
107+
processingLatch.countDown();
108+
}
109+
110+
@Override
111+
public void onTaskFinalized(String taskId) {
112+
// Not needed for basic event processing wait
113+
}
114+
});
115+
116+
try {
117+
action.run();
118+
assertTrue(processingLatch.await(5, TimeUnit.SECONDS),
119+
"MainEventBusProcessor should have processed the event within timeout");
120+
} finally {
121+
mainEventBusProcessor.setCallback(null);
122+
}
123+
}
124+
93125
@Test
94126
void testReplicationStrategyTriggeredOnNormalEnqueue() throws InterruptedException {
95127
CountingReplicationStrategy strategy = new CountingReplicationStrategy();
@@ -147,10 +179,9 @@ void testReplicatedEventDeliveredToCorrectQueue() throws InterruptedException {
147179
EventQueue queue = queueManager.createOrTap(taskId);
148180

149181
ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, eventForTask);
150-
queueManager.onReplicatedEvent(replicatedEvent);
151182

152-
// Use retry logic to handle async MainEventBusProcessor distribution
153-
EventQueueItem item = dequeueEventWithRetry(queue, 5000);
183+
// Use callback to wait for event processing
184+
EventQueueItem item = dequeueEventWithRetry(queue, () -> queueManager.onReplicatedEvent(replicatedEvent));
154185
assertNotNull(item, "Event should be available in queue");
155186
Event dequeuedEvent = item.getEvent();
156187
assertEquals(eventForTask, dequeuedEvent);
@@ -175,12 +206,11 @@ void testReplicatedEventCreatesQueueIfNeeded() throws InterruptedException {
175206

176207
ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, eventForTask);
177208

178-
// Process the replicated event
179-
assertDoesNotThrow(() -> queueManager.onReplicatedEvent(replicatedEvent));
180-
181-
// Verify the event was enqueued and distributed to our ChildQueue
182-
// Use retry logic to handle async MainEventBusProcessor distribution
183-
EventQueueItem item = dequeueEventWithRetry(childQueue, 5000);
209+
// Process the replicated event and wait for distribution
210+
// Use callback to wait for event processing
211+
EventQueueItem item = dequeueEventWithRetry(childQueue, () -> {
212+
assertDoesNotThrow(() -> queueManager.onReplicatedEvent(replicatedEvent));
213+
});
184214
assertNotNull(item, "Event should be available in queue");
185215
Event dequeuedEvent = item.getEvent();
186216
assertEquals(eventForTask, dequeuedEvent, "The replicated event should be enqueued in the newly created queue");
@@ -379,11 +409,10 @@ void testReplicatedEventProcessedWhenTaskActive() throws InterruptedException {
379409

380410
// Process a replicated event for an active task
381411
ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, eventForTask);
382-
queueManager.onReplicatedEvent(replicatedEvent);
383412

384413
// Verify the event was enqueued and distributed to our ChildQueue
385-
// Use retry logic to handle async MainEventBusProcessor distribution
386-
EventQueueItem item = dequeueEventWithRetry(childQueue, 5000);
414+
// Use callback to wait for event processing
415+
EventQueueItem item = dequeueEventWithRetry(childQueue, () -> queueManager.onReplicatedEvent(replicatedEvent));
387416
assertNotNull(item, "Event should be available in queue");
388417
Event dequeuedEvent = item.getEvent();
389418
assertEquals(eventForTask, dequeuedEvent, "Event should be enqueued for active task");
@@ -501,21 +530,17 @@ void testReplicatedQueueClosedEventTerminatesConsumer() throws InterruptedExcept
501530
TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId
502531
EventQueue queue = queueManager.createOrTap(taskId);
503532

504-
// Enqueue a normal event
505-
queue.enqueueEvent(eventForTask);
506-
507533
// Simulate receiving QueueClosedEvent from remote node
508534
QueueClosedEvent closedEvent = new QueueClosedEvent(taskId);
509535
ReplicatedEventQueueItem replicatedClosedEvent = new ReplicatedEventQueueItem(taskId, closedEvent);
510-
queueManager.onReplicatedEvent(replicatedClosedEvent);
511536

512-
// Dequeue the normal event first (use retry for async processing)
513-
EventQueueItem item1 = dequeueEventWithRetry(queue, 5000);
537+
// Dequeue the normal event first (use callback to wait for async processing)
538+
EventQueueItem item1 = dequeueEventWithRetry(queue, () -> queue.enqueueEvent(eventForTask));
514539
assertNotNull(item1, "First event should be available");
515540
assertEquals(eventForTask, item1.getEvent());
516541

517-
// Next dequeue should get the QueueClosedEvent (use retry for async processing)
518-
EventQueueItem item2 = dequeueEventWithRetry(queue, 5000);
542+
// Next dequeue should get the QueueClosedEvent (use callback to wait for async processing)
543+
EventQueueItem item2 = dequeueEventWithRetry(queue, () -> queueManager.onReplicatedEvent(replicatedClosedEvent));
519544
assertNotNull(item2, "QueueClosedEvent should be available");
520545
assertTrue(item2.getEvent() instanceof QueueClosedEvent,
521546
"Second event should be QueueClosedEvent");
@@ -577,28 +602,23 @@ public void setActive(boolean active) {
577602
}
578603

579604
/**
580-
* Helper method to dequeue an event with retry logic to handle async MainEventBusProcessor distribution.
581-
* This handles the race condition where MainEventBusProcessor may not have distributed the event yet.
605+
* Helper method to dequeue an event after waiting for MainEventBusProcessor distribution.
606+
* Uses callback-based waiting instead of polling for deterministic synchronization.
582607
*
583608
* @param queue the queue to dequeue from
584-
* @param maxWaitMs maximum time to wait in milliseconds (default 5000)
585-
* @return the dequeued EventQueueItem, or null if timeout occurs
609+
* @param enqueueAction the action that enqueues the event (triggers event processing)
610+
* @return the dequeued EventQueueItem, or null if queue is closed
586611
*/
587-
private EventQueueItem dequeueEventWithRetry(EventQueue queue, long maxWaitMs) throws InterruptedException {
588-
EventQueueItem item = null;
589-
long pollStart = System.currentTimeMillis();
590-
while (item == null && (System.currentTimeMillis() - pollStart) < maxWaitMs) {
591-
try {
592-
item = queue.dequeueEventItem(100); // Short timeout per attempt
593-
if (item == null) {
594-
// No event yet, wait a bit before retry
595-
Thread.sleep(50);
596-
}
597-
} catch (EventQueueClosedException e) {
598-
// Queue closed, return null
599-
return null;
600-
}
612+
private EventQueueItem dequeueEventWithRetry(EventQueue queue, Runnable enqueueAction) throws InterruptedException {
613+
// Wait for event to be processed and distributed
614+
waitForEventProcessing(enqueueAction);
615+
616+
// Event is now available, dequeue directly
617+
try {
618+
return queue.dequeueEventItem(100);
619+
} catch (EventQueueClosedException e) {
620+
// Queue closed, return null
621+
return null;
601622
}
602-
return item;
603623
}
604624
}

server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,42 @@ public void init() {
8080
@AfterEach
8181
public void cleanup() {
8282
if (mainEventBusProcessor != null) {
83+
mainEventBusProcessor.setCallback(null); // Clear any test callbacks
8384
EventQueueUtil.stop(mainEventBusProcessor);
8485
}
8586
}
8687

88+
/**
89+
* Helper to wait for MainEventBusProcessor to process an event.
90+
* Replaces polling patterns with deterministic callback-based waiting.
91+
*
92+
* @param action the action that triggers event processing
93+
* @throws InterruptedException if waiting is interrupted
94+
* @throws AssertionError if processing doesn't complete within timeout
95+
*/
96+
private void waitForEventProcessing(Runnable action) throws InterruptedException {
97+
CountDownLatch processingLatch = new CountDownLatch(1);
98+
mainEventBusProcessor.setCallback(new MainEventBusProcessorCallback() {
99+
@Override
100+
public void onEventProcessed(String taskId, Event event) {
101+
processingLatch.countDown();
102+
}
103+
104+
@Override
105+
public void onTaskFinalized(String taskId) {
106+
// Not needed for basic event processing wait
107+
}
108+
});
109+
110+
try {
111+
action.run();
112+
assertTrue(processingLatch.await(5, TimeUnit.SECONDS),
113+
"MainEventBusProcessor should have processed the event within timeout");
114+
} finally {
115+
mainEventBusProcessor.setCallback(null);
116+
}
117+
}
118+
87119
@Test
88120
public void testConsumeOneTaskEvent() throws Exception {
89121
Task event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class);
@@ -421,15 +453,9 @@ public void testConsumeAllHandlesQueueClosedException() throws Exception {
421453

422454
// Add a message event (which will complete the stream)
423455
Event message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class);
424-
queue.enqueueEvent(message);
425456

426-
// Poll for event to arrive in ChildQueue (async MainEventBusProcessor distribution)
427-
long startTime = System.currentTimeMillis();
428-
long timeout = 2000;
429-
while (queue.size() == 0 && (System.currentTimeMillis() - startTime) < timeout) {
430-
Thread.sleep(50);
431-
}
432-
assertTrue(queue.size() > 0, "Event should arrive in ChildQueue within timeout");
457+
// Use callback to wait for event processing
458+
waitForEventProcessing(() -> queue.enqueueEvent(message));
433459

434460
// Close the queue before consuming
435461
queue.close();
@@ -525,20 +551,12 @@ public void onComplete() {
525551
}
526552

527553
private void enqueueAndConsumeOneEvent(Event event) throws Exception {
528-
eventQueue.enqueueEvent(event);
529-
// Poll for event with 2-second timeout
530-
long startTime = System.currentTimeMillis();
531-
long timeout = 2000;
532-
Event result = null;
533-
while (result == null && (System.currentTimeMillis() - startTime) < timeout) {
534-
try {
535-
result = eventConsumer.consumeOne();
536-
} catch (A2AServerException e) {
537-
// Event not available yet, wait a bit and try again
538-
Thread.sleep(50);
539-
}
540-
}
541-
assertNotNull(result, "Event should arrive within timeout");
554+
// Use callback to wait for event processing
555+
waitForEventProcessing(() -> eventQueue.enqueueEvent(event));
556+
557+
// Event is now available, consume it directly
558+
Event result = eventConsumer.consumeOne();
559+
assertNotNull(result, "Event should be available");
542560
assertSame(event, result);
543561
}
544562

server-common/src/test/java/io/a2a/server/events/EventQueueTest.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import static org.junit.jupiter.api.Assertions.assertTrue;
1111

1212
import java.util.List;
13+
import java.util.concurrent.CountDownLatch;
14+
import java.util.concurrent.TimeUnit;
1315

1416
import io.a2a.server.tasks.InMemoryTaskStore;
1517
import io.a2a.server.tasks.PushNotificationSender;
@@ -74,6 +76,7 @@ public void init() {
7476
@AfterEach
7577
public void cleanup() {
7678
if (mainEventBusProcessor != null) {
79+
mainEventBusProcessor.setCallback(null); // Clear any test callbacks
7780
EventQueueUtil.stop(mainEventBusProcessor);
7881
}
7982
}
@@ -87,6 +90,37 @@ private EventQueue createQueueWithEventBus(String taskId) {
8790
.build();
8891
}
8992

93+
/**
94+
* Helper to wait for MainEventBusProcessor to process an event.
95+
* Replaces polling patterns with deterministic callback-based waiting.
96+
*
97+
* @param action the action that triggers event processing
98+
* @throws InterruptedException if waiting is interrupted
99+
* @throws AssertionError if processing doesn't complete within timeout
100+
*/
101+
private void waitForEventProcessing(Runnable action) throws InterruptedException {
102+
CountDownLatch processingLatch = new CountDownLatch(1);
103+
mainEventBusProcessor.setCallback(new io.a2a.server.events.MainEventBusProcessorCallback() {
104+
@Override
105+
public void onEventProcessed(String taskId, io.a2a.spec.Event event) {
106+
processingLatch.countDown();
107+
}
108+
109+
@Override
110+
public void onTaskFinalized(String taskId) {
111+
// Not needed for basic event processing wait
112+
}
113+
});
114+
115+
try {
116+
action.run();
117+
assertTrue(processingLatch.await(5, TimeUnit.SECONDS),
118+
"MainEventBusProcessor should have processed the event within timeout");
119+
} finally {
120+
mainEventBusProcessor.setCallback(null);
121+
}
122+
}
123+
90124
@Test
91125
public void testConstructorDefaultQueueSize() {
92126
EventQueue queue = EventQueueUtil.getEventQueueBuilder(mainEventBus).build();
@@ -269,17 +303,10 @@ public void testDequeueEventWhenClosedButHasEvents() throws Exception {
269303
EventQueue childQueue = mainQueue.tap();
270304
Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class);
271305

272-
// Enqueue to mainQueue
273-
mainQueue.enqueueEvent(event);
274-
275-
// Poll for event to arrive in childQueue (async MainEventBusProcessor distribution)
276-
long startTime = System.currentTimeMillis();
277-
long timeout = 2000;
278-
while (childQueue.size() == 0 && (System.currentTimeMillis() - startTime) < timeout) {
279-
Thread.sleep(50);
280-
}
281-
assertTrue(childQueue.size() > 0, "Event should arrive in ChildQueue within timeout");
306+
// Use callback to wait for event processing instead of polling
307+
waitForEventProcessing(() -> mainQueue.enqueueEvent(event));
282308

309+
// At this point, event has been processed and distributed to childQueue
283310
childQueue.close(); // Graceful close - events should remain
284311
assertTrue(childQueue.isClosed());
285312

0 commit comments

Comments
 (0)