Checked first and logged after search.list, requests sent together at the last free slot all passed, and each spent 100 units of the shared budget. Every live search now claims its slot (quota.claim_action) whatever the source, and gives it back when it showed nothing. The cap counts the zero-unit claims only, so search.list's own spend event is accounting, not a second slot. log_action, the unlocked writer, goes. Tests: the API source reads one page per request, is refused before the call when the budget cannot pay, counts one slot per search and refunds a failed one; the lock test now proves the count happens after the lock.
74 lines
3.5 KiB
Python
74 lines
3.5 KiB
Python
"""DB-backed tests for quota transaction ownership: quota accounting writes in its OWN
|
|
session, so a spend is DURABLE even when the calling job rolls back (the units were really consumed
|
|
at YouTube), and it NEVER commits the caller's session (the old behaviour flushed a job's partial
|
|
state mid-run behind its own rollback). Uses the DB lane (`db` fixture) — skipped without Postgres.
|
|
"""
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
from app import quota
|
|
from app.models import QuotaEvent, User
|
|
|
|
|
|
def test_record_usage_survives_caller_rollback_and_leaves_caller_uncommitted(db):
|
|
# The caller has uncommitted work of its own; then a YouTube call spends quota; then the caller
|
|
# rolls back (a failed job).
|
|
db.add(User(email="pending@example.com")) # caller's own, never committed
|
|
quota.record_usage(10) # dedicated session — commits independently
|
|
db.rollback() # the job fails and rolls back
|
|
|
|
# Quota spend PERSISTED (real units were consumed) — the guard stays accurate ...
|
|
assert quota.units_used_today(db) == 10
|
|
assert db.scalar(select(func.count()).select_from(QuotaEvent)) == 1
|
|
# ... but the caller's own partial work did NOT leak (quota never committed the caller's session).
|
|
assert (
|
|
db.scalar(select(func.count()).select_from(User).where(User.email == "pending@example.com"))
|
|
== 0
|
|
)
|
|
|
|
|
|
def test_record_usage_accumulates(db):
|
|
quota.record_usage(10)
|
|
quota.record_usage(5)
|
|
assert quota.units_used_today(db) == 15 # atomic upsert: +10 then +5
|
|
|
|
|
|
def test_reads_see_separate_session_spend_mid_transaction(db):
|
|
# Mimics measured()'s before/after diff: a read, a spend (in quota's SEPARATE session), a read.
|
|
# The second read must reflect the spend even though the caller's own transaction is still open —
|
|
# units_used_today is a scalar SELECT (READ COMMITTED sees other sessions' commits), NOT a cached
|
|
# ORM row that db.get would return stale.
|
|
before = quota.units_used_today(db)
|
|
quota.record_usage(7)
|
|
after = quota.units_used_today(db)
|
|
assert after - before == 7
|
|
|
|
|
|
def test_a_claimed_slot_survives_caller_rollback(db):
|
|
user = User(email="searcher@example.com")
|
|
db.add(user)
|
|
db.commit() # a committed user to attribute the (free) action to
|
|
db.add(User(email="pending@example.com")) # uncommitted caller work
|
|
assert quota.claim_action(user.id, quota.QuotaAction.VIDEOS_SEARCH, 5) is not None
|
|
db.rollback()
|
|
|
|
# The zero-cost claim event survived (per-user daily caps must still count it) ...
|
|
events = db.execute(select(QuotaEvent)).scalars().all()
|
|
assert len(events) == 1
|
|
assert events[0].user_id == user.id
|
|
assert events[0].units == 0
|
|
# ... and the caller's uncommitted user is gone.
|
|
assert db.scalar(select(func.count()).select_from(User)) == 1
|
|
|
|
|
|
def test_backfill_reserve_reached_tracks_the_boundary(db):
|
|
# The shared yield gate for every backfill/explore/revalidation path: True once today's remaining
|
|
# budget is at or below the backfill reserve. It reads in its OWN session, so it must reflect
|
|
# record_usage's independently-committed spend — and the boundary is inclusive (<=). Defaults:
|
|
# quota_daily_budget 9000, backfill_quota_reserve 2000 -> reached once 7000 units are used.
|
|
assert quota.backfill_reserve_reached() is False # 0 used, remaining 9000
|
|
quota.record_usage(6999) # remaining 2001, just above the reserve
|
|
assert quota.backfill_reserve_reached() is False
|
|
quota.record_usage(1) # remaining 2000, exactly at the reserve
|
|
assert quota.backfill_reserve_reached() is True # boundary is <=, so the reserve is reached
|