fix(app): cancel readiness loop on lifespan shutdown (review round 2)
_readiness_refresh_loop is started with tg.start_soon and loops forever with no shutdown_event check. anyio waits for start_soon tasks on normal task-group exit rather than cancelling them, so graceful shutdown hung until uvicorn's timeout. Cancel the task group's scope after teardown() to stop the loop and any stragglers, mirroring _maybe_login_flow_cleanup. Also document the cache ttl_seconds startup-override and the inclusive is_stale boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cc2ce6e853
commit
fc62a30384
@@ -434,6 +434,8 @@ def _clear_vector_sync_state() -> None:
|
|||||||
# Fallback public URL when NEXTCLOUD_MCP_SERVER_URL is unset (dev/local).
|
# Fallback public URL when NEXTCLOUD_MCP_SERVER_URL is unset (dev/local).
|
||||||
_DEFAULT_MCP_SERVER_URL = "http://localhost:8000"
|
_DEFAULT_MCP_SERVER_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
# Pre-loop default; _readiness_refresh_loop overrides ttl_seconds at startup to
|
||||||
|
# 2x the configured refresh interval, so bumping this value alone has no effect.
|
||||||
_readiness_cache = ReadinessCache(ttl_seconds=30.0)
|
_readiness_cache = ReadinessCache(ttl_seconds=30.0)
|
||||||
|
|
||||||
|
|
||||||
@@ -2186,6 +2188,12 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
# Request path must not spawn into a cancelling group.
|
# Request path must not spawn into a cancelling group.
|
||||||
_vector_sync_state.eviction_task_group = None
|
_vector_sync_state.eviction_task_group = None
|
||||||
await teardown()
|
await teardown()
|
||||||
|
# teardown() signals the sync tasks to drain, but the readiness loop
|
||||||
|
# runs forever and has no shutdown_event to observe. anyio waits for
|
||||||
|
# start_soon tasks on normal exit rather than cancelling them, so
|
||||||
|
# without this the lifespan shutdown hangs until uvicorn's graceful
|
||||||
|
# timeout. Cancel the group to stop the loop (and any stragglers).
|
||||||
|
tg.cancel_scope.cancel()
|
||||||
|
|
||||||
# Health check endpoints for Kubernetes probes
|
# Health check endpoints for Kubernetes probes
|
||||||
def health_live(request):
|
def health_live(request):
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class ReadinessCache(BaseModel):
|
|||||||
if not self.statuses:
|
if not self.statuses:
|
||||||
return True
|
return True
|
||||||
current = time.monotonic() if now is None else now
|
current = time.monotonic() if now is None else now
|
||||||
|
# Inclusive boundary: exactly ttl_seconds old counts as stale.
|
||||||
return any(
|
return any(
|
||||||
current - status.checked_at >= self.ttl_seconds
|
current - status.checked_at >= self.ttl_seconds
|
||||||
for status in self.statuses.values()
|
for status in self.statuses.values()
|
||||||
|
|||||||
Reference in New Issue
Block a user