fix(mcp): close timer shutdown scheduling race
CI / load-versions (pull_request) Successful in 27s
CI / push-validation (pull_request) Successful in 35s
CI / lint (pull_request) Successful in 38s
CI / typecheck (pull_request) Successful in 1m1s
CI / security (pull_request) Successful in 1m7s
CI / build (pull_request) Successful in 32s
CI / quality (pull_request) Successful in 1m14s
CI / helm (pull_request) Successful in 40s
CI / unit_tests (pull_request) Successful in 5m40s
CI / docker (pull_request) Successful in 1m44s
CI / integration_tests (pull_request) Successful in 9m17s
CI / coverage (pull_request) Successful in 9m48s
CI / status-check (pull_request) Successful in 3s
CI / load-versions (push) Successful in 11s
CI / push-validation (push) Successful in 27s
CI / lint (push) Successful in 44s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 1m2s
CI / build (push) Successful in 37s
CI / security (push) Successful in 1m10s
CI / helm (push) Successful in 40s
CI / unit_tests (push) Successful in 5m4s
CI / docker (push) Successful in 1m34s
CI / integration_tests (push) Successful in 8m43s
CI / coverage (push) Successful in 9m39s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / load-versions (pull_request) Successful in 27s
CI / push-validation (pull_request) Successful in 35s
CI / lint (pull_request) Successful in 38s
CI / typecheck (pull_request) Successful in 1m1s
CI / security (pull_request) Successful in 1m7s
CI / build (pull_request) Successful in 32s
CI / quality (pull_request) Successful in 1m14s
CI / helm (pull_request) Successful in 40s
CI / unit_tests (pull_request) Successful in 5m40s
CI / docker (pull_request) Successful in 1m44s
CI / integration_tests (pull_request) Successful in 9m17s
CI / coverage (pull_request) Successful in 9m48s
CI / status-check (pull_request) Successful in 3s
CI / load-versions (push) Successful in 11s
CI / push-validation (push) Successful in 27s
CI / lint (push) Successful in 44s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 1m2s
CI / build (push) Successful in 37s
CI / security (push) Successful in 1m10s
CI / helm (push) Successful in 40s
CI / unit_tests (push) Successful in 5m4s
CI / docker (push) Successful in 1m34s
CI / integration_tests (push) Successful in 8m43s
CI / coverage (push) Successful in 9m39s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
This commit was merged in pull request #11159.
This commit is contained in:
committed by
Forgejo
parent
a103d755ce
commit
ab983e5ccb
@@ -42,6 +42,19 @@ def _mock_tool(name: str, desc: str = "", schema: dict | None = None) -> dict:
|
||||
}
|
||||
|
||||
|
||||
class _ShutdownOnCancelTimer:
|
||||
"""Timer probe that marks shutdown while scheduler cancellation is active."""
|
||||
|
||||
def __init__(self, client: McpClient) -> None:
|
||||
self._client = client
|
||||
self.cancel_called = False
|
||||
|
||||
def cancel(self) -> None:
|
||||
self.cancel_called = True
|
||||
with self._client._lock:
|
||||
self._client._shutting_down = True
|
||||
|
||||
|
||||
# ── Given ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -117,6 +130,18 @@ def step_mark_timer_race_client_shutting_down(context: Context) -> None:
|
||||
context.timer_race_client._shutting_down = True
|
||||
|
||||
|
||||
@given("cancellation of existing timers begins shutdown")
|
||||
def step_existing_timer_cancellation_begins_shutdown(context: Context) -> None:
|
||||
"""Install cancel probes that flip shutdown after the scheduler's first guard."""
|
||||
client = context.timer_race_client
|
||||
context.timer_race_idle_cancel_probe = _ShutdownOnCancelTimer(client)
|
||||
context.timer_race_health_cancel_probe = _ShutdownOnCancelTimer(client)
|
||||
with client._lock:
|
||||
client._idle_timer = context.timer_race_idle_cancel_probe
|
||||
client._health_timer = context.timer_race_health_cancel_probe
|
||||
client._shutting_down = False
|
||||
|
||||
|
||||
# ── When ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -245,8 +270,12 @@ def step_wait_for_late_fires(context: Context) -> None:
|
||||
@when("I ask the client to schedule idle and health timers")
|
||||
def step_schedule_idle_and_health_timers(context: Context) -> None:
|
||||
"""Invoke both scheduler guards while shutdown is in progress."""
|
||||
context.timer_race_client._schedule_idle_timer()
|
||||
context.timer_race_client._schedule_health_check()
|
||||
client = context.timer_race_client
|
||||
client._schedule_idle_timer()
|
||||
if hasattr(context, "timer_race_health_cancel_probe"):
|
||||
with client._lock:
|
||||
client._shutting_down = False
|
||||
client._schedule_health_check()
|
||||
|
||||
|
||||
# ── Then ──────────────────────────────────────────────────────────
|
||||
@@ -281,3 +310,14 @@ def step_no_idle_or_health_timer_scheduled(context: Context) -> None:
|
||||
client = context.timer_race_client
|
||||
assert client._idle_timer is None, "Idle timer was scheduled during shutdown"
|
||||
assert client._health_timer is None, "Health timer was scheduled during shutdown"
|
||||
|
||||
|
||||
@then("cancellation should have been observed for both timers")
|
||||
def step_cancellation_observed_for_both_timers(context: Context) -> None:
|
||||
"""Assert both scheduler paths reached the cancellation probe."""
|
||||
assert context.timer_race_idle_cancel_probe.cancel_called, (
|
||||
"Idle timer cancellation probe was not reached"
|
||||
)
|
||||
assert context.timer_race_health_cancel_probe.cancel_called, (
|
||||
"Health timer cancellation probe was not reached"
|
||||
)
|
||||
|
||||
@@ -82,3 +82,10 @@ Feature: TDD Issue #10516 — McpClient timer can fire after cancellation race
|
||||
And the timer race client is marked as shutting down
|
||||
When I ask the client to schedule idle and health timers
|
||||
Then no idle or health timer should be scheduled
|
||||
|
||||
Scenario: Timer schedulers recheck shutdown after cancellation
|
||||
Given an McpClient with active idle and health timers
|
||||
And cancellation of existing timers begins shutdown
|
||||
When I ask the client to schedule idle and health timers
|
||||
Then cancellation should have been observed for both timers
|
||||
And no idle or health timer should be scheduled
|
||||
|
||||
@@ -330,13 +330,14 @@ class McpClient:
|
||||
with self._lock:
|
||||
if self._shutting_down:
|
||||
return
|
||||
|
||||
self._cancel_idle_timer()
|
||||
timeout = self._config.idle_timeout_seconds
|
||||
if timeout <= 0:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
if self._idle_timer is not None:
|
||||
self._idle_timer.cancel()
|
||||
self._idle_timer = None
|
||||
if self._shutting_down:
|
||||
return
|
||||
timeout = self._config.idle_timeout_seconds
|
||||
if timeout <= 0:
|
||||
return
|
||||
timer = threading.Timer(timeout, self._check_idle)
|
||||
timer.daemon = True
|
||||
self._idle_timer = timer
|
||||
@@ -378,13 +379,14 @@ class McpClient:
|
||||
with self._lock:
|
||||
if self._shutting_down:
|
||||
return
|
||||
|
||||
self._cancel_health_check()
|
||||
interval = self._config.health_check_interval_seconds
|
||||
if interval <= 0:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
if self._health_timer is not None:
|
||||
self._health_timer.cancel()
|
||||
self._health_timer = None
|
||||
if self._shutting_down:
|
||||
return
|
||||
interval = self._config.health_check_interval_seconds
|
||||
if interval <= 0:
|
||||
return
|
||||
timer = threading.Timer(interval, self._perform_health_check)
|
||||
timer.daemon = True
|
||||
self._health_timer = timer
|
||||
|
||||
Reference in New Issue
Block a user