From d67b86f88b828a2719dca893883f779511ba3059 Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Wed, 9 Sep 2026 20:00:41 +0900 Subject: [PATCH 1/3] Add specs for where Fiber#transfer returns control A Fiber entered by Fiber#transfer has no resumer, so when it finishes or is killed, control goes to the innermost Fiber reachable from the root Fiber by following resumes, which is not necessarily the Fiber that transferred. Also covers a transferring Fiber that was itself entered by Fiber#raise. --- core/fiber/kill_spec.rb | 11 +++++++++ core/fiber/transfer_spec.rb | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/core/fiber/kill_spec.rb b/core/fiber/kill_spec.rb index abf23ff17..632507293 100644 --- a/core/fiber/kill_spec.rb +++ b/core/fiber/kill_spec.rb @@ -75,6 +75,17 @@ rescue_executed.should == false end + it "returns control to the transferring fiber when killing a fiber entered by Fiber#transfer" do + states = [] + runner = Fiber.new do + Fiber.new { Fiber.current.kill; states << :unreachable }.transfer + states << :runner_resumed + end + + runner.resume + states.should == [:runner_resumed] + end + it "repeatedly kills a fiber" do fiber = Fiber.new do while true; Fiber.yield; end diff --git a/core/fiber/transfer_spec.rb b/core/fiber/transfer_spec.rb index d8737aeeb..4f695db1b 100644 --- a/core/fiber/transfer_spec.rb +++ b/core/fiber/transfer_spec.rb @@ -20,6 +20,54 @@ f2.transfer.should == :fiber_2 end + it "returns control to the transferring fiber when the target finishes" do + states = [] + runner = Fiber.new do + Fiber.new { states << :target_end }.transfer + states << :runner_resumed + end + + runner.resume + states.should == [:target_end, :runner_resumed] + end + + it "returns control to a transferring fiber which was itself entered by Fiber#raise" do + states = [] + target = Fiber.new do + begin + Fiber.yield + rescue RuntimeError + Fiber.new { states << :inner_end }.transfer + states << :target_after_transfer + end + end + + target.resume + target.raise "raised" + states.should == [:inner_end, :target_after_transfer] + end + + it "returns control according to the resume chain, not to the Fiber which transferred" do + states = [] + outer = Fiber.new do + middle = Fiber.new do + inner = Fiber.new do + Fiber.new { states << :leaf_end }.transfer + states << :inner_after_transfer + end + inner.resume + states << :middle_after_resume + end + middle.transfer + states << :outer_after_transfer + end + + outer.resume + # The leaf has no resumer, so control follows resumes down from the root Fiber and + # stops at outer. inner, which transferred to the leaf, stays suspended. + states.should == [:leaf_end, :outer_after_transfer] + end + it "can be invoked from the same Fiber it transfers control to" do states = [] fiber = Fiber.new { states << :start; fiber.transfer; states << :end } From 96e0ac9c4dc450d1ba6968c79b329458097432e3 Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Thu, 10 Sep 2026 14:15:31 +0900 Subject: [PATCH 2/3] Add specs for Fiber#kill edge cases Fiber#kill unwinds by transferring into the fiber, so it returns the fiber rather than nil, is a no-op on one that is already dead, and runs the ensure block while the fiber is still current and alive. An exception raised from that ensure block reaches the caller of kill, and killing a fiber belonging to another Thread raises FiberError. Also covers two cases where kill and transfer meet: killing a fiber from the fiber it transferred to, and killing a fiber whose ensure block transfers away. --- core/fiber/kill_spec.rb | 91 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/core/fiber/kill_spec.rb b/core/fiber/kill_spec.rb index 632507293..10c76ac6d 100644 --- a/core/fiber/kill_spec.rb +++ b/core/fiber/kill_spec.rb @@ -75,6 +75,97 @@ rescue_executed.should == false end + it "returns the fiber" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + + fiber.kill.should.equal?(fiber) + end + + it "returns the fiber when it is already dead" do + fiber = Fiber.new { :done } + fiber.resume + + fiber.kill.should.equal?(fiber) + end + + it "does nothing when killing a fiber twice" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + + fiber.kill + fiber.kill + fiber.alive?.should == false + end + + it "runs the ensure block in the fiber being killed, while it is still alive" do + states = [] + + fiber = Fiber.new do + Fiber.yield + ensure + states << Fiber.current.equal?(fiber) << fiber.alive? + end + + fiber.resume + fiber.kill + states.should == [true, true] + end + + it "propagates an exception raised by the ensure block to the killing fiber" do + fiber = Fiber.new do + Fiber.yield + ensure + raise "from ensure" + end + + fiber.resume + -> { fiber.kill }.should.raise(RuntimeError, "from ensure") + end + + it "raises a FiberError when killing a fiber from a different Thread" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + + Thread.new do + -> { fiber.kill }.should.raise(FiberError) + end.join + end + + it "kills a fiber which transferred, from the fiber it transferred to" do + states = [] + outer = nil + + outer = Fiber.new do + Fiber.new do + outer.kill + states << :inner_resumed + end.transfer + states << :outer_resumed + end + + outer.resume + # Killing outer transfers into it to unwind, and the inner fiber is left + # suspended, so neither fiber runs again. + states.should == [] + outer.alive?.should == false + end + + it "kills a fiber whose ensure block transfers to another fiber" do + other = Fiber.new { :other } + + fiber = Fiber.new do + Fiber.yield + ensure + other.transfer + end + + fiber.resume + fiber.kill + fiber.alive?.should == false + other.alive?.should == false + end + it "returns control to the transferring fiber when killing a fiber entered by Fiber#transfer" do states = [] runner = Fiber.new do From 74b86be27e55d41c855ac1faabe7bbfe281c2f0b Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Thu, 10 Sep 2026 19:33:23 +0900 Subject: [PATCH 3/3] Add specs for Fiber#kill's return and Fiber#raise Kill a Fiber twice and the second call returns false, not the Fiber. Running to completion does not count as killed, so one that ended on its own still returns itself. Raising into the Fiber that resumed you delivers the exception there and leaves you suspended. The resume it interrupts never returns, so the rest of the raising Fiber never runs. --- core/fiber/kill_spec.rb | 8 ++++++++ core/fiber/raise_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/core/fiber/kill_spec.rb b/core/fiber/kill_spec.rb index 10c76ac6d..41e0deeb5 100644 --- a/core/fiber/kill_spec.rb +++ b/core/fiber/kill_spec.rb @@ -89,6 +89,14 @@ fiber.kill.should.equal?(fiber) end + it "returns false when the fiber was already killed" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + + fiber.kill.should.equal?(fiber) + fiber.kill.should == false + end + it "does nothing when killing a fiber twice" do fiber = Fiber.new { Fiber.yield } fiber.resume diff --git a/core/fiber/raise_spec.rb b/core/fiber/raise_spec.rb index 107e5bd4c..9ebd46518 100644 --- a/core/fiber/raise_spec.rb +++ b/core/fiber/raise_spec.rb @@ -17,6 +17,30 @@ -> { FiberSpecs::NewFiberToRaise.raise }.should.raise(RuntimeError) end + it "raises in a Fiber which is resuming the current Fiber, without resuming the current Fiber again" do + states = [] + outer = inner = nil + + outer = Fiber.new do + inner = Fiber.new do + outer.raise RuntimeError, "raised" + states << :inner_continued + end + + begin + inner.resume + states << :resume_returned + rescue RuntimeError + states << :outer_rescued + end + + states << :outer_done + end + + outer.transfer + states.should == [:outer_rescued, :outer_done] + end + it "raises FiberError if Fiber is not born" do fiber = Fiber.new { true } -> { fiber.raise }.should.raise(FiberError, "cannot raise exception on unborn fiber")