ams_assignment_lock.py 1.6 KB

1234567891011121314151617181920212223242526272829303132333435363738
  1. """Per-printer lock for writes to a printer's spool assignments (AMS slots).
  2. on_ams_change serialises its spool-assignment side (auto-unlink stale +
  3. auto-assign new) when MQTT bursts deliver multiple AMS updates for the same
  4. printer in quick succession (~30 ms apart, observed in the wild on H2D + dual
  5. AMS).
  6. Without this serialisation, two concurrent on_ams_change callbacks each read
  7. "no assignment for (printer, ams, tray)", each call auto_assign_spool, and
  8. the second commit hits
  9. IntegrityError: duplicate key value violates unique constraint
  10. "spool_assignment_printer_id_ams_id_tray_id_key"
  11. SQLite's WAL serial-write semantics had been silently swallowing the race
  12. until optional Postgres support landed (asyncpg allows true concurrent
  13. transactions and surfaces the constraint violation).
  14. The assign, unassign and create-from-slot routes write the same rows and take
  15. the same lock (SA-5), before their first write: on SQLite a session that
  16. already holds the write lock would otherwise wait here on an AMS update that
  17. waits for that write lock.
  18. Scope is intentionally narrow: only the DB-mutating blocks are inside the lock.
  19. The Spoolman sync block in on_ams_change stays concurrent because it's
  20. network-bound and idempotent.
  21. """
  22. import asyncio
  23. ams_assignment_locks: dict[int, asyncio.Lock] = {}
  24. def get_ams_assignment_lock(printer_id: int) -> asyncio.Lock:
  25. """Return the per-printer assignment lock, creating it on first use."""
  26. lock = ams_assignment_locks.get(printer_id)
  27. if lock is None:
  28. lock = asyncio.Lock()
  29. ams_assignment_locks[printer_id] = lock
  30. return lock