Skill Tracer
Skills › Scientific Agent Skills › simpy › process-interaction.md

process-interaction.md

269 lines · skills/simpy/references/process-interaction.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Process interaction and interrupts
2
3Reviewed 2026-10-01 against SimPy 4.1.2.
4

Process interaction and interrupts

Reviewed 2026-10-01 against SimPy 4.1.2.

Raw lines 1 to 4
5## Processes are event-yielding generators
6
7A process function must return a generator object. `env.process(generator)` starts
8it through an urgent `Initialize` event. The generator executes until it yields an
9Event, then resumes with that Event's value or exception.
11```python
12import simpy
14def operation(env, duration):
15 yield env.timeout(duration)
16 return {"finished_at": env.now}
18env = simpy.Environment()
19process = env.process(operation(env, 3))
20result = env.run(until=process)
21assert result == {"finished_at": 3}
22```
24Store a `Process` reference when another process must wait for or interrupt it.
25`Process` is itself an Event.

Processes are event-yielding generators

A process function must return a generator object. env.process(generator) starts it through an urgent Initialize event. The generator executes until it yields an Event, then resumes with that Event's value or exception.

import simpy

def operation(env, duration):
    yield env.timeout(duration)
    return {"finished_at": env.now}

env = simpy.Environment()
process = env.process(operation(env, 3))
result = env.run(until=process)
assert result == {"finished_at": 3}

Store a Process reference when another process must wait for or interrupt it. Process is itself an Event.

Raw lines 5 to 26
27## Waiting for another process
29```python
30def stage(env, label, duration):
31 yield env.timeout(duration)
32 return label
34def workflow(env):
35 first = env.process(stage(env, "first", 2))
36 first_result = yield first
38 second = env.process(stage(env, "second", 3))
39 second_result = yield second
40 return first_result, second_result
41```
43For parallel activities, create all Processes before yielding:
45```python
46def parallel(env):
47 a = env.process(stage(env, "a", 2))
48 b = env.process(stage(env, "b", 3))
49 results = yield a & b
50 assert results[a] == "a"
51 assert results[b] == "b"
52```
54Condition results map Event objects to values. Keep the original Process/Event
55references.

Waiting for another process

def stage(env, label, duration):
    yield env.timeout(duration)
    return label

def workflow(env):
    first = env.process(stage(env, "first", 2))
    first_result = yield first

    second = env.process(stage(env, "second", 3))
    second_result = yield second
    return first_result, second_result

For parallel activities, create all Processes before yielding:

def parallel(env):
    a = env.process(stage(env, "a", 2))
    b = env.process(stage(env, "b", 3))
    results = yield a & b
    assert results[a] == "a"
    assert results[b] == "b"

Condition results map Event objects to values. Keep the original Process/Event references.

Raw lines 27 to 56
57## Shared one-shot events
59A plain Event can passivate multiple waiters and broadcast one value:
61```python
62def listener(env, signal, log, name):
63 value = yield signal
64 log.append((name, env.now, value))
66env = simpy.Environment()
67signal = env.event()
68log = []
69env.process(listener(env, signal, log, "a"))
70env.process(listener(env, signal, log, "b"))
71signal.succeed("go")
72env.run()
73```
75Events are one-shot. For repeated signals, retain the old Event, replace the shared
76attribute, then trigger the old Event; ensure all participants read that attribute:
78```python
79class Clock:
80 def __init__(self, env):
81 self.env = env
82 self.tick = env.event()
84 def pulse(self):
85 current = self.tick
86 self.tick = self.env.event()
87 current.succeed(self.env.now)
88```
90Passing separate local event variables to two loops and then "resetting" each local
91variable creates disconnected signals. Encapsulate ownership.

Shared one-shot events

A plain Event can passivate multiple waiters and broadcast one value:

def listener(env, signal, log, name):
    value = yield signal
    log.append((name, env.now, value))

env = simpy.Environment()
signal = env.event()
log = []
env.process(listener(env, signal, log, "a"))
env.process(listener(env, signal, log, "b"))
signal.succeed("go")
env.run()

Events are one-shot. For repeated signals, retain the old Event, replace the shared attribute, then trigger the old Event; ensure all participants read that attribute:

class Clock:
    def __init__(self, env):
        self.env = env
        self.tick = env.event()

    def pulse(self):
        current = self.tick
        self.tick = self.env.event()
        current.succeed(self.env.now)

Passing separate local event variables to two loops and then "resetting" each local variable creates disconnected signals. Encapsulate ownership.

