diff --git a/lib/async/task.rb b/lib/async/task.rb index ffc2850d..6629c0e1 100644 --- a/lib/async/task.rb +++ b/lib/async/task.rb @@ -327,6 +327,11 @@ def result # @parameter later [Boolean] Whether to cancel the task later, or immediately. # @parameter cause [Exception] The cause of the cancel operation. def cancel(later = false, cause: $!) + # On affected Ruby versions, `Thread#kill` exposes the internal `TAG_FATAL` value through `$!`, which is not a valid exception cause: + unless cause.is_a?(::Exception) + cause = nil + end + # If no cause is given, we generate one from the current call stack: unless cause cause = Cancel::Cause.for("Cancelling task!") diff --git a/releases.md b/releases.md index 0510ff3d..362b07e1 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Fixed `Thread#kill` raising a `TypeError` while shutting down a thread with an active `Async::Reactor`. + ## v2.45.1 - Fixed `Scheduler#io_wait` returning `nil` instead of `false` when an explicit timeout expired. Native callers such as `Socket#connect` with `connect_timeout:` distinguish a timeout by checking for `false`, so the `nil` caused `TypeError: no implicit conversion from nil to integer` instead of the intended `IO::TimeoutError`. diff --git a/test/async/scheduler.rb b/test/async/scheduler.rb index 6870b356..d845f238 100644 --- a/test/async/scheduler.rb +++ b/test/async/scheduler.rb @@ -85,6 +85,33 @@ end with "#close" do + it "can close when the owning thread is killed" do + ready = Thread::Queue.new + + thread = Thread.new do + reactor = Async::Reactor.new + + reactor.async do + reactor.async do + sleep + end + end + + ready << true + reactor.run + end + thread.report_on_exception = false + + ready.pop + thread.kill + + expect do + thread.join + end.not.to raise_exception + + expect(thread).not.to be(:alive?) + end + it "runs the event loop until terminated" do scheduler = Async::Scheduler.new Async::Node.new(scheduler)