Skip to content

Fix session processor crash when a queue item is deleted while running#9352

Open
lstein wants to merge 5 commits into
invoke-ai:mainfrom
lstein:fix/tolerate-queue-item-deleted-mid-run
Open

Fix session processor crash when a queue item is deleted while running#9352
lstein wants to merge 5 commits into
invoke-ai:mainfrom
lstein:fix/tolerate-queue-item-deleted-mid-run

Conversation

@lstein

@lstein lstein commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #9350, addressing the root cause of the missing queue item.

Deleting a queue item while it is executing is a supported operation: clear and delete_queue_item remove the DB row immediately and rely on the processor noticing the cancel event afterwards. But the workflow-call lifecycle re-fetched the queue item (and its parents) unconditionally after the session runner returned, raising SessionQueueItemNotFoundError. Before #9350 this escalated to a fatal error that killed the session processor thread; after #9350 it still logs a scary non-fatal error and traceback on every cancel-via-delete.

Two fixes:

BackendWorkflowCallQueueLifecycle now treats a missing queue item or parent as "deleted mid-run" and returns quietly instead of raising, matching the existing handling in _on_after_run_session:

  • run_queue_item catches SessionQueueItemNotFoundError on its post-run re-fetch (the exact failure point in the reported traceback).
  • _get_parent_queue_item returns None when the parent row is gone (e.g. queue cleared while a child workflow was running); the resume/fail/cancel parent handlers all check it, as do the parent re-fetches inside the failure cascades.

Frontend — the shift+x hotkey labeled "cancel queue item" was still calling the delete endpoint. 2ddcde1 intentionally migrated the queue UI from cancel to delete semantics, but 5baa4bd later moved the visible queue controls back to cancelation and missed the hotkey. The hotkey now uses useCancelCurrentQueueItem, aligning it with the CancelCurrentQueueItemIconButton it mirrors and removing the most common trigger of the delete-while-running race.

Related Issues / Discussions