Raw lines 57 to 92
93## Interrupt delivery
95`target_process.interrupt(cause=None)` schedules an urgent `Interruption`. When
96processed, it:
981. removes the target process's resume callback from its current target Event;
992. throws `simpy.Interrupt(cause)` into the generator immediately.
101```python
102def worker(env, log):
103 try:
104 yield env.timeout(10)
105 log.append(("finished", env.now))
106 except simpy.Interrupt as interrupt:
107 log.append(("interrupted", env.now, interrupt.cause))
109def controller(env, target):
110 yield env.timeout(3)
111 target.interrupt("maintenance")
113env = simpy.Environment()
114log = []
115worker_process = env.process(worker(env, log))
116env.process(controller(env, worker_process))
117env.run()
118assert log == [("interrupted", 3, "maintenance")]
119```
121Interrupting a terminated process or the currently active process itself raises
122`RuntimeError`.

Interrupt delivery

target_process.interrupt(cause=None) schedules an urgent Interruption. When processed, it:

  1. removes the target process's resume callback from its current target Event;
  2. throws simpy.Interrupt(cause) into the generator immediately.
def worker(env, log):
    try:
        yield env.timeout(10)
        log.append(("finished", env.now))
    except simpy.Interrupt as interrupt:
        log.append(("interrupted", env.now, interrupt.cause))

def controller(env, target):
    yield env.timeout(3)
    target.interrupt("maintenance")

env = simpy.Environment()
log = []
worker_process = env.process(worker(env, log))
env.process(controller(env, worker_process))
env.run()
assert log == [("interrupted", 3, "maintenance")]

Interrupting a terminated process or the currently active process itself raises RuntimeError.

Raw lines 93 to 123
124### The target Event is not canceled
126An interrupt removes the Process callback from the Event it was yielding; it does
127not cancel the Event. The generator can re-yield that same Event:
129```python
130def temporarily_distracted(env, opening_event):
131 while True:
132 try:
133 return (yield opening_event)
134 except simpy.Interrupt:
135 yield env.timeout(1) # Handle interruption.
136 # Loop and wait for the original opening_event again.
137```
139If the original Event occurred during handling, yielding it resumes immediately
140with its value.
142Resource request events need an additional decision:
144- still waiting: re-yield the same request;
145- abandoning the wait: call `request.cancel()`;
146- already acquired: release it when leaving.
148Using the resource request as a context manager handles release/cancel on exception
149exit.

The target Event is not canceled

An interrupt removes the Process callback from the Event it was yielding; it does not cancel the Event. The generator can re-yield that same Event:

def temporarily_distracted(env, opening_event):
    while True:
        try:
            return (yield opening_event)
        except simpy.Interrupt:
            yield env.timeout(1)  # Handle interruption.
            # Loop and wait for the original opening_event again.

If the original Event occurred during handling, yielding it resumes immediately with its value.

Resource request events need an additional decision:

  • still waiting: re-yield the same request;
  • abandoning the wait: call request.cancel();
  • already acquired: release it when leaving.

Using the resource request as a context manager handles release/cancel on exception exit.

Raw lines 124 to 150
Involves a person and wait :136
151## Resumable work
153A Timeout cannot be "paused." On interruption, calculate completed work and create
154a new Timeout for the remainder:
156```python
157def resumable_job(env, total_work, log):
158 remaining = total_work
159 while remaining > 0:
160 started = env.now
161 try:
162 yield env.timeout(remaining)
163 remaining = 0
164 except simpy.Interrupt as interrupt:
165 remaining -= env.now - started
166 log.append(
167 {
168 "cause": interrupt.cause,
169 "remaining": remaining,
170 "time": env.now,
171 }
172 )
173```
175Define whether interrupted setup is lost, retained, or repeated. The code above
176assumes linear preempt-resume work.

Resumable work

A Timeout cannot be "paused." On interruption, calculate completed work and create a new Timeout for the remainder:

def resumable_job(env, total_work, log):
    remaining = total_work
    while remaining > 0:
        started = env.now
        try:
            yield env.timeout(remaining)
            remaining = 0
        except simpy.Interrupt as interrupt:
            remaining -= env.now - started
            log.append(
                {
                    "cause": interrupt.cause,
                    "remaining": remaining,
                    "time": env.now,
                }
            )

Define whether interrupted setup is lost, retained, or repeated. The code above assumes linear preempt-resume work.

