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>
75 lines
2.0 KiB
Python
75 lines
2.0 KiB
Python
"""FastAPI app — Signal V2 Pull Worker."""
|
|
from __future__ import annotations
|
|
import asyncio
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
|
|
from signal_v2 import state as state_mod
|
|
from signal_v2.config import get_settings
|
|
from signal_v2.pull_worker import poll_loop
|
|
from signal_v2.rate_limit import SignalDedup
|
|
from signal_v2.stock_client import StockClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AppContext:
|
|
client: StockClient | None = None
|
|
dedup: SignalDedup | None = None
|
|
shutdown: asyncio.Event | None = None
|
|
poll_task: asyncio.Task | None = None
|
|
|
|
|
|
_ctx = AppContext()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
settings = get_settings()
|
|
if not settings.webai_api_key:
|
|
logger.warning(
|
|
"WEBAI_API_KEY not configured — stock API calls will fail with 401"
|
|
)
|
|
|
|
_ctx.client = StockClient(settings.stock_api_url, settings.webai_api_key)
|
|
_ctx.dedup = SignalDedup(settings.db_path)
|
|
_ctx.shutdown = asyncio.Event()
|
|
_ctx.poll_task = asyncio.create_task(
|
|
poll_loop(_ctx.client, state_mod.state, _ctx.shutdown)
|
|
)
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
if _ctx.shutdown is not None:
|
|
_ctx.shutdown.set()
|
|
if _ctx.poll_task is not None:
|
|
try:
|
|
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()
|
|
|
|
|
|
app = FastAPI(
|
|
title="Signal V2 Pull Worker", version="0.1.0", lifespan=lifespan
|
|
)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
settings = get_settings()
|
|
return {
|
|
"status": "online",
|
|
"stock_api_url": settings.stock_api_url,
|
|
"last_poll": state_mod.state.last_updated,
|
|
"cache_size": _ctx.client.cache_size() if _ctx.client is not None else 0,
|
|
}
|