Process Interaction
Level: Intermediate
Packed orders now wait for a courier, who comes every 10 minutes and takes every order that is waiting. An order that waits too long goes by post instead, which costs more and takes longer. This page asks: when does a waiting order resume, and how long must it be allowed to wait so that most orders go by courier? It compares limits of 5 and 8 minutes over an 8-hour day.
What You Add
A signal, courier_left, that the courier fires as it leaves, and a dispatch task that waits for either the signal or its limit, whichever comes first. The shop is the one from First Simulation: one packer, an order every 2 seconds on average and 1.5 seconds of packing.
A Signal
So far a process has waited for time, a resource or an item. Processes can also wait for each other. In SimPy one process yields an event, and another succeeds it; every process waiting for the event resumes. desflow gives such an event a name, a signal: declared once, fired by a task when it finishes, and fired again later, each firing waking every waiter that waits at that moment:
| docs/snippets/tutorials/process_interaction_low_level.py:signal | |
|---|---|
Signal holds one pending SimPy Event. wait() returns it, and fire(value) succeeds it and puts a fresh one in its place, so a process that starts waiting after a firing waits for the next one. SimPy and desflow describes events.
The courier arrives every 10 minutes, and its task fires the signal with its payload:
| docs/snippets/yaml/process_interaction.yaml:courier | |
|---|---|
| docs/snippets/tutorials/process_interaction.py:courier | |
|---|---|
| docs/snippets/tutorials/process_interaction_low_level.py:courier | |
|---|---|
Wait for the First of Two
A packed order waits for the courier or for its limit. SimPy waits for the first of several events with a | b, an AnyOf; desflow's wait takes the same with any and a name for each branch. After the wait, wait.<branch>.done says whether that branch happened, so next can route on it:
"${LIMIT:-5 min}" reads the environment variable LIMIT, 5 minutes when it is not set, so LIMIT="8 min" desflow run process_interaction.yaml runs the longer limit.
env.any_of([...]) is the AnyOf; its value holds the events that happened, so courier in done is the YAML's wait.courier.done.
The task publishes queued when it starts waiting and started when the wait ends, so its wait statistics measure the time waiting for the courier:
Result
When does a waiting order resume? When the courier leaves, if that is within its limit, and at the limit otherwise. With a limit of 5 minutes, 6,952 orders go by courier and 7,020 by post; with a limit of 8 minutes, 11,005 go by courier and 2,872 by post, and the mean wait for dispatch rises from 225.3 to 288.2 seconds:

The Builder and Low-level scripts print both runs:
Why It Works
Orders are packed at random moments, so the time from packing to the next courier is spread evenly over the 10 minutes between couriers. A limit of 5 minutes catches the courier for half the orders, and a limit of 8 minutes for 8 in 10, which is what the runs show. Every waiting order resumes at the same firing, which is why the courier takes them all at once. Tasks and Processes describes every kind of wait, and Wait for One or All shows ties and failures.
Full Files
Full files
| docs/snippets/tutorials/process_interaction_low_level.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
Next, Patience asks which orders leave a queue after waiting too long, and which refuse to join.