mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
Bambu MQTT can deliver two ams_data push frames for the same printer
~30 ms apart (observed on H2D + dual AMS at K-profile-load / RFID-read
boundaries). Each frame triggers on_ams_change in main.py, whose
auto-assign block reads (printer_id, ams_id, tray_id), decides "no
existing assignment", and INSERTs via auto_assign_spool — and the two
callbacks raced in their respective sessions, both deciding to insert,
with the second commit losing on:
asyncpg.exceptions.UniqueViolationError: duplicate key value
violates unique constraint
"spool_assignment_printer_id_ams_id_tray_id_key"
DETAIL: Key (printer_id, ams_id, tray_id)=(1, 0, 0) already exists.
SQLite's WAL serial-write semantics had been silently swallowing the
race for ~7 weeks since the spool-assignment feature shipped (latent in
ec82092b "Sync", 2026-02-12). When optional Postgres support landed in
610431d6 (2026-04-03) and asyncpg started allowing true concurrent
transactions, it surfaced. Net impact: log noise + one assignment cycle
skipped, retried on the next on_ams_change.
Adds a per-printer asyncio.Lock (_ams_assignment_locks keyed by
printer_id) wrapping the auto-assign critical section. By the time the
second callback's session runs the SELECT, the first's commit is
visible and the early-return "existing assignment" branch fires instead
of a duplicate INSERT.
The Spoolman sync block further down in on_ams_change intentionally
stays OUTSIDE the lock — it's network-bound and idempotent, so
serialising it would block subsequent AMS callbacks for the duration of
a remote roundtrip. Per-printer scope keeps unrelated printers fully
parallel. The auto-unlink block above isn't wrapped because its
DELETE/UPDATE operations don't have the same constraint surface.
5 new regression tests in test_ams_assignment_lock.py: same-printer-
same-lock identity, different-printers-different-lock isolation, second
acquirer waits for first (proves serialisation), different printers run
truly in parallel under a held lock (proves per-printer scope), and an
autouse fixture that resets the module-level dict between tests so
cross-test loop affinity bugs can't surface.