Skip to content

fix(core): isolate shared initialization cancellation - #745

Open
hsusul wants to merge 1 commit into
googleapis:mainfrom
hsusul:fix/core-shield-shared-initialization
Open

fix(core): isolate shared initialization cancellation#745
hsusul wants to merge 1 commit into
googleapis:mainfrom
hsusul:fix/core-shield-shared-initialization

Conversation

@hsusul

@hsusul hsusul commented Jul 28, 2026

Copy link
Copy Markdown

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Communicate test infrastructure changes, i.e. API enablement, secrets — no test infrastructure changes
  • Appropriate docs were updated (if necessary) — no documentation change is needed for this internal lifecycle correction

🛠️ Fixes #743

Summary

  • Isolate the shared MCP initialization task from cancellation of an individual request waiter.
  • Add deterministic regression coverage for concurrent and later initialization waiters.

Problem

All first MCP requests share one _init_task so session initialization runs once. However, _ensure_initialized() awaited that task directly. Python therefore propagated cancellation from any waiting request into the shared task.

A minimal reproduction starts two _ensure_initialized() waiters while initialization is blocked on an event, then cancels only the first waiter.

Before this change:

first_waiter=cancelled
second_waiter=cancelled
shared_init_cancelled=True
later_initialization=cancelled

The cancelled request unintentionally cancelled unrelated concurrent and later requests.

Root cause

_McpHttpTransportBase._ensure_initialized() retained one shared task but used await self._init_task. Cancellation of a coroutine awaiting another task is propagated to that task unless it is shielded.

Solution

Await the retained initialization task through asyncio.shield(). The calling request still receives CancelledError, while the shared initialization continues for other waiters.

Genuine initialization exceptions are unchanged and still propagate to waiters.

Tests

  • python -m pytest tests/mcp_transport/test_base.py::TestMcpHttpTransportBase::test_cancelled_waiter_does_not_cancel_shared_initialization -v
    • Before the fix: failed because the surviving waiter returned CancelledError.
    • After the fix: 1 passed.
  • python -m pytest tests/mcp_transport/test_base.py -q — 38 passed.
  • python -m pytest tests -q --ignore=tests/test_e2e.py --ignore=tests/test_sync_e2e.py --ignore=tests/test_e2e_mcp.py --ignore=tests/conformance --cov=src/toolbox_core --cov-report=term --cov-fail-under=90 — 481 passed, 15 warnings, 91.71% coverage.
  • python -m pytest tests/unit -q in toolbox-adk — 59 passed, 10 dependency deprecation warnings.
  • python -m pytest -k "not e2e" -q in toolbox-langchain — 60 passed, 64 deselected.
  • python -m pytest -k "not e2e" -q in toolbox-llamaindex — 61 passed, 64 deselected.

Validation

  • black --check . — passed; 44 files unchanged.
  • isort --check . — passed.
  • MYPYPATH='./src' mypy --install-types --non-interactive --cache-dir=.mypy_cache/ -p toolbox_core — passed; no issues in 29 source files.
  • git diff upstream/main...HEAD --check — passed.
  • Live Toolbox/GCP E2E, Cloud Build integration, and MCP conformance were not run locally because they require external services or maintainer-triggered infrastructure.

Compatibility and risk

There is no public API, schema, serialization, authentication, telemetry, or successful-call behavior change. The change applies uniformly to every MCP protocol implementation through the shared transport base.

Shielding means initialization can finish after the request that first triggered it is cancelled. This is intentional because the task is retained by the transport and may already serve other requests.

Non-goals

Documentation

No documentation change is required because this restores internal lifecycle isolation without changing the public interface.

Related issues and prior work

AI assistance

This issue investigation and patch were prepared with AI assistance. The reproduction, regression sensitivity, diff, and validation results were verified locally against current upstream/main.

@hsusul
hsusul requested a review from a team as a code owner July 28, 2026 15:22
@anubhav756 anubhav756 added the priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release. label Aug 11, 2026
@anubhav756

Copy link
Copy Markdown
Contributor

/gcbrun

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

Labels

priority: p1 Important issue which blocks shipping the next release. Will be fixed prior to next release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(core): cancelled waiter cancels shared MCP initialization

2 participants