Environment.run(until=event_that_never_gets_triggered) always succeeds.

Issue #64 resolved
Stefan Scherfke
created an issue

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 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:

    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 EmptySchedule and until is not triggered and tell the user that he would have created a simulation blocking forever.

What do you think @Ontje Lünsdorf ?

Comments (6)

  1. Ontje Lünsdorf

    simpy should never block forever. It is a simulation. I think an EmptySchedule exception should be raised if all events have been processed but the until event is not triggered.

  2. Andreas Beham

    The only case that I could think of where waiting makes sense despite an EmptySchedule would be in the RealtimeEnvironment. In the other environment I would not do this.

  3. Ontje Lünsdorf

    I think we should also raise an error in RealtimeEnvironment. run(until=evt) should run until evt is triggered, but we know that this will never happen and should report that to the user.

  4. Log in to comment