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()