While investigating issue
#63, I stumbled upon a very strange (and IMHO unintended) behavior of SimPy.
>>> 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
When we explicitly pass an event, that we never trigger, to
run() we cause the same behavior, so
run() is equivalent to
So the main question that we have to discuss is:
What is the intended behavior for
- 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:
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
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 @Ontje Lünsdorf ?