1===================
2Process Interaction
3===================
4
5Discrete event simulation is only made interesting by interactions between
6processes.
7
8So this guide is about:
9
10* :ref:`sleep-until-woken-up` (passivate/reactivate)
11* :ref:`waiting-for-another-process-to-terminate`
12* :ref:`interrupting-another-process`
13
14The first two items were already covered in the :doc:`events` guide, but we'll
15also include them here for the sake of completeness.
16
17Another possibility for processes to interact are resources. They are discussed
18in a :doc:`separate guide <resources>`.
19
20
21.. _sleep-until-woken-up:
22
23Sleep until woken up
24====================
25
26Imagine you want to model an electric vehicle with an intelligent
27battery-charging controller. While the vehicle is driving, the controller can
28be passive but needs to be reactivate once the vehicle is connected to the
29power grid in order to charge the battery.
30
31In SimPy 2, this pattern was known as *passivate / reactivate*. In SimPy 3,
32you can accomplish that with a simple, shared :class:`~simpy.events.Event`:
33
34.. code-block:: python
35
36   >>> from random import seed, randint
37   >>> seed(23)
38   >>>
39   >>> import simpy
40   >>>
41   >>> class EV:
42   ...     def __init__(self, env):
43   ...         self.env = env
44   ...         self.drive_proc = env.process(self.drive(env))
45   ...         self.bat_ctrl_proc = env.process(self.bat_ctrl(env))
46   ...         self.bat_ctrl_reactivate = env.event()
47   ...
48   ...     def drive(self, env):
49   ...         while True:
50   ...             # Drive for 20-40 min
51   ...             yield env.timeout(randint(20, 40))
52   ...
53   ...             # Park for 1–6 hours
54   ...             print('Start parking at', env.now)
55   ...             self.bat_ctrl_reactivate.succeed()  # "reactivate"
56   ...             self.bat_ctrl_reactivate = env.event()
57   ...             yield env.timeout(randint(60, 360))
58   ...             print('Stop parking at', env.now)
59   ...
60   ...     def bat_ctrl(self, env):
61   ...         while True:
62   ...             print('Bat. ctrl. passivating at', env.now)
63   ...             yield self.bat_ctrl_reactivate  # "passivate"
64   ...             print('Bat. ctrl. reactivated at', env.now)
65   ...
66   ...             # Intelligent charging behavior here …
67   ...             yield env.timeout(randint(30, 90))
68   ...
69   >>> env = simpy.Environment()
70   >>> ev = EV(env)
71   >>> env.run(until=150)
72   Bat. ctrl. passivating at 0
73   Start parking at 29
74   Bat. ctrl. reactivated at 29
75   Bat. ctrl. passivating at 60
76   Stop parking at 131
77
78Since ``bat_ctrl()`` just waits for a normal event, we no longer call this
79pattern *passivate / reactivate* in SimPy 3.
80
81
82.. _waiting-for-another-process-to-terminate:
83
84Waiting for another process to terminate
85========================================
86
87The example above has a problem: it may happen that the vehicle wants to park
88for a shorter duration than it takes to charge the battery (this is the case if
89both charging and parking would take 60 to 90 minutes).
90
91To fix this problem we have to slightly change our model. A new ``bat_ctrl()``
92will be started every time the EV starts parking. The EV then waits until the
93parking duration is over *and* until the charging has stopped:
94
95.. code-block:: python
96
97   >>> class EV:
98   ...     def __init__(self, env):
99   ...         self.env = env
100   ...         self.drive_proc = env.process(self.drive(env))
101   ...
102   ...     def drive(self, env):
103   ...         while True:
104   ...             # Drive for 20-40 min
105   ...             yield env.timeout(randint(20, 40))
106   ...
107   ...             # Park for 1–6 hours
108   ...             print('Start parking at', env.now)
109   ...             charging = env.process(self.bat_ctrl(env))
110   ...             parking = env.timeout(randint(60, 360))
111   ...             yield charging & parking
112   ...             print('Stop parking at', env.now)
113   ...
114   ...     def bat_ctrl(self, env):
115   ...         print('Bat. ctrl. started at', env.now)
116   ...         # Intelligent charging behavior here …
117   ...         yield env.timeout(randint(30, 90))
118   ...         print('Bat. ctrl. done at', env.now)
119   ...
120   >>> env = simpy.Environment()
121   >>> ev = EV(env)
122   >>> env.run(until=310)
123   Start parking at 29
124   Bat. ctrl. started at 29
125   Bat. ctrl. done at 83
126   Stop parking at 305
127
128Again, nothing new (if you've read the :doc:`events` guide) and special is
129happening. SimPy processes are events, too, so you can yield them and will thus
130wait for them to get triggered. You can also wait for two events at the same
131time by concatenating them with ``&`` (see
132:ref:`waiting_for_multiple_events_at_once`).
133
134
135.. _interrupting-another-process:
136
137Interrupting another process
138============================
139
140As usual, we now have another problem: Imagine, a trip is very urgent, but with
141the current implementation, we always need to wait until the battery is fully
142charged. If we could somehow interrupt that ...
143
144Fortunate coincidence, there is indeed a way to do exactly this. You can call
145``interrupt()`` on a :class:`~simpy.events.Process`. This will throw an
146:class:`~simpy.exceptions.Interrupt` exception into that process, resuming it
147immediately:
148
149.. code-block:: python
150
151   >>> class EV:
152   ...     def __init__(self, env):
153   ...         self.env = env
154   ...         self.drive_proc = env.process(self.drive(env))
155   ...
156   ...     def drive(self, env):
157   ...         while True:
158   ...             # Drive for 20-40 min
159   ...             yield env.timeout(randint(20, 40))
160   ...
161   ...             # Park for 1 hour
162   ...             print('Start parking at', env.now)
163   ...             charging = env.process(self.bat_ctrl(env))
164   ...             parking = env.timeout(60)
165   ...             yield charging | parking
166   ...             if not charging.triggered:
167   ...                 # Interrupt charging if not already done.
168   ...                 charging.interrupt('Need to go!')
169   ...             print('Stop parking at', env.now)
170   ...
171   ...     def bat_ctrl(self, env):
172   ...         print('Bat. ctrl. started at', env.now)
173   ...         try:
174   ...             yield env.timeout(randint(60, 90))
175   ...             print('Bat. ctrl. done at', env.now)
176   ...         except simpy.Interrupt as i:
177   ...             # Onoes! Got interrupted before the charging was done.
178   ...             print('Bat. ctrl. interrupted at', env.now, 'msg:',
179   ...                   i.cause)
180   ...
181   >>> env = simpy.Environment()
182   >>> ev = EV(env)
183   >>> env.run(until=100)
184   Start parking at 31
185   Bat. ctrl. started at 31
186   Stop parking at 91
187   Bat. ctrl. interrupted at 91 msg: Need to go!
188
189What ``process.interrupt()`` actually does is scheduling an
190:class:`~simpy.events.Interruption` event for immediate execution. If this
191event is executed it will remove the victim process' ``_resume()`` method from
192the callbacks of the event that it is currently waiting for (see
193:attr:`~simpy.events.Process.target`). Following that it will throw the
194``Interrupt`` exception into the process.
195
196Since we don't do anything special to the original target event of the process,
197the interrupted process can yield the same event again after catching the
198``Interrupt`` – Imagine someone waiting for a shop to open. The person may get
199interrupted by a phone call.  After finishing the call, he or she checks if the
200shop already opened and either enters or continues to wait.
201