Skip to content

Fix#68

Draft
Tyjfre-j wants to merge 5 commits into
developfrom
Fix
Draft

Fix#68
Tyjfre-j wants to merge 5 commits into
developfrom
Fix

Conversation

@Tyjfre-j

Copy link
Copy Markdown
Collaborator

Summary

Replaces the original Python-side session cap enforcement with a database-native eviction strategy using FOR UPDATE SKIP LOCKED, eliminating race conditions under concurrent logins without adding application-level locking.

What changed

SQL queries (db/queries/session.sql)

Added:

  • EvictOverflowSessionsDELETE ... RETURNING with FOR UPDATE SKIP LOCKED subquery. Computes overflow internally via COUNT(*), sorts by (last_active, created_at), and atomically evicts the oldest sessions.
  • LockUserSessions — advisory SELECT ... FOR UPDATE to serialize per-user session mutations.

Removed:

  • EvictOldestSessions — replaced by EvictOverflowSessions (different parameter contract: passes session_limit instead of pre-computed limit).

Service layer (app/service/users.py)

_create_mobile_session flow updated:

  1. lock_user_sessions — serialize per-user mutations (acquires row lock on all user sessions)
  2. _ensure_device_for_login — get/create device row
  3. upsert_session — create/replace session for this device
  4. evict_overflow_sessions — evict oldest if over cap (yields evicted session IDs)
  5. Redis cache eviction + logging for each evicted session

Replaces Python-side list_sessions_by_user + in-memory sort + delete_session_by_id with atomic SQL eviction.

Test updates

Updated fixtures/fakes across 4 test files to match new query signatures:

  • tests/unit/test_auth_service.pyTestSessionLimit updated for evict_overflow_sessions + lock_user_sessions; added test_multiple_new_devices_at_cap_evict_exact_overflow
  • tests/unit/test_mobile_auth_intent_validation.py — stateful FakeSessionQuerier now computes overflow internally from session_limit
  • tests/unit/test_mobile_auth_email_logging.py — added missing lock_user_sessions + evict_overflow_sessions
  • tests/unit/test_auth_email_otp.py — configured evict_overflow_sessions on bare AsyncMock (async generator side-effect)

Added integration test:

  • test_concurrent_new_device_logins_settle_at_cap_real_db — stress test using separate DB connections per concurrent task; validates final session count == SESSION_LIMIT under true Postgres concurrency

Why SKIP LOCKED

Without it, two concurrent logins from the same user at cap could both read "3 sessions," both decide to evict, and both create a new session → 4 sessions briefly exist. FOR UPDATE SKIP LOCKED makes the subquery rows invisible to competing transactions, so evictions serialize cleanly at the database level.

The advisory lock (lock_user_sessions) ensures the entire device-lookup → upsert → evict sequence is serialized per user, preventing interleaving between upsert_session and evict_overflow_sessions.

@Tyjfre-j
Tyjfre-j marked this pull request as draft July 20, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant