fix(gateway): make resource teardown cancellation-safe

This commit is contained in:
Xubin Ren
2026-08-03 16:00:39 +08:00
parent a91ce900ef
commit 39e1533c3b
4 changed files with 118 additions and 24 deletions
+13 -12
View File
@@ -67,7 +67,7 @@ async def test_runtime_tasks_cancelled_before_resources_closed() -> None:
task = asyncio.create_task(_cancellable_task(events))
await asyncio.sleep(0) # let the task start (cancellation pre-start skips its body)
await _close_gateway_runtime(agent, channels, [task], None, False)
await _close_gateway_runtime(agent, channels, [task], None)
assert events == ["cancelled", "close_mcp"] # cancel happens before close
assert channels.stopped == 1
@@ -86,7 +86,7 @@ async def test_pending_background_work_is_drained_before_close_returns() -> None
agent.background = asyncio.create_task(background_work())
await _close_gateway_runtime(agent, channels, [], None, False)
await _close_gateway_runtime(agent, channels, [], None)
assert done["done"] is True
assert agent.close_calls == 1
@@ -98,14 +98,14 @@ async def test_stubborn_task_does_not_block_past_wait_timeout() -> None:
events: list[str] = []
task = asyncio.create_task(_stubborn_task(events))
await asyncio.sleep(0) # let the task start (cancellation pre-start skips its body)
runtime_tasks = asyncio.gather(task)
start = time.monotonic()
await _close_gateway_runtime(
agent,
channels,
[task],
None,
False,
runtime_tasks,
task_wait_timeout=0.05,
)
elapsed = time.monotonic() - start
@@ -113,7 +113,8 @@ async def test_stubborn_task_does_not_block_past_wait_timeout() -> None:
await asyncio.sleep(0) # let the swallowed cancellation handler run
assert "swallowed" in events # task was cancelled, then refused to die
assert not task.done() # still running despite cancellation
assert task.done() # the timed-out task received a second cancellation
assert runtime_tasks.done()
assert agent.close_calls == 1 # resources still closed underneath it
assert elapsed < 1.0 # bounded, not held open by the stubborn task
@@ -124,7 +125,7 @@ async def test_hanging_close_is_bounded_and_does_not_raise() -> None:
channels = _FakeChannels()
start = time.monotonic()
await _close_gateway_runtime(agent, channels, [], None, False, close_timeout=0.05)
await _close_gateway_runtime(agent, channels, [], None, close_timeout=0.05)
elapsed = time.monotonic() - start
assert agent.close_calls == 1
@@ -137,7 +138,7 @@ async def test_failing_close_is_logged_but_shutdown_proceeds() -> None:
agent.raise_on_close = True
channels = _FakeChannels()
await _close_gateway_runtime(agent, channels, [], None, False)
await _close_gateway_runtime(agent, channels, [], None)
assert agent.close_calls == 1
assert channels.stopped == 1 # teardown continued past the failure
@@ -148,20 +149,20 @@ async def test_duplicate_cleanup_is_idempotent() -> None:
channels = _FakeChannels()
task = asyncio.create_task(_cancellable_task([]))
await _close_gateway_runtime(agent, channels, [task], None, False)
await _close_gateway_runtime(agent, channels, [task], None, False)
await _close_gateway_runtime(agent, channels, [task], None)
await _close_gateway_runtime(agent, channels, [task], None)
assert agent.close_calls == 2 # second pass is a clean no-op
assert channels.stopped == 2
assert task.cancelled()
async def test_runtime_tasks_gather_is_awaited_when_not_drained() -> None:
async def test_finished_runtime_tasks_gather_is_retrieved() -> None:
agent = _FakeAgent()
channels = _FakeChannels()
runtime_tasks = asyncio.gather(asyncio.sleep(0))
await _close_gateway_runtime(agent, channels, [], runtime_tasks, False)
await _close_gateway_runtime(agent, channels, [], runtime_tasks)
assert runtime_tasks.done()
assert agent.close_calls == 1
@@ -173,7 +174,7 @@ async def test_cancelled_runtime_tasks_gather_does_not_raise() -> None:
runtime_tasks = asyncio.gather(asyncio.sleep(3600))
runtime_tasks.cancel()
await _close_gateway_runtime(agent, channels, [], runtime_tasks, False)
await _close_gateway_runtime(agent, channels, [], runtime_tasks)
assert runtime_tasks.done() # the cancelled gather was awaited without raising
assert agent.close_calls == 1