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