Example of SimPy event that is triggered but not processed

102 Views Asked by At

Just like this question, I don't understand what triggered means for a SimPy event. The only answer posted does not clarify it for me.

The docs say an event:

  • might happen (not triggered),
  • is going to happen (triggered) or
  • has happened (processed).

I originally thought those corresponded to:

  • event defined
  • event yielded
  • code in event completes execution

But then I found through experimentation that was wrong. I have not been able to create an example for which I can print p.triggered and p.processed for some process p and have the first be True and the second False. They're always either both True or both False.

So my question is, can anyone give code of an example (ideally as simple as possible) for which there exists some moment of time that a process is triggered but not processed?

Ideally, it would include an explanation of "triggered" that matches the example.

2

There are 2 best solutions below

1
On BEST ANSWER

This stuff is not clear, but my understanding is that when you use env.process() to convert one of your processes to a event, that event does not get triggered until you exit your process, not when it hits a yield. So if you write a process with a infinite loop, it will always be in that might happen (not triggered) state.

Events are not marked as processed until all events that can be triggered at a time x, are triggered. I think this allows for events to talk to each other and add callbacks, before callbacks get executed. I'm not sure what happens if a callback creates a new event that is immediately triggered.

here is some code to show the event flow

"""
Quick demo showing the states of events over time

Three events are created which all expire at the same time.
One event has a callback

Looks like events are not marked as triggered until they exit their process.
And callback are not processed until all triggering is done.

Programmer: Michael R. Gibbs
"""

import simpy

class EventCheck():
    """
    class with a event process that checks the state
    of a list of events.
    """

    def __init__(self, env, name):
        self.eventList = []
        self.env = env
        self.name = name

        # create the event
        self.e = self.env.process(self.e_process())

    def set_event_list(self, list):
        """
        The list of objects of this class type
        """
        self.eventList = list

    def check_events(self):
        """
        print out the state of a list of events
        """
        for obj in self.eventList:
            print(obj.name, obj.e.triggered, obj.e.processed)

    def e_process(self):
        """
        The event that get turned into a evnet with env.process()
        call the method that prints out the states of a list of events
        """
        print(self.env.now, self.name, "created")
        yield self.env.timeout(2)
        print(self.env.now,self.name, "timeout finished")
        self.check_events()
        print(self.env.now,self.name, "finish process")

def pre_check(env, list):
    """
    Prints out the states of a event before its trigger time
    """
    
    yield env.timeout(1)
    print(env.now, "pre trigger")
    list[0].check_events()

def watcher(env, e):
    """
    Shows how a process sees a event
    """
    print(env.now, "watching")
    print(e.triggered, e.processed)
    yield e

    print(env.now, "done watching")
    print(e.triggered, e.processed)

def my_callback(e):
    """
    Shows how a callback sees a event calling it
    """
    print("callback")
    print(e.triggered, e.processed)

env = simpy.Environment()

# create three events checkers, each has its own event
e_list = [
    EventCheck(env, "e1"),
    EventCheck(env, 'e2'),
    EventCheck(env, 'e3'),    
]

# give one event a callback
e_list[0].e.callbacks.append(my_callback)

# assing each checker the same list of events
for e in e_list:
    e.set_event_list(e_list)

# start the other process that show a events state
env.process(pre_check(env, e_list))

env.process(watcher(env, e_list[0].e))

env.run(10)

# show a events state after its trigger time
print(env.now, "post trigger")
e_list[0].check_events()

and here is the output I got

0 e1 created
0 e2 created
0 e3 created
0 watching
False False
1 pre trigger
e1 False False
e2 False False
e3 False False
2 e1 timeout finished
e1 False False
e2 False False
e3 False False
2 e1 finish process
2 e2 timeout finished
e1 True False
e2 False False
e3 False False
2 e2 finish process
2 e3 timeout finished
e1 True False
e2 True False
e3 False False
2 e3 finish process
callback
True True
2 done watching
True True
10 post trigger
e1 True True
e2 True True
e3 True True
0
On

The key statement in @Michael's answer to me is "when you use env.process() to convert one of your processes to a event, that event does not get triggered until you exit your process, not when it hits a yield".

This was where I was confused, because I knew that a process is an event, and that triggered meant that the event was added to the event queue, so I didn't see how some of the code from the process could have already executed if the process was not yet triggered, since that would mean the process was not added to the event queue yet.

The answer is that, although Process is a subclass of Event, a process doesn't result in one event getting added to the event queue, but rather multiple (at least three I think) events getting added to the event queue. Whatever number of events get added to the event queue, it is the last event that is equal to the process.

If you create a process with p = env.process(your_generator(params)), then Process creates a instance of type Process and schedules an instance of type Initialize (another subclass of Event) which has a callback to the _resume method of p. So at that point, p is not scheduled (aka triggered). Then, each time p yields some event, that event gets scheduled. Only once your_generator raises StopIteration does p itself get scheduled.

Here's an example:

import simpy

def print_queue(env):
    """Print every event tuple in the queue"""
    print("  Environment queue:")
    for event in env._queue:
        print(f"\t{event}")

def print_statuses(events):
    """Print every event tuple in the queue"""
    print("  Event statuses:")
    for name, e in events.items():
        print(f"\t{name}={e}: triggered={e.triggered}, processed={e.processed}")

def monitor(env, time, **events):

    while True:

        # create (and hence trigger) next timeout
        w = env.timeout(1)

        # print current time, event queue, and the status of the events we're monitoring
        print(f'At now={env.now}')
        print_queue(env)
        print_statuses(events)

        yield w

