feat(server): add multi-replica cluster mode (#1281) · ningyixi/a2a-python@942d621 · GitHub
Skip to content

Commit 942d621

Browse files
authored
feat(server): add multi-replica cluster mode (a2aproject#1281)
Adds a2a.server.cluster, key interfaces (__VersionedTaskStore__ and __TaskEventStream__) for running the A2A server as several replicas, with no need to route a task's requests back to the replica that started it, and wires DefaultRequestHandlerV2 to use them so any replica can serve any request for a remote task. __Opt-in and backward compatible:__ with no event stream and a plain (non-versioned) TaskStore, pre-existing behavior is preserved. __Key changes:__ - VersionedTaskStore - a task store that keeps track of which revision of a task a replica last read. When that replica saves, the store checks the task has not changed in the meantime and refuses the write / retries if it has, instead of quietly overwriting whatever another replica just did. VersionedDatabaseTaskStore is the database-backed implementation. - TaskEventStream - a shared record of the events a task produces, so a replica that is not running the agent can still see the event stream. This lets a client reconnect to any replica and keep receiving updates for a task running elsewhere. DatabaseTaskEventStream is the database-backed implementation. - Schema for both: a new task_versions table and a new task_events table. The tasks table is unchanged. - New optional event_stream argument; task_store now accepts a plain or a versioned store and is still exposed unchanged as self.task_store. - Cancel works from any replica. The cancelling replica records the cancellation in the shared store, and the replica actually running the agent notices on its next save and stops. - Resubscribe works from any replica. It returns the current task, then follows the shared event stream from that point on, so the client sees no gap and no duplicates. - Alembic revision b5e3d1c8a2f7 (revises 38ce57e08137) creates the task_versions table and the task_events table with an index on task_id. It only creates tables and does not alter existing ones. - A runnable sample showing how to deploy the server as several replicas: it starts N replicas as separate processes sharing one database, and a driver starts a task on one replica, watches it from a second, and cancels it from a third. Defaults to a shared SQLite file so it runs with no external services; set A2A_CLUSTER_DSN to point every replica at Postgres or MySQL instead. __Tested:__ - Integration tests run two DefaultRequestHandlerV2 replicas against one real Postgres or MySQL database, each replica on its own engine. They cover send, resubscribe and cancel handled by a replica other than the one running the agent, and a multi-turn input_required conversation that moves between replicas. - An integration test sends follow-ups to the same task from both replicas at once and asserts that neither turn is lost. - Unit tests cover the same cross-replica flows without a database, using in-memory versions of the shared store and event stream. - Unit tests for the migration's full up/down cycle, idempotency and offline SQL generation. - Adds the database-backed cluster tests to scripts/run_db_tests.sh. Run the sample from the repository root: ``` python -m samples.clustermode.run_cluster --replicas 3 --base-port 41241 python -m samples.clustermode.exercise --host 127.0.0.1 --ports 41241,41242,41243 ``` __Design discussion:__ a2aproject#1224 Release-As: 1.2.0
1 parent dc5a5da commit 942d621

34 files changed

Lines changed: 5315 additions & 59 deletions

‎.github/actions/spelling/allow.txt‎

Lines changed: 7 additions & 0 deletions

‎samples/clustermode/README.md‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Cluster-mode sample (multi-replica A2A)
2+
3+
Runs several A2A server replicas that share one durable task store and one event
4+
stream, so any replica can serve any request for any task. This demonstrates the
5+
multi-server support in `a2a.server.cluster`: send, resubscribe, and cancel all
6+
work regardless of which replica a request lands on — no sticky routing needed.
7+
8+
## What it shows
9+
10+
- **Shared, versioned task store** (`VersionedDatabaseTaskStore`) — concurrent
11+
writes across replicas are serialized by optimistic concurrency (CAS); no lost
12+
updates.
13+
- **Shared event stream** (`DatabaseTaskEventStream`) — a subscription on one replica
14+
streams events produced by an agent running on another.
15+
- **Cancel via CAS** — a cancel on replica C stops an agent running on replica A
16+
(A's next save fails the compare-and-swap and aborts).
17+
18+
## Requirements
19+
20+
Nothing extra by default: it uses a file-backed SQLite database
21+
(`/tmp/a2a_cluster_demo.db`) shared by all replica processes.
22+
23+
For a realistic setup, point every replica at a shared Postgres/MySQL:
24+
25+
```bash
26+
export A2A_CLUSTER_DSN='postgresql+asyncpg://user:pass@localhost/a2a'
27+
```
28+
29+
## Run
30+
31+
Start a 3-replica cluster (ports 41241, 41242, 41243):
32+
33+
```bash
34+
python -m samples.clustermode.run_cluster --replicas 3 --base-port 41241
35+
```
36+
37+
In another terminal, exercise it (one task, spread across replicas):
38+
39+
```bash
40+
python -m samples.clustermode.exercise --host 127.0.0.1 --ports 41241,41242,41243
41+
```
42+
43+
Expected output ends with:
44+
45+
```
46+
OK: one task was started, observed, and cancelled across three different replicas.
47+
```
48+
49+
## Files
50+
51+
| File | Purpose |
52+
|------|---------|
53+
| `cluster_common.py` | Shared store/stream wiring, agent card, and the demo agent |
54+
| `server.py` | One replica (a FastAPI app on one port) |
55+
| `run_cluster.py` | Launches N replicas as subprocesses |
56+
| `exercise.py` | Client that sends/gets/cancels one task across replicas |
57+
58+
## How it maps to the API
59+
60+
Every replica builds its handler the same way — the only multi-server-specific
61+
part is passing a shared `event_stream` and a shared `VersionedTaskStore`:
62+
63+
```python
64+
handler = DefaultRequestHandler(
65+
agent_executor=SlowEchoAgent(...),
66+
task_store=VersionedDatabaseTaskStore(engine=..., create_table=False),
67+
agent_card=agent_card,
68+
event_stream=DatabaseTaskEventStream(engine=..., create_table=False),
69+
)
70+
```
71+
72+
Omit `event_stream` (and use a plain `InMemoryTaskStore`) and you get the ordinary
73+
single-process behaviour — the multi-server path is fully opt-in.
74+
```

‎samples/clustermode/__init__.py‎

Whitespace-only changes.
Lines changed: 171 additions & 0 deletions

0 commit comments

Comments
 (0)