How do I interrupt or cancel a SimPy Timeout event?
I want to create a timer with a callback that can be interrupted or reset using SimPy. If interrupted, I do not want the callback to be executed, and if reset, I want the timer to restart with the same delay from env.now
. This seemed like an easy thing to do initially by simply using env.timeout
. However, the documentation notes:
To actually let time pass in a simulation, there is the timeout event. A timeout has two parameters: a delay and an optional value: Timeout(delay, value=None). It triggers itself during its creation and schedules itself at now + delay. Thus, the succeed() and fail() methods cannot be called again and you have to pass the event value to it when you create the timeout.
Because the simulation starts triggered, I can't add callbacks and because you can't call fail
, I can't interrupt the timeout.
I've considered just implementing a process that waits one timestep and checks a flag if it's been interrupted or reached the env.now
it was waiting for, but this seems horribly inefficient, and if I have a lot of timers (which I will), I'm worried that the number of generators will overwhelm the simulation. (The timeout function seems to work by scheduling itself in the future of the simulation, which is why you can have a ton of those running around).
So the spec is - create an event that triggers a callback after a specified amount of time, but that can be reset or interrupted before that time occurs. Any thoughts?
Well, if I understood your question correctly one thing you can do is create a Timer
class with a wait method which checks for simpy.Interrupt
. You can imlement stop()
so that when it's called, you also call interrupt()
. That way the callback will not be executed as long as interrupt()
has previously been called. A reset method would simply call stop()
(interrupt) and start()
again thus setting the action back to running()
and calling wait()
again, allowing the callback to be executed again after every timeout until interrupt is called again.
Here's an example implementation of such a Timer
class:
import simpy
class Timer(object):
def __init__(self, env, delay, callback):
self.env = env
self.delay = delay
self.action = None
self.callback = callback
self.running = False
self.canceled = False
def wait(self):
"""
Calls a callback after time has elapsed.
"""
try:
yield self.env.timeout(self.delay)
self.callback()
self.running = False
except simpy.Interrupt as i:
print "Interrupted!"
self.canceled = True
self.running = False
def start(self):
"""
Starts the timer
"""
if not self.running:
self.running = True
self.action = self.env.process(self.wait())
def stop(self):
"""
Stops the timer
"""
if self.running:
self.action.interrupt()
self.action = None
def reset(self):
"""
Interrupts the current timer and restarts.
"""
self.stop()
self.start()
I know this is an old SO question, but I was looking for a simple SimPy Timer
class just like the one provided in @kxirog's answer (which was very useful!), and had a fix for the issue with reset
pointed out by @bbengfort. The reset
function assumes that interrupt
executes immediately, but this is not the case as explained in the SimPy documentation:
What process.interrupt() actually does is scheduling an Interruption event for immediate execution.
So the call to self.start
actually executes before the wait
process has been interrupted, and that prevented it from starting a new wait
process.
Below is a modified version where the reset should work.
import simpy
class Timer(object):
def __init__(self, env, delay, callback):
self.env = env
self.delay = delay
self.action = None
self.callback = callback
def wait(self):
"""
Calls a callback after time has elapsed.
"""
try:
yield self.env.timeout(self.delay)
self.callback()
except simpy.Interrupt:
print("Interrupted!")
def running(self):
"""
Check if timer is running
"""
return self.action is not None and self.action.is_alive
def start(self):
"""
Starts the timer
"""
if not self.running():
self.action = self.env.process(self.wait())
def stop(self):
"""
Stops the timer
"""
if self.running():
self.action.interrupt()
self.action = None
def reset(self):
"""
Interrupts the current timer and restarts.
"""
self.stop()
self.start()