def wait(env, t):
    print("\n*** Process setup ***\n")
    yield env.timeout(t)
    print("\n*** Process shutdown ***\n")

# create an environment
env = simpy.Environment()
print("Created environment.")
print_queue(env)

# create a timeout event
e = env.timeout(1)
print("Created a timeout event.")
print_queue(env)

# register the process with the environment
p = env.process(wait(env, 2))
print("Registered wait process.")
print_queue(env)

# because we're calling env.process(mon) after the other events, it's ID is higher,
# so it "monitors" the environment AFTER every other events/processes with the same time
# have already been processed
m = env.process(monitor(env, 4, e=e, p=p))
print("Registered monitor process.")
print_queue(env)

# print the statuses of the the events
print()
print_statuses(dict(e=e, p=p, m=m))

Which has output:

Created environment.
  Environment queue:
Created a timeout event.
  Environment queue:
    (1, 1, 0, <Timeout(1) object at 0x2a4de52a080>)
Registered wait process.
  Environment queue:
    (0, 0, 1, <Initialize() object at 0x2a4de56dbd0>)
    (1, 1, 0, <Timeout(1) object at 0x2a4de52a080>)
Registered monitor process.
  Environment queue:
    (0, 0, 1, <Initialize() object at 0x2a4de56dbd0>)
    (1, 1, 0, <Timeout(1) object at 0x2a4de52a080>)
    (0, 0, 2, <Initialize() object at 0x2a4de56db40>)

  Event statuses:
    e=<Timeout(1) object at 0x2a4de52a080>: triggered=True, processed=False
    p=<Process(wait) object at 0x2a4de529e40>: triggered=False, processed=False
    m=<Process(monitor) object at 0x2a4de528550>: triggered=False, processed=False

Then:

# run the simulation
env.run(4)

Which has output:

*** Process setup ***

At now=0
  Environment queue:
    (1, 1, 0, <Timeout(1) object at 0x2a4de52a080>)
    (1, 1, 5, <Timeout(1) object at 0x2a4de4ee2c0>)
    (4, 0, 3, <Event() object at 0x2a4de4ed960>)
    (2, 1, 4, <Timeout(2) object at 0x2a4de4ed900>)
  Event statuses:
    e=<Timeout(1) object at 0x2a4de52a080>: triggered=True, processed=False
    p=<Process(wait) object at 0x2a4de529e40>: triggered=False, processed=False
At now=1
  Environment queue:
    (2, 1, 4, <Timeout(2) object at 0x2a4de4ed900>)
    (4, 0, 3, <Event() object at 0x2a4de4ed960>)
    (2, 1, 6, <Timeout(1) object at 0x2a4de4ee860>)
  Event statuses:
    e=<Timeout(1) object at 0x2a4de52a080>: triggered=True, processed=True
    p=<Process(wait) object at 0x2a4de529e40>: triggered=False, processed=False

*** Process shutdown ***

At now=2
  Environment queue:
    (2, 1, 7, <Process(wait) object at 0x2a4de529e40>)
    (4, 0, 3, <Event() object at 0x2a4de4ed960>)
    (3, 1, 8, <Timeout(1) object at 0x2a4de4ee8f0>)
  Event statuses:
    e=<Timeout(1) object at 0x2a4de52a080>: triggered=True, processed=True
    p=<Process(wait) object at 0x2a4de529e40>: triggered=True, processed=False
At now=3
  Environment queue:
    (4, 0, 3, <Event() object at 0x2a4de4ed960>)
    (4, 1, 9, <Timeout(1) object at 0x2a4de4ee170>)
  Event statuses:
    e=<Timeout(1) object at 0x2a4de52a080>: triggered=True, processed=True
    p=<Process(wait) object at 0x2a4de529e40>: triggered=True, processed=True

To understand this output, it's helpful to know that the tuples in the event queue have 4 items: (time, priority, id, event)

The time is when they will get popped. If two events have the same time, then they are popped in order of priority. If multiple events have the same time and priority, the id is the tie breaker. The id is an integer that gets incremented each time an event is scheduled. Hence, the tuples are totally ordered.

The step by step explanation of how the event queue changes is:

The env._queue before executing `env.run(4)`
1,1,0 : e
0,0,1 : the INITIALIZE event for p
0,0,2 : the INITIALIZE event for m

Then, `env.run(4)` adds:
4,0,3 : the event that terminates the run

2,1,4 : a timeout that gets added after popping 0,0,1
1,1,5 : a timeout (the next time to monitor) that gets added after popping 0,0,2

At this point, now=0, e is already in the queue, but p has not yet been scheduled

pop 1,1,0 : e leaves the queue

2,1,6 : a timeout (the next time to monitor) that gets added after popping 1,1,5

At this point, now=1, e has left the queue, but p has not yet been scheduled

pop 2,1,4 : the timeout, which will call p to resume it

2,1,7 : after p resumes and prints *** Process shutdown ***, it raises StopIteration which causes it to get scheduled

3,1,8 : a timeout (the next time to monitor) that gets added after popping 2,1,6

At this point, now=2, e has left the queue, and p has been scheduled

pop 2,1,7 : p leaves the queue

4,1,9 : a timeout (the next time to monitor) that gets added after popping 3,1,8

At this point, now=3, e has left the queue, and p has left the queue

pop 4,0,3 : terminates the run

Although it's probably only an implementation detail, according to the docs for an event:

self.triggered = self._value is not PENDING

and

self.processed = self.callbacks is None

When a process is created, it starts with self._value = PENDING and self.callbacks = []. The methods trigger, succeed, and fail all change self._value to something other than PENDING, then schedule the event. When an event is popped from the event queue, its callbacks are called and set to None.