diff --git a/ext/cool.io/loop.c b/ext/cool.io/loop.c index 7bd1bbc..9e2e4fd 100644 --- a/ext/cool.io/loop.c +++ b/ext/cool.io/loop.c @@ -113,6 +113,12 @@ void Coolio_Loop_process_event(VALUE watcher, int revents) /* The Global VM lock isn't held right now, but hopefully * we can still do this safely */ watcher_data = Coolio_Watcher_ptr(watcher); + + if (watcher_data->enabled == 0) { + /* Ignore event because watcher was already detached. */ + return; + } + loop_data = Coolio_Loop_ptr(watcher_data->loop); /* Well, what better place to explain how this all works than diff --git a/ext/cool.io/watcher.h b/ext/cool.io/watcher.h index 225e44c..b4892f2 100644 --- a/ext/cool.io/watcher.h +++ b/ext/cool.io/watcher.h @@ -33,6 +33,10 @@ if(watcher_data->loop == Qnil) \ rb_raise(rb_eRuntimeError, "not attached to a loop"); \ \ + if (watcher_data->enabled == 0) { \ + /* Ignore because watcher was already detached. */ \ + return Qnil; \ + } \ loop_data = Coolio_Loop_ptr(watcher_data->loop); \ \ ev_##watcher_type##_stop(loop_data->ev_loop, &watcher_data->event_types.ev_##watcher_type); \ diff --git a/spec/detach_race_condition_spec.rb b/spec/detach_race_condition_spec.rb new file mode 100644 index 0000000..b22970c --- /dev/null +++ b/spec/detach_race_condition_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Cool.io::Loop do + class Victim < Cool.io::IOWatcher + def initialize(io) + super + @io = io + end + + def on_readable + begin + @io.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + end + end + end + + # https://github.com/socketry/cool.io/issues/87 + it "does not raise TypeError when a watcher is detached while an event is pending" do + loop = Cool.io::Loop.default + + iterations = 200 + + expect { + iterations.times do + r_victim, w_victim = IO.pipe + victim_watcher = Victim.new(r_victim) + victim_watcher.attach(loop) + + t1 = Thread.new do + sleep 0.01 + w_victim.write("dummy\n") + end + + t2 = Thread.new do + sleep 0.01 + victim_watcher.detach + end + + loop.run_once + + t1.join + t2.join + + r_victim.close + w_victim.close + end + }.not_to raise_error + end +end