public overview: background refresh (inline recompute took 40 s); HTML no-cache, hashed assets immutable
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 changed files +42 −9
modified
services/api/src/findata_api/main.py
+13 −0
@@ -52,6 +52,12 @@ class Observability(BaseHTTPMiddleware): | |||||
| 52 | M.REQUESTS.labels(request.method, _route_label(route), str(response.status_code)).inc() | 52 | M.REQUESTS.labels(request.method, _route_label(route), str(response.status_code)).inc() | ||
| 53 | M.LATENCY.labels(_route_label(route)).observe(dt) | 53 | M.LATENCY.labels(_route_label(route)).observe(dt) | ||
| 54 | response.headers["X-Request-ID"] = rid | 54 | response.headers["X-Request-ID"] = rid | ||
| 55 | + if not route.startswith("/api/") and route != "/metrics": | ||||
| 56 | + # static console: hashed assets are immutable, HTML must be revalidated so deployments show up at once | ||||
| 57 | + if route.startswith("/_next/static/"): | ||||
| 58 | + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" | ||||
| 59 | + else: | ||||
| 60 | + response.headers["Cache-Control"] = "no-cache" | ||||
| 55 | response.headers["X-Content-Type-Options"] = "nosniff" | 61 | response.headers["X-Content-Type-Options"] = "nosniff" | ||
| 56 | response.headers["X-Frame-Options"] = "DENY" | 62 | response.headers["X-Frame-Options"] = "DENY" | ||
| 57 | response.headers["Referrer-Policy"] = "no-referrer" | 63 | response.headers["Referrer-Policy"] = "no-referrer" | ||
@@ -149,6 +155,13 @@ def create_app() -> FastAPI: | |||||
| 149 | body, ctype = M.render() | 155 | body, ctype = M.render() | ||
| 150 | return Response(body, media_type=ctype) | 156 | return Response(body, media_type=ctype) | ||
| 151 | 157 | ||||
| 158 | + @app.on_event("startup") | ||||
| 159 | + def _warm() -> None: | ||||
| 160 | + from findata_api.health import start_refresher as _h | ||||
| 161 | + from findata_api.routers.public import start_refresher as _p | ||||
| 162 | + _h() | ||||
| 163 | + _p() | ||||
| 164 | + | ||||
| 152 | web = s.web_dist or Path(s.root) / "apps" / "web" / "out" | 165 | web = s.web_dist or Path(s.root) / "apps" / "web" / "out" | ||
| 153 | if web.exists(): | 166 | if web.exists(): | ||
| 154 | app.mount("/", StaticFiles(directory=str(web), html=True), name="web") | 167 | app.mount("/", StaticFiles(directory=str(web), html=True), name="web") | ||
modified
services/api/src/findata_api/routers/public.py
+29 −9
@@ -66,14 +66,34 @@ def _compute() -> dict[str, Any]: | |||||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | 68 | ||||
| 69 | +_started = False | ||||
| 70 | + | ||||
| 71 | + | ||||
| 72 | +def _refresh_loop(period_s: float) -> None: | ||||
| 73 | + while True: | ||||
| 74 | + try: | ||||
| 75 | + v = _compute() | ||||
| 76 | + _cache.update(at=time.monotonic(), value=v) | ||||
| 77 | + except Exception as e: | ||||
| 78 | + _cache.setdefault("error", str(e)[:200]) | ||||
| 79 | + time.sleep(period_s) | ||||
| 80 | + | ||||
| 81 | + | ||||
| 82 | +def start_refresher(period_s: float = 300.0) -> None: | ||||
| 83 | + global _started | ||||
| 84 | + if _started: | ||||
| 85 | + return | ||||
| 86 | + _started = True | ||||
| 87 | + threading.Thread(target=_refresh_loop, args=(period_s,), name="public-overview-refresher", daemon=True).start() | ||||
| 88 | + | ||||
| 89 | + | ||||
| 69 | @router.get("/overview") | 90 | @router.get("/overview") | ||
| 70 | def overview(): | 91 | def overview(): | ||
| 71 | − now = time.monotonic() | 92 | + """Served from a cache refreshed in the background every 5 minutes; the request path never runs the queries.""" | ||
| 72 | − if _cache["value"] is not None and now - _cache["at"] < 120: | 93 | + start_refresher() | ||
| 73 | − return _cache["value"] | 94 | + v = _cache["value"] | ||
| 74 | − with _lock: | 95 | + if v is None: | ||
| 75 | − if _cache["value"] is not None and time.monotonic() - _cache["at"] < 120: | 96 | + return {"generated_at": datetime.now(UTC).isoformat(), "status": "starting", "totals": None, "families": [], "datasets": [], | ||
| 76 | − return _cache["value"] | 97 | + "coverage": [], "options": {}, "fundamentals": {}, "instruments": {}, "freshness": {"jobs_24h": {}}, "storage": {}, | ||
| 77 | − v = _compute() | 98 | + "backup": {}, "license": "FirstRate Data — private research use; raw bars and tables are not redistributed. Sign in for data access."} | ||
| 78 | − _cache.update(at=time.monotonic(), value=v) | 99 | + return v | ||
| 79 | − return v | ||||
| 80 | 100 | ||||