Environment.run(until=event_that_never_gets_triggered) always succeeds.
Created by: sscherfke
While investigating issue #63 (closed), I stumbled upon a very strange (and IMHO unintended) behavior of SimPy.
#!python
>>> import simpy
>>>
>>> env = simpy.Environment()
>>>
>>> def proc(env):
... evt = env.event()
... yield evt
... assert evt.triggered
...
>>> # This *should* block forever
... env.run(until=env.process(proc(env)))
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "/Users/stefan/Projects/simpy/simpy/core.py", line 130, in run
assert until.triggered
AssertionError
>>>
>>> # Okay, but THIS should block forever
... evt = env.event()
>>> env.run(until=evt)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/stefan/Projects/simpy/simpy/core.py", line 130, in run
assert until.triggered
AssertionError
>>> assert evt.triggered
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
If you pass an event to run() I would expect that run() only returns once the event has been triggered and I would also expect that the event has been triggered.
The reason for the example above to work is the intended behavior for run([until=None]). In that case we create an internal dummy event that never gets triggered (and thus never gets scheduled). So eventually, step() will raise an EmptySchedule and run() returns.
When we explicitly pass an event, that we never trigger, to run() we cause the same behavior, so run() is equivalent to run(until=None), run(until=env.event()) or run(until=process_that_blocks_forever).
So the main question that we have to discuss is:
What is the intended behavior for run(until=event_that_never_is_triggered)?
- a. Actually wait for event_that_never_is_triggered which would mean, block forever0
- b. Ignore event_that_never_is_triggered if our internal schedule becomes empty and that event has not yet been triggered.
b is the current (untested) behavior, but a would be what I expect.
Implementing a seems easy at first:
#!python
def run(self, until=None):
"""Executes :meth:`step()` until the given criterion *until* is met.
- If it is ``None`` (which is the default), this method will return
when there are no further events to be processed.
- If it is an :class:`~simpy.events.Event`, the method will continue
stepping until this event has been triggered and will return its
value.
- If it is a number, the method will continue stepping
until the environment's time reaches *until*.
"""
if not (until is None or isinstance(until, Event)):
# Assume that *until* is a number if it is not None and
# not an event. Create a Timeout(until) in this case.
at = float(until)
if at <= self.now:
raise ValueError('until(=%s) should be > the current '
'simulation time.' % at)
# Schedule the event with before all regular timeouts.
until = Event(self)
until.ok = True
until._value = None
self.schedule(until, URGENT, at - self.now)
if until is not None:
if until.callbacks is None:
# Until event has already been processed.
return until.value
until.callbacks.append(_stop_simulate)
try:
while True:
self.step()
except EmptySchedule:
pass
if until is None:
return
assert until.triggered
if not until.ok:
raise until.value
return until.value
However, an event that never is triggered won’t prevent EmptySchedule from being raised, so the assert will fail if we do something like run(until=env.event()).
To solve this, we could either:
- find a way to block until until is actually triggered (meaning that
run()will never return if we never trigger until) - Raise an exception if we get an
EmptyScheduleand until is not triggered and tell the user that he would have created a simulation blocking forever.
What do you think @luensdorf ?