perf: run bcrypt on a thread so it doesn't block the event loop

verify_password / get_password_hash are CPU-bound and take ~250ms each
at rounds=12. Called directly from async endpoints, they stall every
other coroutine for that window — the single biggest single-worker
bottleneck on the login path.

Adds averify_password / ahash_password that wrap the sync versions in
asyncio.to_thread. Sync versions stay put because _ensure_admin_user and
tests still use them.

5 call sites updated: login, change-password, create-user, reset-password.
tests/test_auth_async.py asserts parallel averify runs concurrently (~1x
of a single verify, not 2x).
This commit is contained in:
2026-04-17 14:52:22 -04:00
parent bd406090a7
commit 3945e72e11
15 changed files with 724 additions and 42 deletions

View File

@@ -3,7 +3,7 @@ import uuid as _uuid
from fastapi import APIRouter, Depends, HTTPException
from decnet.telemetry import traced as _traced
from decnet.web.auth import get_password_hash
from decnet.web.auth import ahash_password
from decnet.web.dependencies import require_admin, repo
from decnet.web.db.models import (
CreateUserRequest,
@@ -39,7 +39,7 @@ async def api_create_user(
await repo.create_user({
"uuid": user_uuid,
"username": req.username,
"password_hash": get_password_hash(req.password),
"password_hash": await ahash_password(req.password),
"role": req.role,
"must_change_password": True, # nosec B105 — not a password
})
@@ -125,7 +125,7 @@ async def api_reset_user_password(
await repo.update_user_password(
user_uuid,
get_password_hash(req.new_password),
await ahash_password(req.new_password),
must_change_password=True,
)
return {"message": "Password reset successfully"}