Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/run-tck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,88 @@ jobs:
done

- name: Run TCK
id: run-tck
timeout-minutes: 5
run: |
./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category all --transports jsonrpc,grpc,rest --compliance-report report.json
working-directory: tck/a2a-tck
- name: Capture Thread Dump
if: failure()
run: |
# Find the actual Quarkus JVM (child of Maven process), not the Maven parent
# Look for the dev.jar process which is the actual application
QUARKUS_PID=$(pgrep -f "a2a-tck-server-dev.jar" || echo "")
if [ -n "$QUARKUS_PID" ]; then
echo "📊 Capturing thread dump for Quarkus JVM PID $QUARKUS_PID"
jstack $QUARKUS_PID > tck/target/thread-dump.txt || echo "Failed to capture thread dump"
if [ -f tck/target/thread-dump.txt ]; then
echo "✅ Thread dump captured ($(wc -l < tck/target/thread-dump.txt) lines)"
fi
else
echo "⚠️ No Quarkus JVM process found for thread dump"
echo "Available Java processes:"
ps aux | grep java || true
fi
- name: Capture Heap Dump
if: failure()
run: |
# Find the actual Quarkus JVM (child of Maven process), not the Maven parent
QUARKUS_PID=$(pgrep -f "a2a-tck-server-dev.jar" || echo "")
if [ -n "$QUARKUS_PID" ]; then
echo "📊 Capturing heap dump for Quarkus JVM PID $QUARKUS_PID"
jmap -dump:live,format=b,file=tck/target/heap-dump.hprof $QUARKUS_PID || echo "Failed to capture heap dump"
if [ -f tck/target/heap-dump.hprof ]; then
SIZE=$(du -h tck/target/heap-dump.hprof | cut -f1)
echo "✅ Heap dump captured ($SIZE)"
# Compress to reduce artifact size
gzip tck/target/heap-dump.hprof
COMPRESSED_SIZE=$(du -h tck/target/heap-dump.hprof.gz | cut -f1)
echo "✅ Compressed heap dump ($COMPRESSED_SIZE)"
fi
else
echo "⚠️ No Quarkus JVM process found for heap dump"
echo "Available Java processes:"
ps aux | grep java || true
fi
- name: Stop Quarkus Server
if: always()
run: |
# Find and kill the Quarkus process to ensure logs are flushed
pkill -f "quarkus:dev" || true
sleep 2
- name: Verify TCK Log
if: failure()
run: |
echo "Checking for log file..."
if [ -f tck/target/tck-test.log ]; then
echo "✅ Log file exists ($(wc -l < tck/target/tck-test.log) lines)"
ls -lh tck/target/tck-test.log
else
echo "❌ Log file not found at tck/target/tck-test.log"
echo "Contents of tck/target/:"
ls -la tck/target/ || echo "tck/target/ does not exist"
fi
- name: Upload TCK Log
if: failure()
uses: actions/upload-artifact@v4
with:
name: tck-test-log-java-${{ matrix.java-version }}
path: tck/target/tck-test.log
retention-days: 2
if-no-files-found: warn
- name: Upload Thread Dump
if: failure()
uses: actions/upload-artifact@v4
with:
name: thread-dump-java-${{ matrix.java-version }}
path: tck/target/thread-dump.txt
retention-days: 2
if-no-files-found: warn
- name: Upload Heap Dump
if: failure()
uses: actions/upload-artifact@v4
with:
name: heap-dump-java-${{ matrix.java-version }}
path: tck/target/heap-dump.hprof.gz
retention-days: 2
if-no-files-found: warn
Binary file not shown.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,38 @@ public class WeatherAgentExecutorProducer {
}
```

### 4. Configure Executor Settings (Optional)

The A2A Java SDK uses a dedicated executor for handling asynchronous operations like streaming subscriptions. By default, this executor is configured with a core pool size of 5 threads and a maximum pool size of 50 threads, optimized for I/O-bound operations.

You can customize the executor settings in your `application.properties`:

```properties
# Core thread pool size for the @Internal executor (default: 5)
a2a.executor.core-pool-size=5

# Maximum thread pool size (default: 50)
a2a.executor.max-pool-size=50

