Skip to content

feat(server): release parked interrupted tasks after a configurable timeout - #1338

Open
christophstach wants to merge 1 commit into
a2aproject:mainfrom
christophstach:feat/interrupted-task-timeout
Open

christophstach wants to merge 1 commit into
a2aproject:mainfrom
christophstach:feat/interrupted-task-timeout

Conversation

@christophstach

Copy link
Copy Markdown
Contributor

Description

Second half of #1296, following the direction agreed in #1296 (comment) and the maintainer's reply.

A task in input-required or auth-required keeps its ActiveTask (producer, consumer, two dispatchers, registry entry) until a follow-up arrives, which may never happen.

Change

  • New DefaultRequestHandlerV2(..., interrupted_task_timeout: float | None = None). The default None keeps today's behavior.
  • When the timeout is set, ActiveTask starts a timer once a request finishes or the last subscriber leaves. A new request or subscriber cancels it. When it fires, the ActiveTask is released only if all of these still hold:
    • the task is in an interrupted state,
    • no request is running or queued,
    • no subscriber is attached.
  • The stored task is not modified. A follow-up recreates the ActiveTask from the store.
  • 0 releases as soon as the task is idle.

Registry fix

Removing a finished ActiveTask from the registry runs asynchronously. A follow-up that arrived in between was rejected with Task … is already completed. get_or_create now replaces a finished entry, and the scheduled removal only deletes the instance it was scheduled for. Side effect without a timeout: a follow-up to a task that just ended now gets is in terminal state: …, the same error as outside that window.

Tests

Seven new tests in test_default_request_handler_v2.py:

  • no timeout by default,
  • release for input-required and auth-required, with no leftover asyncio tasks and the stored state unchanged,
  • resume from the store after a release,
  • reset by a follow-up,
  • reset by a subscriber,
  • no release while the executor is still running,
  • a follow-up to a finished but still registered ActiveTask (this one fails on main).

Checks

  • ./scripts/lint.sh: passed (the 3 ty warnings already exist on main).
  • uv run pytest: 2285 passed. The new tests passed 10/10 repeated runs and also pass on Python 3.14.
  • Not tested: cluster mode (event_stream).

Part of #1296

…imeout

A task in input-required or auth-required keeps its ActiveTask (producer,
consumer, two dispatchers, registry entry) until a follow-up arrives, which
may never happen. Add `interrupted_task_timeout` to DefaultRequestHandlerV2:
after that many seconds with no request running and no subscriber attached,
the ActiveTask is finished. The stored task is not modified; a follow-up
recreates the ActiveTask from the task store. The default, None, keeps the
current behavior.

The registry now replaces a finished ActiveTask that is still registered,
because its removal runs asynchronously and a follow-up arriving in between
was rejected with "already completed". Removal only deletes the instance it
was scheduled for, so it cannot drop the replacement.

Part of a2aproject#1296
@github-actions

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/agent_execution/active_task.py 95.14% 95.46% 🟢 +0.33%
src/a2a/server/agent_execution/active_task_registry.py 97.37% 97.65% 🟢 +0.28%
src/a2a/server/cluster/database_event_stream.py 96.94% 92.86% 🔴 -4.08%
Total 93.16% 93.15% 🔴 -0.01%

Generated by coverage-comment.yml

@christophstach
christophstach marked this pull request as ready for review October 14, 2026 12:29
@christophstach
christophstach requested a review from a team as a code owner October 14, 2026 12:29
@Iwaniukooo11 Iwaniukooo11 self-assigned this Oct 14, 2026
@Iwaniukooo11 Iwaniukooo11 added the component: server Issues related to frameworks for agent execution, HTTP/event handling, database persistence logic. label Oct 14, 2026
task_store=self._versioned_store,
push_sender=self._push_sender,
event_stream=self._event_stream,
interrupted_task_timeout=interrupted_task_timeout,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate that timeout value>0

name=f'idle-release:{self._task_id}',
)

def _cancel_idle_release(self) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add docstring

self._idle_release_task.cancel()
self._idle_release_task = None

async def _release_when_idle(self, timeout: float) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please introduce try/catch to handle self._task_manager.get_task() and asyncio.CancelledError (in fact asyncio.sleep handles that, but it's good practise to put async threads in try/catch to ensure proper cancellation)

# set, which is the on_message_send create path establishing
# ownership. Masked as not-found so existence is not leaked. Done
# outside _lock because the store read is I/O and the miss-path
# check runs outside the lock too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here _cancel_idle_release should happen. Since we want to cancel timeout every time some action is taken (which requires calling get_or_create), placing cancel here ensures that timeout is cleaned

f'Task {self._task_id} is already completed.'
)
self._reference_count += 1
self._cancel_idle_release()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be moved above async lock

result = await _send(handler, 'Friday', task)

assert result.status.state == TaskState.TASK_STATE_COMPLETED
await handler.aclose()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider testing edge case scenario, when timeout is extremely close to 0



@pytest.mark.asyncio
async def test_subscriber_keeps_interrupted_task_active() -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure each test has a docstring

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: server Issues related to frameworks for agent execution, HTTP/event handling, database persistence logic.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants