Repository navigation
feat(server): recover tasks abandoned by a crashed replica - #1395
Draft
mykytanetipa wants to merge 1 commit into
Draft
mykytanetipa wants to merge 1 commit into
mykytanetipa wants to merge 1 commit into
Conversation
In cluster mode, a replica that dies mid-turn leaves its task WORKING in the shared store, and no other replica ever finishes it. - A turn now holds a lease on its task (TaskLeaseStore, with a database implementation and migration). The running replica renews it on a heartbeat and releases it when the turn ends, including when the task waits for input. - fail_abandoned_tasks / AbandonedTaskSweeper mark tasks FAILED once their lease expires. Every replica can run the sweeper; each expired lease is claimed by one of them. A task already waiting for input recorded the end of its turn, so only its lease is dropped. - A follow-up arriving while another replica holds the lease is rejected with UnsupportedOperationError instead of starting a second executor. If the task already waits for input, the other replica is about to release the lease, so the follow-up waits for it for up to one heartbeat interval. - A replica that loses its lease stops its executor, and its late events are dropped. - The local task a replica created for a rejected follow-up is released as soon as it is idle, so a later subscription on that replica follows the shared event stream instead of a task that never runs there. - DefaultRequestHandler.aclose() marks turns still running on this replica FAILED. On by default when an event stream is configured. The lease store is optional; without it, only aclose() behaves differently. The clustermode sample wires it up and documents how to try a crash. Closes #1324
🧪 Code Coverage (vs
|
| Base | PR | Delta | |
|---|---|---|---|
| src/a2a/server/agent_execution/active_task.py | 95.80% | 95.67% | 🔴 -0.13% |
| src/a2a/server/cluster/database_event_stream.py | 96.94% | 92.86% | 🔴 -4.08% |
| src/a2a/server/cluster/database_task_store.py | 99.31% | 99.32% | 🟢 +0.01% |
| src/a2a/server/events/event_queue_v2.py | 92.27% | 91.79% | 🔴 -0.48% |
| src/a2a/server/models.py | 84.72% | 87.06% | 🟢 +2.34% |
| src/a2a/server/request_handlers/default_request_handler_v2.py | 88.57% | 89.30% | 🟢 +0.73% |
| src/a2a/utils/proto_utils.py | 95.78% | 95.85% | 🟢 +0.07% |
| src/a2a/utils/telemetry.py | 91.47% | 90.70% | 🔴 -0.78% |
| src/a2a/migrations/versions/b0f01b04ff26_add_task_leases.py (new) | — | 77.27% | — |
| src/a2a/server/cluster/database_lease_store.py (new) | — | 97.22% | — |
| src/a2a/server/cluster/lease.py (new) | — | 100.00% | — |
| src/a2a/server/cluster/recovery.py (new) | — | 100.00% | — |
| Total | 93.24% | 93.29% | 🟢 +0.05% |
Generated by coverage-comment.yml
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In cluster mode nothing finishes a task whose replica dies mid-turn, and nothing stops two replicas from running the same task at once.
Fixes #1324
Example:
sequenceDiagram participant C as Client participant A as Replica A (runs agent) participant S as Shared store participant B as Replica B C->>A: SendMessage(T) A->>S: T is WORKING C->>B: SendMessage(T, follow-up) Note over B: accepted, starts a second executor on T Note over A: killed (deploy, OOM) C->>B: GetTask(T) B-->>C: WORKING, and stays WORKING for good Note over C: a client subscribed through B waits foreverFix: a replica now holds a lease on a task while it runs a turn. It renews the lease on a heartbeat and releases it when the turn ends, including when the task stops to wait for input. AbandonedTaskSweeper, which every replica can run, claims leases that have expired and marks their tasks FAILED. Each expired lease is claimed by exactly one replica, and the FAILED write goes to the event stream and push notifications like any other. A task that was already waiting for input had finished its turn, so the sweeper only drops its lease.
A follow-up that arrives while another replica holds the lease is rejected with UnsupportedOperationError. If the task is already waiting for input, the other replica is about to release the lease, so the follow-up waits for it for up to one heartbeat interval instead of being rejected. A replica that rejected a follow-up does not keep a local copy of the task, so a later SubscribeToTask there follows the shared event stream. A replica that loses its lease, e.g. cut off from the database for longer than the TTL, stops its executor and its late events are dropped. DefaultRequestHandlerV2.aclose() also marks turns still running on the closing replica FAILED, which is on by default when an event stream is configured.
In the example, the follow-up sent to B is rejected while A holds the lease, and after A dies B marks T FAILED one TTL later and the subscribed client receives it.
The lease store is optional; without it, only aclose() behaves differently. It works alongside interrupted_task_timeout (#1338). Alembic revision b0f01b04ff26 (revises b5e3d1c8a2f7) adds the task_leases table and does not alter existing ones. The clustermode sample wires up the lease store and the sweeper, and its README shows how to try a crash.
Tested: multi-replica and DatabaseTaskLeaseStore tests (SQLite, Postgres, MySQL), migration tests, and kill -9 of a replica in the sample, where the task goes FAILED instead of staying WORKING.