fix(signal_v2): await cancelled poll_task + public cache_size

Code review fixes:
- main.py lifespan: await poll_task after cancel() to avoid client
  close racing with mid-fetch task (CRITICAL).
- stock_client: add public cache_size() method; main.py /health uses
  it instead of private _cache attribute (IMPORTANT).

19 tests still pass. Deferred to Phase 7 backlog:
- _ctx singleton test isolation (importlib.reload provides isolation in practice)
- poll_loop interval floor (interval >= 60 by design)
- shutdown logging
- response schema validation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 03:52:45 +09:00
parent 94c684bab8
commit e47947fb69
2 changed files with 9 additions and 1 deletions

View File

@@ -50,6 +50,10 @@ async def lifespan(app: FastAPI):
await asyncio.wait_for(_ctx.poll_task, timeout=5.0)
except asyncio.TimeoutError:
_ctx.poll_task.cancel()
try:
await _ctx.poll_task
except asyncio.CancelledError:
pass
if _ctx.client is not None:
await _ctx.client.close()
@@ -66,5 +70,5 @@ async def health():
"status": "online",
"stock_api_url": settings.stock_api_url,
"last_poll": state_mod.state.last_updated,
"cache_size": len(_ctx.client._cache) if _ctx.client is not None else 0,
"cache_size": _ctx.client.cache_size() if _ctx.client is not None else 0,
}

View File

@@ -34,6 +34,10 @@ class StockClient:
async def close(self) -> None:
await self._client.aclose()
def cache_size(self) -> int:
"""Number of cached endpoint responses (public surface for /health)."""
return len(self._cache)
async def get_portfolio(self) -> dict:
return await self._cached_request(
"portfolio", "GET", "/api/webai/portfolio"