How SlotWorker Handles Priority Queuing and Auto‑Preemption in Forge
SlotWorker serializes access to a WorkflowRunner by combining an asyncio.PriorityQueue with a lightweight cancellation protocol that automatically aborts lower‑priority tasks when urgent work arrives.
In the antoinezambelli/forge repository, the SlotWorker class manages concurrent workflow execution by ensuring that higher‑priority tasks always take precedence, even if that means preempting work already in flight. This design guarantees that critical operations are not blocked by background jobs while maintaining FIFO ordering among tasks of equal priority.
The Priority Queue Architecture
The core of SlotWorker is an asyncio.PriorityQueue that stores pending workflow tasks. Because Python’s PriorityQueue orders elements by the first item of a tuple, lower integer values represent higher priority.
Queue Entry Structure
Each item placed into the queue is a six‑element tuple defined in the class constructor at src/forge/core/slot_worker.py (lines 34‑36):
(priority, self._counter, workflow, user_message, prompt_vars, future)
priority: An integer where smaller values indicate higher urgency.self._counter: A monotonically increasing integer that breaks ties.workflow,user_message,prompt_vars: Execution context for the runner.future: Anasyncio.Futurethat will hold the result or exception.
FIFO Guarantee for Equal Priorities
The second element of the tuple—self._counter—ensures that tasks sharing the same priority value are executed in the order they were submitted. This counter increments with every call to submit(), preventing starvation of older tasks when the queue contains multiple entries with identical priorities.
Auto‑Preemption Mechanics
Auto‑preemption allows SlotWorker to interrupt a running workflow the moment a higher‑priority task enters the system. This is implemented entirely through Python’s asyncio.Event mechanism rather than process‑level signals.
The Preemption Trigger in submit()
Immediately after enqueueing a new task, submit() compares the incoming priority against the currently executing task. If the new task is strictly more important, it signals cancellation (lines 88‑96 of src/forge/core/slot_worker.py):
if (
self._current_priority is not None
and priority < self._current_priority
and self._cancel_event is not None
):
self._cancel_event.set()
This logic executes synchronously during the submit() call, meaning preemption happens instantly—even before the submitting coroutine yields control.
Cancellation Event Propagation
The _worker() loop (lines 25‑33) creates a fresh asyncio.Event for each task it processes:
self._cancel_event = asyncio.Event()
self._current_priority = priority
result = await self.runner.run(..., cancel_event=self._cancel_event)
When submit() sets _cancel_event, the running WorkflowRunner detects the signal via its cancel_event argument and raises a cancellation exception. The worker catches this, clears its internal state, and immediately begins processing the next highest‑priority item from the queue.
The Worker Processing Loop
The _worker() coroutine runs continuously in the background after start() is called. Its lifecycle follows this pattern:
- Dequeue: Pull the next tuple from
self._queue.get(). - Prepare: Instantiate a new
asyncio.Event()and record the task’s priority inself._current_priority. - Execute: Invoke
self.runner.run()with the cancellation event. - Finalize: Set the task’s
futurewith either the result or the exception, then clearself._current_priorityandself._cancel_event.
If the task completes normally, its associated future receives the return value. If it is preempted or fails, the future receives the corresponding exception, allowing the caller to handle the outcome via await worker.submit().
Complete Working Example
The following example demonstrates how a task with priority=1 preempts a running task with priority=5:
import asyncio
from forge.core.runner import WorkflowRunner
from forge.core.slot_worker import SlotWorker
from forge.core.workflow import Workflow
async def main():
runner = WorkflowRunner(...)
worker = SlotWorker(runner)
await worker.start()
# Low‑priority task
low_prio = asyncio.create_task(
worker.submit(Workflow(...), "low‑prio request", priority=5)
)
# High‑priority task arrives shortly after
await asyncio.sleep(0.1)
high_prio = asyncio.create_task(
worker.submit(Workflow(...), "high‑prio request", priority=1)
)
# The high‑priority task returns successfully
result = await high_prio
print("High‑priority result:", result)
# The low‑priority task raises WorkflowCancelledError
try:
await low_prio
except Exception as exc:
print("Low‑priority task was pre‑empted:", exc)
await worker.stop()
asyncio.run(main())
In this scenario, the high‑priority submission triggers self._cancel_event.set(), causing the runner executing the low‑priority workflow to abort and raise an exception that propagates back to the original caller.
Summary
SlotWorkeruses anasyncio.PriorityQueueto order tasks, with lower integers indicating higher priority.- A monotonic counter ensures FIFO execution among tasks sharing the same priority, preventing starvation.
- Auto‑preemption is triggered inside
submit()when a new task’s priority is strictly lower (higher urgency) than_current_priority. - The cancellation event is passed to
WorkflowRunner.run(), allowing graceful abortion of in‑flight work. - Preempted tasks receive a cancellation exception through their associated future, enabling callers to distinguish between success and preemption.
Frequently Asked Questions
What exception does a preempted task receive?
A preempted task typically receives a WorkflowCancelledError (or similar cancellation exception defined in the forge framework) through its future. This allows the caller to catch the exception and handle cleanup logic or retry the task at a lower priority later.
How does the counter prevent starvation of equal‑priority tasks?
The self._counter value increments monotonically for every submission and serves as the second sort key in the priority queue tuple. Because asyncio.PriorityQueue falls back to the next tuple element when priorities are equal, tasks with identical priority values are dequeued in the exact order they were enqueued, guaranteeing fairness.
Can auto‑preemption be disabled?
The current implementation in src/forge/core/slot_worker.py does not provide a configuration flag to disable auto‑preemption. Preemption is intrinsic to the submit() method’s logic (lines 88‑96). To disable it, you would need to subclass SlotWorker and override submit() to remove the cancellation check.
Which class actually performs the cancellation check?
While SlotWorker creates and sets the asyncio.Event, the actual responsiveness to cancellation depends on WorkflowRunner in src/forge/core/runner.py. The runner must accept the cancel_event parameter and periodically check event.is_set() during long‑running operations to enable cooperative cancellation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →