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:
Emi Griffith 2026-07-22 12:07:15 -07:00
parent 20b81c0afe
commit 83b6e2ebc0
4 changed files with 29 additions and 9 deletions

View file

@ -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,

View file

@ -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 "

View file

@ -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"),

View file

@ -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", ""),