refactor(quota): own the quota session; Plex per-section commit (R6 S2)

Transaction ownership (C-A5):
- quota.record_usage / log_action write in their OWN SessionLocal(), committed
  immediately, and no longer take/commit the caller's db. The spend stays DURABLE
  regardless of the caller (read-only YouTube paths never commit; a failed job rolls
  back) — under-counting the shared daily budget would risk overspending the real
  YouTube quota — AND quota stops flushing a job's partial state mid-run behind its
  own `except: db.rollback()`. units_used_today switches to a scalar SELECT so
  measured()'s before/after diff sees the separate session's commits (READ COMMITTED).
  Call sites updated (youtube/client.py ×3, routes/search.py ×1).
- plex sync commits PER SECTION (moved the end-of-loop commit inside), so a PlexError
  on a later library keeps the ones already mirrored (idempotent; next run reconciles).

The sync jobs did NOT rely on record_usage's incidental commit for persistence — their
per-item functions (apply_rss_feed, backfill_channel_recent, import_subscriptions,
sync_user_playlists, apply_channel_autotags) all commit internally, so the in-loop
rollback handlers already isolate a failed item. No loop restructuring needed.

DB-lane tests (test_quota_ownership, 4): spend survives a caller rollback while the
caller's uncommitted row does NOT leak; accumulation; measured-style mid-txn read.
Gate: ruff clean; DB lane 180 green.
This commit is contained in:
2026-07-26 15:32:46 +02:00
parent 921c2bf5a4
commit 5809f3e431
5 changed files with 97 additions and 19 deletions
+3 -3
View File
@@ -113,7 +113,7 @@ class YouTubeClient:
else:
headers["Authorization"] = f"Bearer {self._access_token()}"
resp = self._send("GET", f"{API_BASE}/{path}", params=p, headers=headers)
quota.record_usage(self.db, cost)
quota.record_usage(cost)
if resp.status_code != 200:
log.warning("YouTube API %s -> %s: %s", path, resp.status_code, resp.text[:200])
raise YouTubeError(f"GET {path} -> {resp.status_code}: {resp.text[:300]}")
@@ -239,7 +239,7 @@ class YouTubeClient:
params={"id": subscription_id},
headers={"Authorization": f"Bearer {self._access_token()}"},
)
quota.record_usage(self.db, 50)
quota.record_usage(50)
if resp.status_code not in (200, 204):
log.warning(
"YouTube subscriptions.delete -> %s: %s",
@@ -282,7 +282,7 @@ class YouTubeClient:
resp = self._send(
method, f"{API_BASE}/{path}", params=params, json=json, headers=headers
)
quota.record_usage(self.db, 50)
quota.record_usage(50)
if resp.status_code not in (200, 204):
log.warning("YouTube %s %s -> %s: %s", method, path, resp.status_code, resp.text[:200])
raise YouTubeError(f"{method} {path} -> {resp.status_code}: {resp.text[:300]}")