Raw lines 151 to 177
178## PreemptiveResource interrupts
180A `PreemptiveResource` generates the interrupt, not application code. The cause is
181a `Preempted` record:
183```python
184def preemptible(env, resource, priority, work):
185 with resource.request(priority=priority, preempt=True) as request:
186 try:
187 yield request
188 yield env.timeout(work)
189 except simpy.Interrupt as interrupt:
190 cause = interrupt.cause
191 print(
192 "preempted by",
193 cause.by,
194 "used since",
195 cause.usage_since,
196 "resource",
197 cause.resource,
198 )
199```
201Do not assume every Interrupt has those attributes; manually generated interrupt
202causes can be any object. Use `isinstance(cause, simpy.resources.resource.Preempted)`
203when the distinction matters.

PreemptiveResource interrupts

A PreemptiveResource generates the interrupt, not application code. The cause is a Preempted record:

def preemptible(env, resource, priority, work):
    with resource.request(priority=priority, preempt=True) as request:
        try:
            yield request
            yield env.timeout(work)
        except simpy.Interrupt as interrupt:
            cause = interrupt.cause
            print(
                "preempted by",
                cause.by,
                "used since",
                cause.usage_since,
                "resource",
                cause.resource,
            )

Do not assume every Interrupt has those attributes; manually generated interrupt causes can be any object. Use isinstance(cause, simpy.resources.resource.Preempted) when the distinction matters.

Raw lines 178 to 204
205## Timeout/renege races
207```python
208def impatient(env, resource, patience):
209 with resource.request() as request:
210 patience_event = env.timeout(patience)
211 result = yield request | patience_event
212 if request not in result:
213 return {"outcome": "reneged", "time": env.now}
214 yield env.timeout(2)
215 return {"outcome": "served", "time": env.now}
216```
218At an exact tie, `AnyOf` may contain multiple events that occurred before the
219Condition was processed. Decide tie policy explicitly:
221```python
222if request in result:
223 # This policy treats simultaneous grant/patience as served.
224 ...
225```
227The losing Timeout remains in the queue. The context manager cancels only a pending
228request, not arbitrary condition members.

Timeout/renege races

def impatient(env, resource, patience):
    with resource.request() as request:
        patience_event = env.timeout(patience)
        result = yield request | patience_event
        if request not in result:
            return {"outcome": "reneged", "time": env.now}
        yield env.timeout(2)
        return {"outcome": "served", "time": env.now}

At an exact tie, AnyOf may contain multiple events that occurred before the Condition was processed. Decide tie policy explicitly:

if request in result:
    # This policy treats simultaneous grant/patience as served.
    ...

The losing Timeout remains in the queue. The context manager cancels only a pending request, not arbitrary condition members.

Raw lines 205 to 229
230## Failure propagation
232An uncaught process exception fails the Process. A parent yielding that Process
233receives the exception:
235```python
236def broken(env):
237 yield env.timeout(1)
238 raise ValueError("model invariant failed")
240def supervisor(env):
241 try:
242 yield env.process(broken(env))
243 except ValueError:
244 return "handled"
245```
247Do not broadly suppress failures merely to keep a simulation running. Convert only
248expected domain failures into explicit state; let invariant/programming errors fail
249tests.

Failure propagation

An uncaught process exception fails the Process. A parent yielding that Process receives the exception:

def broken(env):
    yield env.timeout(1)
    raise ValueError("model invariant failed")

def supervisor(env):
    try:
        yield env.process(broken(env))
    except ValueError:
        return "handled"

Do not broadly suppress failures merely to keep a simulation running. Convert only expected domain failures into explicit state; let invariant/programming errors fail tests.

Raw lines 230 to 250
251## Deadlock and liveness checks
253`env.run()` returning because the queue is empty does not prove that every intended
254process completed. Processes can remain waiting on untriggered events with no future
255trigger. Keep references to required completion Processes and run until an explicit
256completion Event; if the schedule empties first, SimPy raises `RuntimeError`.
258For production models:
260- set a numeric horizon and event/entity caps;
261- count completed and unfinished entities;
262- assert conservation of resources/items;
263- detect zero-delay recurrence;
264- trace a small deterministic scenario before stochastic experiments.

Deadlock and liveness checks

env.run() returning because the queue is empty does not prove that every intended process completed. Processes can remain waiting on untriggered events with no future trigger. Keep references to required completion Processes and run until an explicit completion Event; if the schedule empties first, SimPy raises RuntimeError.

For production models:

  • set a numeric horizon and event/entity caps;
  • count completed and unfinished entities;
  • assert conservation of resources/items;
  • detect zero-delay recurrence;
  • trace a small deterministic scenario before stochastic experiments.
Raw lines 251 to 265
266## Sources
268See `sources.md` for the versioned Process Interaction, Events, and resource guides
269and API references.

Sources

See sources.md for the versioned Process Interaction, Events, and resource guides and API references.

Raw lines 266 to 269