events.md
231 lines · skills/simpy/references/events.md at 5f2bc03
Events, environments, and scheduling
Reviewed 2026-10-01 against SimPy 4.1.2 documentation and tagged source.
Raw lines 1 to 4Scheduler model
Environment owns a single event queue and processes one event at a time. Tagged 4.1.2 stores queue entries as (time, priority, event_id, event):
- smallest simulation time first;
- smallest numeric priority first (
URGENT=0,NORMAL=1in public event APIs); - smallest strictly increasing event ID first.
Thus same-time, same-priority events are FIFO by scheduling order. This is deterministic sequential execution, even when model processes represent concurrent activities. Floating-point discretization can collapse physically distinct times onto the same value, so test tie behavior explicitly.
The tuple order selects from the queue at each step. A running callback can schedule an urgent Initialize or Interruption at the same time; priority can therefore decrease in a valid processing trace. A trace without insertion history cannot validate the scheduler by sorting all processed tuples afterward.
Event lifecycle
An Event moves once through:
- not triggered — not scheduled and no value;
- triggered — outcome/value fixed and scheduled;
event.triggeredis true; - processed — removed for callback execution;
event.processedis true whenevent.callbacks is None.
import simpy
env = simpy.Environment()
event = env.event()
assert not event.triggered and not event.processed
event.succeed("ready")
assert event.triggered and not event.processed
env.step()
assert event.processed and event.value == "ready"event.succeed(value) and event.fail(exception) return that event and may be called only once. Use an Exception for fail(); the 4.1.2 implementation accepts BaseException despite the narrower API annotation. event.trigger(other) copies the other event's success/failure and value, and returns None.
A failed event throws its exception into a waiting process. If no process or callback defuses it, Environment.step() raises it. Treat private _ok, _value, and _defused as implementation details.
Callbacks
Before processing, event.callbacks is a mutable list of one-argument callables. Yielding an event adds the waiting process's resume method. Processing executes callbacks in list order. Once fully processed, callbacks becomes None; appending then is invalid.
log = []
timeout = env.timeout(2, value=7)
timeout.callbacks.append(lambda completed: log.append(completed.value))
env.run()
assert log == [7]Keep callbacks short and non-blocking. A callback runs synchronously inside Environment.step() and can affect scheduler latency.
Timeout
env.timeout(delay, value=None) creates a Timeout, immediately triggers it, and schedules it at env.now + delay. Because it is already triggered at construction, do not call succeed() or fail() on it.
def timer(env):
result = yield env.timeout(3, value="elapsed")
assert result == "elapsed"Reject negative delay. Use a consistent numeric time unit; SimPy does not attach units or protect against incompatible scales.
Raw lines 74 to 88Process
env.process(generator) requires a generator object, not an ordinary function result. It schedules an urgent Initialize event. Each yielded event suspends the generator; after the event's outcome, SimPy sends its value back or throws its failure into the generator.
def child(env):
yield env.timeout(1)
return 42
def parent(env):
child_process = env.process(child(env))
result = yield child_process
assert result == 42
env = simpy.Environment()
env.process(parent(env))
env.run()A Process is itself an event. It succeeds with the generator's return value or fails with an uncaught exception. process.is_alive, process.target, and process.name expose its current status.
Common mistakes:
env.process(worker)instead ofenv.process(worker(env));- a function without any reachable
yield, which is not a generator; - performing blocking I/O or
time.sleep()inside a normal Environment process; - yielding a number rather than an Event.
Condition events
AnyOf(env, events) / a | b and AllOf(env, events) / a & b return a Condition. Yielding one produces a ConditionValue, an ordered dict-like mapping whose keys are the original Event objects and whose values are their values.
def coordinate(env):
fast = env.timeout(1, value="fast")
slow = env.timeout(2, value="slow")
first = yield fast | slow
assert fast in first
assert first[fast] == "fast"
both = yield fast & slow
assert list(both.items()) == [(fast, "fast"), (slow, "slow")]For AllOf, all input events appear. For AnyOf, all target events that occurred before the condition itself is processed can appear; do not assume exactly one winner when events tie. Input order determines result order.
If any input event fails before the condition succeeds, AnyOf and AllOf fail. Conditions can be nested. AnyOf does not cancel losing events:
with resource.request() as request:
patience = env.timeout(5)
result = yield request | patience
if request not in result:
# Context-manager exit cancels the still-pending request.
return
yield env.timeout(2)An ordinary losing timeout remains scheduled. This is usually harmless but matters for event counts and traces.
Raw lines 122 to 160Environment.run() boundaries
Raw lines 161 to 162No criterion
env.run() stops only when the event queue is empty. An endless generator such as while True: yield env.timeout(1) makes it nonterminating.
Numeric criterion
env.run(until=10) creates an internal urgent stop event at time 10. It advances env.now to 10 but does not process ordinary events scheduled exactly at 10:
env = simpy.Environment()
boundary = env.timeout(10)
env.run(until=10)
assert env.now == 10
assert not boundary.processedThe numeric target must be strictly greater than env.now.
Event criterion
env.run(until=event) attaches a stop callback and returns the event value when that callback fires. It raises RuntimeError if the schedule empties before the criterion is triggered.
Important 4.1.2 implementation detail: Environment.step() catches StopSimulation, preserves callbacks after the stopping callback, and reschedules the event at priority -1. Since processed means callbacks is None, the target can still report processed == False immediately after run() returns:
env = simpy.Environment()
target = env.timeout(5, value="done")
assert env.run(until=target) == "done"
assert target.triggered
assert not target.processed
env.step()
assert target.processedThis behavior follows tagged 4.1.2 simpy/core.py; the topical guide's informal "processed" wording is not a safe postcondition. Depend on the returned value and your model state, not solely on processed.
A timeout event and a numeric time can reach the same clock value but differ in same-time ordering. Numeric stopping is the clearer half-open horizon.
Raw lines 183 to 210Manual stepping
until = 10
steps = 0
max_steps = 100_000
while env.peek() < until and steps < max_steps:
env.step()
steps += 1
if steps == max_steps and env.peek() < until:
raise RuntimeError("event budget reached")peek() returns infinity when empty; step() raises simpy.core.EmptySchedule when no event remains. Explicit step/event caps prevent a zero-delay loop from hanging diagnostics.
Sources
See sources.md for versioned environment/event guides, API references, tagged core.py, and the scheduling guide.