socketry/timers

Cancelling does not cancel until next interval

Closed this issue · 5 comments

Consider the following app:

require "timers"


class Application
  TEN_SECONDS = 10

  def initialize
    @should_continue = nil
    @timer_group = Timers::Group.new
  end

  def start
    puts "Starting on thread #{Thread.current.object_id}"
    @should_continue = true
    @timer_group.now_and_every(TEN_SECONDS) {
      puts "Running on thread #{Thread.current.object_id}"
    }

    while @should_continue
      puts "Waiting on thread #{Thread.current.object_id}"
      @timer_group.wait
    end
  end

  def stop
    puts "Stopping on thread #{Thread.current.object_id}"
    @should_continue = false
    @timer_group.cancel
  end
end


app = Application.new
Signal.trap("INT") { app.stop }
app.start

When I run this program and then press CTRL+C, I immediately see Stopping on thread xxxxxx but the program does not actually terminate until 10 seconds has elapsed.

$ bundle exec ruby app.rb
Starting on thread 70127555125280
Running on thread 70127555125280
Waiting on thread 70127555125280
Running on thread 70127555125280
Waiting on thread 70127555125280
^CStopping on thread 70127555125280
--- hangs here for 10 seconds ---
$

At first, I suspected that this was due to everything running on one thread but I ran a test where I called @timer_group.cancel on a separate thread and still the program did not terminate until the wait period elapsed. This will be a problem for me because my real program's interval will be 30 minutes.

Is there a way for me to have the call to cancel take effect right away?

Timers::Group is not thread safe so don't try calling methods from a different thread.

@timer_group.wait is like calling sleep x.

@timer_group.wait do |duration|
  sleep duration
end

is the same.

I made a test program:

require 'timers'

group = Timers::Group.new

group.now_and_every(5) do
	puts "Running on thread #{Thread.current.object_id}"
end

while true
	puts "Waiting on thread #{Thread.current.object_id}"
	group.wait
end

Signal.trap("INT") {puts "interrupted"}

It exits with Interrupt when I press Ctrl-C:

Running on thread 60
Waiting on thread 60
^CTraceback (most recent call last):
        2: from /tmp/d891193c-b95e-4af8-9fe4-2483db895759:12:in `<main>'
        1: from /home/samuel/.gem/ruby/2.7.1/gems/timers-4.3.0/lib/timers/group.rb:81:in `wait'
/home/samuel/.gem/ruby/2.7.1/gems/timers-4.3.0/lib/timers/group.rb:81:in `sleep': Interrupt
Exited with signal 2 after 2.4 seconds

Oh, sorry, I made a mistake, the interrupt handler was not registered.

Hmm, I tried this:

Signal.trap("INT") do
	puts "interrupted"
end

while true
	puts "Waiting on thread #{Thread.current.object_id}"
	duration = sleep(2)
	puts "duration: #{duration}"
end

But the interrupt does not cancel sleep. So this is probably not something we can solve with Timers alone. You need to raise an exception from your interrupt handler.

Thanks to your advice regarding the need to raise an exception, the following program now does what I want:

require "timers"


class Cancelled < Exception
end


class Application
  TEN_SECONDS = 10

  def initialize
    @should_continue = nil
    @timer_group = Timers::Group.new
  end

  def start
    @should_continue = true
    puts "Starting on thread #{Thread.current.object_id}"

    while @should_continue
      @timer_group.after(TEN_SECONDS) {
        puts "Running on thread #{Thread.current.object_id}"
      }
      begin
        puts "Waiting on thread #{Thread.current.object_id}"
        @timer_group.wait
      rescue Cancelled
        @should_continue = false
      rescue Exception => ex
        puts "Caught unhandled exception #{ex.message}"
      end
    end
  end

  def stop
    puts "Stopping on thread #{Thread.current.object_id}"
    raise Cancelled.new
  end
end


app = Application.new
Signal.trap("INT") { app.stop }
app.start

When running this program, sending CTRL+C immediately stops the program:

$ bundle exec ruby app.rb
Starting on thread 70358631907280
Waiting on thread 70358631907280
Running on thread 70358631907280
Waiting on thread 70358631907280
Running on thread 70358631907280
Waiting on thread 70358631907280
^CStopping on thread 70358631907280

In C, I believe sleep would return with some kind of errno if interrupted early. At least, that's what I'd check first, but I guess Ruby does not propagate this.