Skip to content

fix(sdk/python): harden cross-loop teardown - #907

Merged
AbirAbbas merged 2 commits into
Agent-Field:mainfrom
mikemikimike:codex/901-cross-loop-hardening
Aug 12, 2026
Merged

fix(sdk/python): harden cross-loop teardown#907
AbirAbbas merged 2 commits into
Agent-Field:mainfrom
mikemikimike:codex/901-cross-loop-hardening

Conversation

@mikemikimike

Copy link
Copy Markdown
Contributor

Fixes #901

Summary

  • Serialize cross-loop ConnectionManager teardown on the owning event loop and its async lock, including task/resource state updates.
  • Make repeated foreign-loop close() calls return safely while teardown is pending, and keep get_session() from yielding a torn-down session.
  • Only suppress the known cross-loop RuntimeError; propagate unrelated task-cleanup failures.

Tests

  • uv run --frozen --extra dev pytest tests/test_async_lifecycle_deadlock.py tests/test_http_connection_manager.py -q (21 passed)
  • Relevant async execution, result-cache, client lifecycle, HTTP, and lifecycle tests (passed)
  • Ruff 0.15.22 check (passed)
  • Full local suite reached 100%; 11 unrelated Windows-environment failures remain because the host uses GBK for UTF-8 pyproject.toml reads and has no bash executable.

@mikemikimike
mikemikimike requested review from a team and AbirAbbas as code owners August 12, 2026 07:00
@CLAassistant

CLAassistant commented Aug 12, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

Copy link
Copy Markdown
Contributor

Performance

SDK Memory Δ Latency Δ Tests Status
Python 9.0 KB - 0.32 µs -9%

✓ No regressions detected

@github-actions

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.10% 87.40% ↓ -0.30 pp 🟡
sdk-go 92.70% 92.00% ↑ +0.70 pp 🟢
sdk-python 94.18% 93.73% ↑ +0.45 pp 🟢
sdk-typescript 91.17% 90.42% ↑ +0.75 pp 🟢
web-ui 84.76% 84.79% ↓ -0.03 pp 🟡
aggregate 85.62% 85.75% ↓ -0.13 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 0 ➖ no changes
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

AbirAbbas
AbirAbbas previously approved these changes Aug 12, 2026

@AbirAbbas AbirAbbas 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.

Verified at d110f9a in a clean clone. The anti-tautology check is the convincing part: reverting just the two source files to main (tests kept) reproduces both #901 items exactly — the idempotency test fails with is bound to a different event loop (the double-close race) and the propagation test fails with DID-NOT-RAISE (the blanket RuntimeError swallow). Gates: ruff 0.15.22 clean, full run_pytest.sh green on 3.10 and 3.12, websockets-compat 12.0/15.0.1, and 12 consecutive repeats of the connection/deadlock suites with zero flakes. Edge probes: stopped owner loop → close() returns immediately, no hang (session leak in that case exists on main too); closed loop → graceful; restart after cross-loop close → correctly rejected. No collision with #904 (no file overlap).

Two hardening notes inline on the same code this PR touches — the first is one line and I'd fold it in here; the second is fine as a follow-up. Minor test-coverage note: the strengthened test_connection_manager_cross_loop_close_no_deadlock also passes on main, and _close_requested/the get_session() capture ordering have no direct test references — the real regression guards are the two new tests, which do cover both #901 items.

Comment thread sdk/python/agentfield/async_lifecycle.py Outdated
Comment thread sdk/python/agentfield/http_connection_manager.py
…k teardown

cancel_and_await_if_same_loop only suppressed the asyncio/tasks.py wording
("attached to a different loop"). CPython raises the same error class with a
second wording from asyncio/mixins.py ("is bound to a different event loop")
when a cancellation cleanup awaits a Lock/Event/Condition bound to a foreign
loop, so that case re-raised out of teardown instead of being absorbed.

Match both wordings. The regression test binds an asyncio.Event to a
background thread's loop and awaits it from a cancelled task's cleanup on
another loop, reproducing the mixins wording genuinely rather than raising a
synthetic RuntimeError; the existing test asserting unrelated RuntimeErrors
still propagate is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@AbirAbbas AbirAbbas 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.

Re-approving at 0e1f663 (my push dismissed the earlier stamp). The wording fix is in with a regression test; independent verification reproduced both directions live — a cleanup awaiting a foreign-loop-bound primitive is now absorbed, an unrelated RuntimeError still propagates — and confirmed the test fails against d110f9a's helper. Full Python gates green (ruff 0.15.22, full suite, deadlock suite 5x). The atomicity follow-up is #909.

@AbirAbbas
AbirAbbas added this pull request to the merge queue Aug 12, 2026
Merged via the queue into Agent-Field:main with commit a2ea9e6 Aug 12, 2026
23 checks passed
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.

Follow-up hardening from #799: cross-thread ConnectionManager teardown + async_lifecycle exception scope

3 participants