Multi-server deployment #1224
mykytanetipa
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
on_message_send/on_message_send_streamBoth handlers share
_setup_active_task(https://github.com/a2aproject/a2a-python/blob/v1.1.3/src/a2a/server/request_handlers/default_request_handler_v2.py#L192), so theyshare their multi-server problems. 3 issues:
Issue 1.1: Two replicas writing one task silently lose each other's writes
Issue:
TaskStore.save()has no version parameter and no return value, so noimplementation can reject a stale write.
Concurrent turns on two replicas produce last-writer-wins with no error anywhere.
Current behaviour:
The shipped DB implementation (server/tasks/database_task_store.py):
2 replicas do concurrent write on same task_id -> Both
savecalls returnNone. Both replicas believe they succeeded. (while 1 was silently overwritten)What the Go SDK did.
Go put the version in the task store interface (https://github.com/a2aproject/a2a-go/blob/v0.3.15/a2asrv/tasks.go#L59):
Proposed fix (Python SDK):
A parallel implementation of VersionedTaskStore, no modification for exsisting to exsisting interface to avoid breaking changes.
only is_missing and is_after are used in code
Issue 1.2: Caching an
ActiveTaskskips the terminal-state checkIssue: The cached path returns an existing
ActiveTaskwithout callingstart()(contains the only terminal guard), so the guard that rejects work on a completed task never runs -> on a multi-replicadeployment a task can reach a terminal state on another replica while this one still holds a live
ActiveTaskfor it.Current behaviour:
What the Go SDK did: NaN, the check is not attached to a cached object, store is re-read on every entry point.
and
Resubscribelikewise consults the store rather than a registry(
distributed_manager.go:62). Because Go's distributed manager holds no per-task state, there isno cache to go stale. This is the payoff of the "no in-process map" property, not a separate fix.
e.g. Task becomes COMPLETED on 1st replica, but 2nd can still wait in the INPUT_REQUIRED and new request on it would skip the terminal guard and rewrite the status (rewrite in concurrent requests case).
Proposed fix.
Issue 1.3:
input_required/ multi-turn resume across replicas (fixed by 1.2)An
ActiveTasksurvivesinput_required, so the next turn arriving after another replica advanced the task is computed against a stale snapshot.on_subscribe_to_taskIssue 2.1: Resubscribe on the wrong replica hangs forever
Issue:
Without a shared store the task does not exist on the other replica, so start() raises TaskNotFoundError -> the resubscribe fails outright.
With a shared store the task is found, so start() succeeds, but on this replica it means creating a new ActiveTask with its own producer / consumer pair. That pair has no work to do (the agent runs on the other replica), so the client receives only the existing task snapshot and then the stream hangs with no further events.
+ resource leak
What the Go SDK did:
Go treats resubscribe as "read the snapshot, then tail a shared log", with the store as the source
of truth for existence.
distributedManager.Resubscribe(internal/taskexec/distributed_manager.go:61-70):No registry lookup, no producer spawned. The real work is in
remoteSubscription.Events(internal/taskexec/subscription.go):
2 main steps:
m.queueManageris the injectedeventqueue.ManagerfromClusterConfig(a2asrv/handler.go). In the reference cluster deployment it polls the shared event table (examples/clustermode/server/eventqueue.go)::135. Events already reflected in the snapshot are dropped, so a fan our consumer does not duplicate.Proposed fix:
+ fix events fan out to subscribers:
No breaking changes:
ActiveTaskgains an optional_event_bus, defaulting toNone, which preserves today'ssingle-process behaviour exactly.
on_cancel_taskIssue 3.1 — Cancel on the wrong replica doesn't cancel actual executing task
Issue: Same as subscribe: with shared Task Store new ActiveTask instance is created for another replica process -> cancel on the wrong object -> main replica still runs task
Current behaviour: (same get_or_create())
The handler (
default_request_handler_v2.py:160-181):What the Go SDK did:
Go's distributed cancel never waits on a local object. It writes to the shared work queue and
waits on the shared event stream (internal/taskexec/distributed_manager.go):
Proposed fix:
implement _cancel_remote that relies on Task Store:
+ ensure that running task is actually canceled:
All reactions