Skip to content

fix(server): cancel and await producer task before cleanup to prevent GC warning (Fixes #1121)#1125

Open
isheng-eqi wants to merge 2 commits into
a2aproject:mainfrom
isheng-eqi:fix/active-task-producer-gc-1121
Open

fix(server): cancel and await producer task before cleanup to prevent GC warning (Fixes #1121)#1125
isheng-eqi wants to merge 2 commits into
a2aproject:mainfrom
isheng-eqi:fix/active-task-producer-gc-1121

Conversation

@isheng-eqi

Copy link
Copy Markdown

Summary

Fix an asyncio "Task was destroyed but it is pending!" warning in ActiveTask that fires under continuous load when the consumer finishes before the producer on the normal-completion teardown path.

Problem

ActiveTask has two teardown paths with asymmetric producer cleanup:

  1. Cancel path (cancel()): cancels _producer_task, agent cancels, waits for _is_finished → clean teardown
  2. Normal completion (_run_consumer finally): sets _is_finished, calls _maybe_cleanup()_on_cleanup(self) → releases ActiveTask reference without cancelling or awaiting the producer

When the consumer finishes first and the reference is dropped, the still-pending producer is GC'd while parked on _request_queue.get() (or inside its finally block at line 566), triggering:

ERROR asyncio Task was destroyed but it is pending!
task: <Task pending name='producer:<uuid>' coro=<ActiveTask._run_producer() running at active_task.py:566>>

Observed 103× over a multi-hour run under continuous load per #1121.

Fix

In _run_consumer's finally block, cancel and await the producer task before _maybe_cleanup(), mirroring what cancel() already does:

if self._producer_task and not self._producer_task.done():
    self._producer_task.cancel()
    with contextlib.suppress(asyncio.CancelledError):
        await self._producer_task

This ensures the producer's finally block (which closes event queues) completes deterministically before the ActiveTask reference is released.

Test Plan

  • New test test_producer_cancelled_on_normal_completion: simulates consumer finishing while producer is pending, verifies producer is cancelled + awaited + cleanup callback fires
  • ruff check + ruff format + ty check pass
  • Existing tests not affected (12 passing, 1 hang is pre-existing Windows asyncio IOCP issue)

Closes #1121

… GC warning (Fixes a2aproject#1121)

On the normal-completion teardown path, _run_consumer's finally block
was releasing the ActiveTask reference (via _maybe_cleanup) before the
producer task had finished, leaving it parked on _request_queue.get().
When GC reclaimed the ActiveTask, the still-pending producer was
destroyed, triggering asyncio 'Task was destroyed but it is pending!'
warnings.

Fix: cancel _producer_task and await it (suppressing CancelledError)
before calling _maybe_cleanup(), mirroring what cancel() already does
on the explicit cancel path. This ensures deterministic teardown and
prevents the GC warning.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request ensures that the producer task is cancelled and awaited before cleanup when the consumer finishes first, preventing asyncio "Task was destroyed but it is pending!" warnings. A regression test has been added to verify this behavior. I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActiveTask producer task GC'd while pending — 'Task was destroyed but it is pending!' at turn boundaries

1 participant