# Thread keep-alive time in seconds (default: 60)
a2a.executor.keep-alive-seconds=60
```

**Why this matters:**
- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load.
- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool used by other async tasks.
- **Concurrency**: In production environments with high concurrent streaming requests, increase the pool sizes accordingly.

**Default Configuration:**
```properties
# These are the defaults - no need to set unless you want different values
a2a.executor.core-pool-size=5
a2a.executor.max-pool-size=50
a2a.executor.keep-alive-seconds=60
```

**Note:** The reference server implementations automatically configure this executor. If you're creating a custom server integration, ensure you provide an `@Internal Executor` bean for optimal streaming performance.

## A2A Client

The A2A Java SDK provides a Java client implementation of the [Agent2Agent (A2A) Protocol](https://google-a2a.github.io/A2A), allowing communication with A2A servers. The Java client implementation supports the following transports:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public EventQueue.EventQueueBuilder getEventQueueBuilder(String taskId) {
.hook(new ReplicationHook(taskId));
}

@Override
public int getActiveChildQueueCount(String taskId) {
return delegate.getActiveChildQueueCount(taskId);
}

private class ReplicatingEventQueueFactory implements EventQueueFactory {
@Override
public EventQueue.EventQueueBuilder builder(String taskId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand All @@ -17,6 +18,7 @@

import io.a2a.server.events.EventQueue;
import io.a2a.server.events.EventQueueClosedException;
import io.a2a.server.events.EventQueueTestHelper;
import io.a2a.spec.Event;
import io.a2a.spec.StreamingEventKind;
import io.a2a.spec.TaskState;
Expand Down Expand Up @@ -147,8 +149,11 @@ void testBasicQueueManagerFunctionality() throws InterruptedException {
EventQueue queue = queueManager.createOrTap(taskId);
assertNotNull(queue);

// createOrTap now returns ChildQueue, get returns MainQueue
EventQueue retrievedQueue = queueManager.get(taskId);
assertEquals(queue, retrievedQueue);
assertNotNull(retrievedQueue);
// queue should be a ChildQueue (cannot be tapped)
assertThrows(IllegalStateException.class, () -> EventQueueTestHelper.tapQueue(queue));

EventQueue tappedQueue = queueManager.tap(taskId);
assertNotNull(tappedQueue);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.a2a.server.events;

/**
* Utils to access package private methods in the io.a2a.server.events package
*/
public class EventQueueTestHelper {
public static EventQueue tapQueue(EventQueue queue) {
return queue.tap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import java.util.concurrent.Executor;

import io.a2a.server.PublicAgentCard;
import io.a2a.server.requesthandlers.RequestHandler;
import io.a2a.server.util.async.Internal;
import io.a2a.spec.AgentCard;
import io.a2a.transport.grpc.handler.CallContextFactory;
import io.a2a.transport.grpc.handler.GrpcHandler;
Expand All @@ -20,14 +23,17 @@ public class QuarkusGrpcHandler extends GrpcHandler {
private final AgentCard agentCard;
private final RequestHandler requestHandler;
private final Instance<CallContextFactory> callContextFactoryInstance;
private final Executor executor;

@Inject
public QuarkusGrpcHandler(@PublicAgentCard AgentCard agentCard,
RequestHandler requestHandler,
Instance<CallContextFactory> callContextFactoryInstance) {
Instance<CallContextFactory> callContextFactoryInstance,
@Internal Executor executor) {
this.agentCard = agentCard;
this.requestHandler = requestHandler;
this.callContextFactoryInstance = callContextFactoryInstance;
this.executor = executor;
}

@Override
Expand All @@ -44,4 +50,9 @@ protected AgentCard getAgentCard() {
protected CallContextFactory getCallContextFactory() {
return callContextFactoryInstance.isUnsatisfied() ? null : callContextFactoryInstance.get();
}

@Override
protected Executor getExecutor() {
return executor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ public Response getStreamingSubscribedCount() {
return Response.ok(String.valueOf(streamingSubscribedCount.get()), TEXT_PLAIN).build();
}

@GET
@Path("/queue/childCount/{taskId}")
@Produces(TEXT_PLAIN)
public Response getChildQueueCount(@PathParam("taskId") String taskId) {
int count = testUtilsBean.getChildQueueCount(taskId);
return Response.ok(String.valueOf(count), TEXT_PLAIN).build();
}

@DELETE
@Path("/task/{taskId}/config/{configId}")
public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String taskId, @PathParam("configId") String configId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ public void getStreamingSubscribedCount(RoutingContext rc) {
.end(String.valueOf(streamingSubscribedCount.get()));
}

@Route(path = "/test/queue/childCount/:taskId", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
public void getChildQueueCount(@Param String taskId, RoutingContext rc) {
int count = testUtilsBean.getChildQueueCount(taskId);
rc.response()
.setStatusCode(200)
.end(String.valueOf(count));
}

@Route(path = "/test/task/:taskId/config/:configId", methods = {Route.HttpMethod.DELETE}, type = Route.HandlerType.BLOCKING)
public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String configId, RoutingContext rc) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
quarkus.arc.selected-alternatives=io.a2a.server.apps.common.TestHttpClient
quarkus.arc.selected-alternatives=io.a2a.server.apps.common.TestHttpClient
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ public void getStreamingSubscribedCount(RoutingContext rc) {
.end(String.valueOf(streamingSubscribedCount.get()));
}

@Route(path = "/test/queue/childCount/:taskId", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
public void getChildQueueCount(@Param String taskId, RoutingContext rc) {
int count = testUtilsBean.getChildQueueCount(taskId);
rc.response()
.setStatusCode(200)
.end(String.valueOf(count));
}

@Route(path = "/test/task/:taskId/config/:configId", methods = {Route.HttpMethod.DELETE}, type = Route.HandlerType.BLOCKING)
public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String configId, RoutingContext rc) {
try {
Expand Down
4 changes: 4 additions & 0 deletions server-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import mutiny.zero.BackpressureStrategy;
import mutiny.zero.TubeConfiguration;
import mutiny.zero.ZeroPublisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EventConsumer {
private static final Logger LOGGER = LoggerFactory.getLogger(EventConsumer.class);
private final EventQueue queue;
private Throwable error;

Expand All @@ -21,6 +24,7 @@ public class EventConsumer {

public EventConsumer(EventQueue queue) {
this.queue = queue;
LOGGER.debug("EventConsumer created with queue {}", System.identityHashCode(queue));
}

public Event consumeOne() throws A2AServerException, EventQueueClosedException {
Expand Down Expand Up @@ -107,4 +111,11 @@ public EnhancedRunnable.DoneCallback createAgentRunnableDoneCallback() {
}
};
}

public void close() {
// Close the queue to stop the polling loop in consumeAll()
// This will cause EventQueueClosedException and exit the while(true) loop
LOGGER.debug("EventConsumer closing queue {}", System.identityHashCode(queue));
queue.close();
}
}
Loading