Priority and Preemption
This page covers three features of a resource. Task priority ranks the waiting requests: a lower priority value is served first. Preemption lets a request take a unit from a task whose priority value is higher than its own. A breakdown takes every unit away until a repair.
What It Is
In a shop, some work cannot wait its turn. An express order goes before regular ones, an urgent job takes a packer from a routine one, and a machine breaks down and must be repaired. Priority orders the waiting line, preemption lets an urgent task take a unit that is in use, and a breakdown takes a resource out of service for a time.
How to Use It
Task Priority
Under queue: priority, waiting requests are served by priority, not in the order they arrived. A lower priority value is served first, and a task with no priority ranks 0, as a SimPy PriorityRequest does. Requests with equal priorities are served first in, first out, as in SimPy's PriorityResource. A task's priority can be:
- a number, the same for every run of the task;
- an expression, read when the task joins the queue, such as
attr.tierfor the entity's attributetier, orpayload.<field>for the previous task's payload; - in the builder, a function of the task id and the entity's attributes.
A priority cannot read the task's own payload, because the payload does not exist yet when the request joins the queue. For a rule that ranks by an expression of the run, see Rank the Line by an Expression.
A low-level process passes the priority to the request: packers.request(priority=entity.attributes["tier"]), as the queue-order example above does. A request with no priority has priority 0. A resource whose queue is fifo or lifo ignores it.
A priority on a task whose resource does not have queue: priority is refused when the blueprint loads, or when run() starts. A priority that is NaN or text fails the task, with a message that names the task and the value. Priorities shows what priority does to waits.
Preemption
On a resource with preempt: true, which needs queue: priority, a request does not always wait for a unit to come free. When every unit is busy, it takes a unit from a task with a higher priority value, as SimPy's PreemptiveResource does. That task is preempted: it loses the unit before its service ends. The task with the highest priority value loses it, and only when that value is higher than the new request's. Among tasks with equal values, the one that asked last loses it. A task with the same priority as the new request keeps its unit. A task with preempt: false never takes a unit this way. Look at an express request that arrives while both packers serve regular orders:

The request of a preempted task is marked as preempted. If the unit belonged to a flow that has ended, the preemption fails the flow that asked, as SimPy's RuntimeError does. A seize unit is asked for again under resume and restart. The preempted task publishes preempted with the resource's name, the key of the entity that took the unit (preempted_by) and the time the task was granted the unit (usage_since). It then follows its on_interrupt: resume, restart or abandon, and on_interrupt_run or on_interrupt_next where the task names one (Interrupts, and the tutorial Interruptions). The interruptions example lets express orders preempt regular ones:
| examples/yaml/interruptions.yaml:packers | |
|---|---|
| examples/yaml/interruptions.yaml:pack | |
|---|---|
| examples/builder/interruptions_example.py:packers | |
|---|---|
DynamicResource(env, "Shop", "packers", queue="priority", preempt=True) builds the resource, and request(priority, preempt=True) asks for a unit. The process that loses its unit receives simpy.Interrupt with a SimPy Preempted cause, whose resource, by (the process that took the unit) and usage_since say what happened.
A gift order with preempt: false is served before any regular order that waits. But it waits for the packer to be free, rather than take it from a regular order being packed:
@app.task(..., priority=0, preempt=False) sets the same on a builder task.
request(priority=0, preempt=False) asks for a unit without taking one from a lower-priority order.
Rank the Line by an Expression
queue: {rank, ties} ranks the line by an expression instead of a task priority. rank is read as each request is made, in the scope of the task that makes it, and the lowest rank is served first. ties orders equal ranks: fifo, the default, serves the oldest first, and lifo the latest first. It changes the order of the line only, not who preempts. A priority on a resource that has a rank is refused when the blueprint loads.
preempt on a resource can be a rule, preempt: {when, victim}. when is tested when a request finds no free unit, and the request takes a unit if it holds. victim gives each holder a value, and the holder with the highest value is the one when is tested on, the first granted among equals. Both read request.rank, request.time, request.preempt and request.key, and holder.rank, holder.time, holder.preempt, holder.key, holder.since and holder.attr.<name>. A preempt on a task, a seize or a request mapping is a boolean, a word such as "t" or an expression. A request: {resource, priority, preempt} branch of a wait gives each branch its own rank. A mapping that leaves out priority or preempt is SimPy's request(), with priority 0 and preempt true, and does not take the task's values. A rank or priority that is NaN, or text on a ranked queue, is refused naming the holder and the task.
app.add_resource("packers", queue={"rank": "attr.tier", "ties": "lifo"}, preempt={"when": "...", "victim": "..."}) takes the same mappings.
A request carries its priority, and a subclass of ResourceBase decides the order and who is taken (Custom Resource Types).
Read Who Holds a Unit
An expression reads the holders and the line of a resource: held(r) lists the requests that hold a unit and waiting(r) the requests in line, each a mapping with entity, rank, time, preempt, key and since. A task that an on_interrupt_next edge reaches reads why it was interrupted as cause.by, cause.usage_since and cause.left; cause.<name> is refused where no on_interrupt_next edge reaches the task.
A task function reads the resource directly, as Units and Queues shows; a Template takes the same expressions as the YAML tab.
packers.users and packers.queue are the lists, and a request has usage_since.
Breakdowns
A breakdown makes a resource fail at random and be repaired. time_to_failure is the time from the start, or from a repair, to the next failure. With clock: calendar, the default, it counts all simulation time from the start or the last repair, busy or idle. With clock: busy it counts only the time a unit is in use, and a seize, a resource:, a wait request and an open unit all count as in use. A custom resource type calls the base _do_put and _do_get, which wake when_busy() and when_idle(), so its busy time counts too. repair is how long a repair takes. While the resource is down its effective capacity is 0 and no request is granted:
Start run_breakdowns(env, station, sampler, work="interrupt") as a SimPy process beside the arrivals. station.fail() and station.repair() put a resource down and back by hand.
A resource can set the clock in the three forms: clock: busy under breakdown in YAML, add_breakdown(clock="busy") in the Builder, and run_breakdowns(clock="busy") or BreakdownConfig(clock="busy") in the Low-level API. Model Breakdowns shows each.
work says what happens to the tasks being served. Under interrupt, the default, each task is interrupted with the cause breakdown and follows its on_interrupt. Under finish, the tasks finish their work. Each failure and repair publishes an event keyed by the resource's name, with status down or repaired and reason: failure. Model Breakdowns checks the downtime of the example, and Interruptions explains on_interrupt.
How It Works
SimPy ranks a request by (priority, time, not preempt). So among requests with equal priorities made at the same time, a request that may preempt goes before one with preempt: false. The tests check it, ties included, in all three APIs (SimPy Compatibility).
SimPy's exact rule
SimPy sorts the tasks holding a unit by (priority, request time, may not preempt). The task that sorts last loses its unit, but only when it also sorts after the new request. A task with the same priority asked earlier than the new request, so it sorts before it and keeps its unit. The one exception is a task with the same priority that asked at the same moment, with preempt: false: it sorts after a request that may preempt, and loses its unit.
Example: Interruptions
In the interruptions example, express orders preempt regular ones, and a fire drill at 60 seconds interrupts the order being packed. Each interrupted order resumes with the packing time it had left. It needs no Docker service and ends on its own after 120 simulation seconds.
The tutorial page Interruptions teaches these features step by step.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
The whole file is examples/yaml/interruptions.yaml.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
The whole file is examples/builder/interruptions_example.py.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
The whole file is examples/low-level/interruptions_example.py.