Port a57d753 Move blocking calls off the event loop in async request handlers from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
This commit is contained in:
parent
20b81c0afe
commit
83b6e2ebc0
4 changed files with 29 additions and 9 deletions
|
|
@ -91,7 +91,10 @@ async def create_subscription(
|
|||
if not label:
|
||||
# Cache-only lookup — never block the event loop on Nominatim. The frontend
|
||||
# normally supplies the label from /api/v2/place; this is just a fallback.
|
||||
_, label = climate.reverse_geocode_cached(cell["center_lat"], cell["center_lon"])
|
||||
# Still a sync DB read under the hood, so keep it off the event loop too.
|
||||
_, label = await run_in_threadpool(
|
||||
climate.reverse_geocode_cached, cell["center_lat"], cell["center_lon"]
|
||||
)
|
||||
|
||||
sub = Subscription(
|
||||
user_id=user.id,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import secrets
|
|||
import uuid
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi_users import BaseUserManager, FastAPIUsers, InvalidPasswordException, UUIDIDMixin
|
||||
from fastapi_users.authentication import AuthenticationBackend, CookieTransport
|
||||
from fastapi_users.authentication.strategy.db import DatabaseStrategy
|
||||
|
|
@ -89,7 +90,10 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
|||
if not mailer.enabled():
|
||||
return
|
||||
link = f"{BASE_URL}{BASE}/?verify_token={token}"
|
||||
mailer.send(
|
||||
# mailer.send() opens a blocking SMTP connection — fires on every
|
||||
# register/resend-verification request, so keep it off the event loop.
|
||||
await run_in_threadpool(
|
||||
mailer.send,
|
||||
user.email,
|
||||
"Confirm your Thermograph account",
|
||||
"Welcome to Thermograph! Confirm your email address to finish setting up "
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import re
|
|||
import time
|
||||
|
||||
from fastapi import Request, Response
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import select
|
||||
|
||||
|
|
@ -67,7 +68,9 @@ async def _store(email: str, place: str | None, source: str | None) -> None:
|
|||
# Nothing is sent until a real mail backend is configured; the console
|
||||
# backend logs it so the flow can be verified end-to-end without a server.
|
||||
if mailer.enabled():
|
||||
mailer.send(
|
||||
# Blocking SMTP connect+send — keep it off the event loop.
|
||||
await run_in_threadpool(
|
||||
mailer.send,
|
||||
email,
|
||||
"Confirm your Thermograph digest",
|
||||
"Thanks for signing up for the Thermograph monthly digest.\n\n"
|
||||
|
|
@ -103,7 +106,8 @@ async def signup(request: Request) -> Response:
|
|||
await _store(email, place, source)
|
||||
except Exception: # noqa: BLE001 - never surface storage detail to the form
|
||||
pass
|
||||
metrics.record_event(
|
||||
await run_in_threadpool(
|
||||
metrics.record_event,
|
||||
"home.digest_signup",
|
||||
referer=request.headers.get("referer"),
|
||||
host=request.headers.get("host"),
|
||||
|
|
|
|||
19
web/app.py
19
web/app.py
|
|
@ -12,6 +12,7 @@ import time
|
|||
|
||||
import httpx
|
||||
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request, Response
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
|
@ -249,12 +250,16 @@ async def revalidate_static(request, call_next):
|
|||
path = request.url.path
|
||||
try:
|
||||
cat = metrics.classify_inbound(path, BASE)
|
||||
metrics.record_inbound(cat, response.status_code)
|
||||
# Both are blocking (sync DB write, sync file write) — run off the event
|
||||
# loop so one request's instrumentation can't stall every other request
|
||||
# this worker is serving concurrently.
|
||||
await run_in_threadpool(metrics.record_inbound, cat, response.status_code)
|
||||
# Retain per-request client IPs for later analysis; skip static assets and the
|
||||
# dashboard's own metrics polling to keep the log to real, meaningful traffic.
|
||||
if cat not in ("static", "metrics", "event"):
|
||||
audit.log_access({"ip": _client_ip(request), "method": request.method,
|
||||
"path": path, "status": response.status_code, "cat": cat})
|
||||
await run_in_threadpool(audit.log_access, {
|
||||
"ip": _client_ip(request), "method": request.method,
|
||||
"path": path, "status": response.status_code, "cat": cat})
|
||||
except Exception: # noqa: BLE001 - never let instrumentation break a response
|
||||
pass
|
||||
pages = (BASE, f"{BASE}/", f"{BASE}/calendar", f"{BASE}/day", f"{BASE}/score",
|
||||
|
|
@ -724,7 +729,8 @@ async def api_event(request: Request) -> Response:
|
|||
except Exception: # noqa: BLE001 - a junk body is just a no-op event
|
||||
pass
|
||||
if name:
|
||||
metrics.record_event(
|
||||
await run_in_threadpool(
|
||||
metrics.record_event,
|
||||
name,
|
||||
referer=request.headers.get("referer"),
|
||||
host=request.headers.get("host"),
|
||||
|
|
@ -810,7 +816,10 @@ async def discord_interactions(request: Request) -> Response:
|
|||
"""Discord posts each slash-command interaction here. Verification runs over the
|
||||
RAW body, so this must read bytes, not request.json()."""
|
||||
raw = await request.body()
|
||||
status, payload = discord_interactions_mod.handle(
|
||||
# handle() does real work for /grade (sync parquet/DB reads + grading math),
|
||||
# not just the cheap signature check — keep it off the event loop.
|
||||
status, payload = await run_in_threadpool(
|
||||
discord_interactions_mod.handle,
|
||||
raw,
|
||||
request.headers.get("x-signature-ed25519", ""),
|
||||
request.headers.get("x-signature-timestamp", ""),
|
||||
|
|
|
|||
Loading…
Reference in a new issue