QA Instructions

  1. Enqueue several generations.
  2. While one is in progress, press shift+x, use Queue → "Cancel and Clear All", or delete the current item from the queue list.
  3. Before: log shows Non-fatal error in session processor SessionQueueItemNotFoundError: No queue item with id N (and pre-Handle missing queue items in session failure #9350, a fatal processor error). After: no error; processing continues with the next item.

Also covered by two new unit tests (item deleted mid-run; parent deleted while a child workflow runs, for completed/failed/canceled children). _DummySessionQueue.get_queue_item now raises the real SessionQueueItemNotFoundError instead of KeyError to match production behavior.

Merge Plan

None - straightforward merge.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

🤖 Generated with Claude Code

lstein and others added 2 commits July 11, 2026 21:59
Deleting a queue item (or clearing the queue) while it is executing is a
supported operation: the delete/clear paths remove the DB row immediately and
rely on the processor noticing the cancel event afterwards. The workflow call
lifecycle re-fetched the queue item and its parents unconditionally after the
session runner returned, raising SessionQueueItemNotFoundError and killing the
processor thread when the row was already gone.

Treat a missing queue item or parent as 'deleted mid-run' and return quietly
instead, matching the existing handling in _on_after_run_session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g it

Commit 5baa4bd moved the visible queue controls back to cancelation
semantics but the cancelQueueItem hotkey was missed and still called the
delete hook. Deleting removes the DB row while the item is still executing,
which raced the session processor. Align the hotkey with the
CancelCurrentQueueItemIconButton it mirrors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added python PRs that change python files services PRs that change app services frontend PRs that change frontend files python-tests PRs that change python tests labels Jul 12, 2026
@lstein lstein added the 6.14.x label Jul 12, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap Jul 12, 2026
Canceling the current item clears $lastProgressEvent on the terminal
queue_item_status_changed event, but a denoise step callback can race the
cancelation and emit invocation_progress afterwards, repopulating the
progress bar which then sticks at its last value. The coordinator's state
machine is designed to reject such trailing events, but the terminal status
handler deleted the item's state entry at exactly the moment it was needed.
Keep the entry instead - item ids are never reused (retries insert new rows)
and the LRU cache bounds memory.

Also reset the progress bar on queue_cleared: clearing the queue deletes the
in-progress item without emitting any per-item terminal status event, so
nothing else clears it on that path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lstein

lstein commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

Added a third commit fixing a follow-on UI issue: after canceling the current item, the progress bar stayed frozen at its last value instead of resetting.

Root cause: the canceled status event clears the progress store, but a denoise step callback can race the cancelation and emit invocation_progress after the terminal status event. The workflowExecutionCoordinator state machine exists to reject exactly these trailing events, but the terminal-status handler deleted the item's state entry at the moment it was needed, so the trailing event was treated as a fresh item and re-applied. The entry is now retained (item ids are never reused — retries insert new rows — and the LRU cache bounds memory).

Also: queue_cleared (Cancel and Clear All) deletes the in-progress item without emitting any per-item terminal status event, so nothing reset the bar on that path either — the listener now clears it explicitly.

Includes a regression test that fails against the previous coordinator behavior.

🤖 Generated with Claude Code

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • invokeai/frontend/web/src/services/events/setEventListeners.tsx:539 - The queue_cleared handler clears $lastProgressEvent but does not mark the deleted in-progress item terminal in workflowExecutionCoordinator. A trailing invocation_progress event is therefore accepted at invokeai/frontend/web/src/services/events/workflowExecutionCoordinator.ts:131 and written back to $lastProgressEvent at setEventListeners.tsx:204, recreating the frozen progress bar this change intends to fix. The retained terminal state only protects cancellation paths that emit queue_item_status_changed; the PR explicitly says queue clearing does not emit that event. To expose this issue, add a test that starts an item, handles queue_cleared, then delivers a trailing progress event and verifies that the coordinator rejects it and the progress store remains empty.

  • invokeai/app/services/session_processor/workflow_call_runtime.py:192 - Parent deletion is handled only during the initial lookup and selected re-fetches, leaving time-of-check/time-of-use races throughout all three parent transitions. If clear() or delete_queue_item() removes the parent after _get_parent_queue_item() returns, set_queue_item_session() at lines 221 or 231, complete_queue_item() at line 235, resume_queue_item() at line 241, the failure path reached at line 259, or cancel_queue_item() at line 271 can still raise SessionQueueItemNotFoundError. The SQLite implementations confirm these mutations re-read the row and raise when it has disappeared. That exception returns to the session processor as the same non-fatal error and traceback the PR is intended to eliminate. The new test deletes the parent before the guarded lookup, so it cannot exercise these windows. To expose this issue, add tests whose queue doubles delete the parent immediately after get_queue_item() succeeds and before each completed, failed, and canceled parent mutation.

lstein and others added 2 commits July 12, 2026 10:36
Per review: guarding only the parent lookups left windows where the parent
row is deleted after get_queue_item succeeds but before the subsequent
mutation (set_queue_item_session, complete_queue_item, resume_queue_item,
cancel_queue_item, or the fail path via _on_node_error). Those mutations
re-read the row and raise SessionQueueItemNotFoundError, which returned to
the session processor as the same non-fatal error this PR eliminates.

Catch the exception around the three parent transitions in run_queue_item:
any item vanishing mid-transition means the chain is being torn down and
there is nothing left to update.

The dummy session queue now supports simulating deletion-after-lookup
(mutations raise, lookups succeed), with one test per transition path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review: clearing the queue reset the progress bar but did not mark the
deleted in-progress item terminal in the workflowExecutionCoordinator, so a
trailing invocation_progress event racing the clear was accepted and
repopulated the bar - recreating the frozen progress bar on the clear path.

The coordinator now exposes onQueueCleared, which aborts pending
reconciliations and marks every tracked non-terminal item canceled so
trailing events are rejected, and the queue_cleared listener calls it before
resetting the progress store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lstein

lstein commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

@JPPhoto Thanks for the careful review — both findings were real. Addressed in the two latest commits:

queue_cleared trailing progress (be3984d): the coordinator now exposes onQueueCleared(), which aborts pending reconciliations and marks every tracked non-terminal item canceled, so a trailing invocation_progress racing the clear is rejected instead of repopulating the progress bar. The queue_cleared listener calls it before resetting the progress store. Added the coordinator-level test you suggested: start an item, accept progress, handle queue-clear, then verify a trailing progress event is rejected (and the pending reconciliation aborted).

Parent-transition TOCTOU windows (6fdcd80): rather than chasing each window individually, run_queue_item now wraps the three parent transitions in a single except SessionQueueItemNotFoundError — any item vanishing between lookup and mutation means the chain is being torn down, and quietly returning is correct regardless of which mutation raised (the fine-grained lookup guards are kept for their early-return semantics). The dummy session queue gained a not_found_item_ids knob that makes mutations raise while lookups still succeed — faithfully simulating deletion-after-lookup — with one test per transition path (completed / failed / canceled). All three tests fail against the previous code.

🤖 Generated with Claude Code

@JPPhoto

JPPhoto commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

@lstein One more:

  • invokeai/frontend/web/src/services/events/workflowExecutionCoordinator.ts:78 - onQueueCleared() unconditionally aborts every pending workflow reconciliation, but QueueClearedEvent is broadcast to the entire queue room because it contains no user identity (invokeai/app/api/sockets.py:309 and :449). In multiuser mode, a non-admin clear deletes only the requesting user's rows, while every connected user receives the event. If another user's completed or failed workflow item is currently being reconciled, this handler aborts that unaffected request; the item remains in the database, but no later event restarts reconciliation, so completed sibling outputs and node execution state can remain missing or stale. The new test verifies that reconciliation is aborted, but does not cover the user-scoped clear behavior that makes this incorrect. To expose this issue, add a test that starts reconciliation for user B's terminal workflow item, delivers a queue-cleared event caused by user A's user-filtered clear, and verifies that user B's reconciliation still completes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.x frontend PRs that change frontend files python PRs that change python files python-tests PRs that change python tests services PRs that change app services

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

3 participants