mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 17:50:12 +02:00
fix(queue): claim a queue item before dispatch so it can't be reassigned mid-upload (#2615)
A queue row stays status='pending' for the whole FTP upload; status only flips to 'printing' at the end. The edit routes only blocked non-pending rows, so a PATCH during the upload window was accepted while the in-flight dispatch kept using its snapshotted printer -- splitting the queue row from the archive / expected-print / physical command across two printers, and enabling a duplicate dispatch on restart. The #1853 CAS guards cancellation, not reassignment. Add a dispatching_at claim, stamped atomically (WHERE status='pending' AND dispatching_at IS NULL) before any slow I/O and cleared on every exit. While held, the single-item PATCH returns 409 (re-checked just before the write), bulk edits skip the row, and the scheduler won't re-select it. Startup reconciliation clears claims orphaned by a crash mid-dispatch. The row stays pending throughout, so no status/UI/completion/reconciliation path changes. New column print_queue.dispatching_at (nullable, dialect-safe DDL). Covered by scheduler tests (claim exclusivity, non-pending rejection, release-on-exit, skip-already-claimed, startup stale-clear) and API tests (reassign 409, printer_id unchanged, bulk skip, unclaimed row still edits).
This commit is contained in:
@@ -774,7 +774,10 @@ async def bulk_update_queue_items(
|
||||
skipped_count = 0
|
||||
|
||||
for item in items:
|
||||
if item.status != "pending":
|
||||
# Skip non-pending rows and rows a dispatch worker has claimed (#2615) —
|
||||
# editing a claimed row mid-upload would split it from the in-flight
|
||||
# dispatch, so it's excluded from the bulk change (cancel to move it).
|
||||
if item.status != "pending" or item.dispatching_at is not None:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
@@ -1082,6 +1085,14 @@ async def update_queue_item(
|
||||
if item.status != "pending":
|
||||
raise HTTPException(400, "Can only update pending items")
|
||||
|
||||
# Dispatch claim (#2615): the row is pending but a scheduler worker has
|
||||
# already claimed it and is uploading to its printer. Editing now (e.g.
|
||||
# reassigning printer_id) would split the queue row from the in-flight
|
||||
# archive/expected-print/physical command. Reject until dispatch finishes;
|
||||
# to move it, cancel first (the coordinated escape) and re-queue.
|
||||
if item.dispatching_at is not None:
|
||||
raise HTTPException(409, "Item is being dispatched — cancel it first to make changes")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# Normalize target_model if being updated
|
||||
@@ -1153,6 +1164,16 @@ async def update_queue_item(
|
||||
json.dumps(update_data["nozzle_mapping"]) if update_data["nozzle_mapping"] else None
|
||||
)
|
||||
|
||||
# Re-check the dispatch claim right before mutating (#2615). Several awaited
|
||||
# validations ran since the guard above, and a scheduler worker may have
|
||||
# claimed the row in that gap. A fresh read (item isn't dirty yet, so no
|
||||
# autoflush races the check) narrows the window to effectively nothing.
|
||||
claimed = (
|
||||
await db.execute(select(PrintQueueItem.dispatching_at).where(PrintQueueItem.id == item_id))
|
||||
).scalar_one_or_none()
|
||||
if claimed is not None:
|
||||
raise HTTPException(409, "Item is being dispatched — cancel it first to make changes")
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(item, field, value)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user