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.