fix: throttle auth last_used_at config writes
Every authenticated API request previously called config_mgr.set(api_keys), which rewrites the entire app config blob to SQLite. Under load this caused significant write amplification and lock contention. Persistence of last_used_at is now throttled per key hash to once every 15 minutes. The in-memory timestamp on the matched key is still updated immediately, so reads within the same process see the live value; only the on-disk persistence is throttled.
This commit is contained in:
parent
e0f036df08
commit
ea875cc7af
1 changed files with 29 additions and 4 deletions
33
api/auth.py
33
api/auth.py
|
|
@ -4,8 +4,9 @@ API key authentication for the SoulSync public API.
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import request, current_app
|
from flask import request, current_app
|
||||||
|
|
@ -13,6 +14,27 @@ from flask import request, current_app
|
||||||
from .helpers import api_error
|
from .helpers import api_error
|
||||||
|
|
||||||
|
|
||||||
|
# Throttle persistence of `last_used_at` so every authenticated request
|
||||||
|
# does not rewrite the full app config. Maps key_hash -> last-persisted datetime.
|
||||||
|
_USAGE_WRITE_INTERVAL = timedelta(minutes=15)
|
||||||
|
_last_persisted_usage: dict[str, datetime] = {}
|
||||||
|
_usage_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _should_persist_usage(key_hash: str, now: datetime) -> bool:
|
||||||
|
"""Return True if `last_used_at` for the given key should be written to disk.
|
||||||
|
|
||||||
|
Thread-safe: tracks the last write per key hash in memory and only returns
|
||||||
|
True once per `_USAGE_WRITE_INTERVAL`.
|
||||||
|
"""
|
||||||
|
with _usage_lock:
|
||||||
|
previous = _last_persisted_usage.get(key_hash)
|
||||||
|
if previous is None or (now - previous) >= _USAGE_WRITE_INTERVAL:
|
||||||
|
_last_persisted_usage[key_hash] = now
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def generate_api_key(label=""):
|
def generate_api_key(label=""):
|
||||||
"""Generate a new API key.
|
"""Generate a new API key.
|
||||||
|
|
||||||
|
|
@ -67,9 +89,12 @@ def require_api_key(f):
|
||||||
if not matched:
|
if not matched:
|
||||||
return api_error("INVALID_KEY", "Invalid API key.", 403)
|
return api_error("INVALID_KEY", "Invalid API key.", 403)
|
||||||
|
|
||||||
# Update last-used timestamp (best-effort)
|
# Update last-used timestamp (best-effort, throttled to avoid rewriting
|
||||||
matched["last_used_at"] = datetime.now(timezone.utc).isoformat()
|
# the full app config on every authenticated request).
|
||||||
config_mgr.set("api_keys", stored_keys)
|
now = datetime.now(timezone.utc)
|
||||||
|
matched["last_used_at"] = now.isoformat()
|
||||||
|
if _should_persist_usage(key_hash, now):
|
||||||
|
config_mgr.set("api_keys", stored_keys)
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue