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:
|
if not label:
|
||||||
# Cache-only lookup — never block the event loop on Nominatim. The frontend
|
# 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.
|
# 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(
|
sub = Subscription(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import secrets
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
from fastapi import Depends, Request
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
from fastapi_users import BaseUserManager, FastAPIUsers, InvalidPasswordException, UUIDIDMixin
|
from fastapi_users import BaseUserManager, FastAPIUsers, InvalidPasswordException, UUIDIDMixin
|
||||||
from fastapi_users.authentication import AuthenticationBackend, CookieTransport
|
from fastapi_users.authentication import AuthenticationBackend, CookieTransport
|
||||||
from fastapi_users.authentication.strategy.db import DatabaseStrategy
|
from fastapi_users.authentication.strategy.db import DatabaseStrategy
|
||||||
|
|
@ -89,7 +90,10 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||||
if not mailer.enabled():
|
if not mailer.enabled():
|
||||||
return
|
return
|
||||||
link = f"{BASE_URL}{BASE}/?verify_token={token}"
|
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,
|
user.email,
|
||||||
"Confirm your Thermograph account",
|
"Confirm your Thermograph account",
|
||||||
"Welcome to Thermograph! Confirm your email address to finish setting up "
|
"Welcome to Thermograph! Confirm your email address to finish setting up "
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy import select
|
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
|
# 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.
|
# backend logs it so the flow can be verified end-to-end without a server.
|
||||||
if mailer.enabled():
|
if mailer.enabled():
|
||||||
mailer.send(
|
# Blocking SMTP connect+send — keep it off the event loop.
|
||||||
|
await run_in_threadpool(
|
||||||
|
mailer.send,
|
||||||
email,
|
email,
|
||||||
"Confirm your Thermograph digest",
|
"Confirm your Thermograph digest",
|
||||||
"Thanks for signing up for the Thermograph monthly digest.\n\n"
|
"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)
|
await _store(email, place, source)
|
||||||
except Exception: # noqa: BLE001 - never surface storage detail to the form
|
except Exception: # noqa: BLE001 - never surface storage detail to the form
|
||||||
pass
|
pass
|
||||||
metrics.record_event(
|
await run_in_threadpool(
|
||||||
|
metrics.record_event,
|
||||||
"home.digest_signup",
|
"home.digest_signup",
|
||||||
referer=request.headers.get("referer"),
|
referer=request.headers.get("referer"),
|
||||||
host=request.headers.get("host"),
|
host=request.headers.get("host"),
|
||||||
|
|
|
||||||
19
web/app.py
19
web/app.py
|
|
@ -12,6 +12,7 @@ import time
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request, Response
|
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.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
@ -249,12 +250,16 @@ async def revalidate_static(request, call_next):
|
||||||
path = request.url.path
|
path = request.url.path
|
||||||
try:
|
try:
|
||||||
cat = metrics.classify_inbound(path, BASE)
|
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
|
# 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.
|
# dashboard's own metrics polling to keep the log to real, meaningful traffic.
|
||||||
if cat not in ("static", "metrics", "event"):
|
if cat not in ("static", "metrics", "event"):
|
||||||
audit.log_access({"ip": _client_ip(request), "method": request.method,
|
await run_in_threadpool(audit.log_access, {
|
||||||
"path": path, "status": response.status_code, "cat": cat})
|
"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
|
except Exception: # noqa: BLE001 - never let instrumentation break a response
|
||||||
pass
|
pass
|
||||||
pages = (BASE, f"{BASE}/", f"{BASE}/calendar", f"{BASE}/day", f"{BASE}/score",
|
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
|
except Exception: # noqa: BLE001 - a junk body is just a no-op event
|
||||||
pass
|
pass
|
||||||
if name:
|
if name:
|
||||||
metrics.record_event(
|
await run_in_threadpool(
|
||||||
|
metrics.record_event,
|
||||||
name,
|
name,
|
||||||
referer=request.headers.get("referer"),
|
referer=request.headers.get("referer"),
|
||||||
host=request.headers.get("host"),
|
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
|
"""Discord posts each slash-command interaction here. Verification runs over the
|
||||||
RAW body, so this must read bytes, not request.json()."""
|
RAW body, so this must read bytes, not request.json()."""
|
||||||
raw = await request.body()
|
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,
|
raw,
|
||||||
request.headers.get("x-signature-ed25519", ""),
|
request.headers.get("x-signature-ed25519", ""),
|
||||||
request.headers.get("x-signature-timestamp", ""),
|
request.headers.get("x-signature-timestamp", ""),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue