Bug #20089
openFiber#kill transfers to root fiber
Description
I was hoping to use Fiber#kill
to clean up formerly .transfer
-d Fibers and work around https://bugs.ruby-lang.org/issues/20081, but I found that Fiber#kill
has a similar control flow jump behavior. Is this on purpose, or a bug?
Here's a script to test the behavior:
manager = Fiber.new do
worker = Fiber.new do
puts "2. Begin Worker"
manager.transfer
puts "This should never print -- killed"
end
puts "1. Transfer to Worker"
worker.transfer
puts "3. Killing Worker"
worker.kill
puts "4. Finished manager"
end
manager.transfer
puts "5. Finished script"
I expected items 1
through 5
to be printed in order, but in fact, 4
is never printed:
$ ruby fiber_transfer_test.rb
1. Transfer to Worker
2. Begin Worker
3. Killing Worker
5. Finished script
It seems like worker.kill
is transferring control to the top-level fiber instead of giving it back to manager
.
I also tried having the thread kill itself, hoping it would return to the fiber that originally .transfer
ed to it, but it also seems to jump out:
manager = Fiber.new do
worker = Fiber.new do
puts "2. Begin Worker"
manager.transfer
Fiber.current.kill
puts "This should never print -- killed"
end
puts "1. Transfer to Worker"
worker.transfer
puts "3. Killing Worker"
worker.transfer
puts "4. Finished manager"
end
manager.transfer
puts "5. Finished script"
Prints:
1. Transfer to Worker
2. Begin Worker
3. Killing Worker
5. Finished script
Updated by ioquatix (Samuel Williams) 8 months ago
Transfer is uni-directional and keeps no state about the caller. It's up to the caller to implement its own control flow if preferred. It's a low level operation which must be used more carefully.
manager = Fiber.new do
worker = Fiber.new do
puts "2. Begin Worker"
manager.transfer
Fiber.current.kill
puts "This should never print -- killed"
ensure
manager.transfer
end
puts "1. Transfer to Worker"
worker.transfer
puts "3. Killing Worker"
worker.transfer
puts "4. Finished manager"
end
manager.transfer
puts "5. Finished script"
By explicitly adding the flow control, we can achieve your desired output. Hope this helps.
Updated by rmosolgo (Robert Mosolgo) 8 months ago
That definitely makes sense for a Fiber killing itself, but would you say that killing a different Fiber should cause a fiber to transfer away? In my first script above, calling worker.kill
causes the manager
fiber to transfer.
I looked a little deeper, it looks like this only happens when both Fibers have been started with .transfer
. Here's a scenario with two different Fibers, one killing the other, and only the .transfer
/.transfer
case exits early:
puts "\n\nResume/Transfer"
fiber1 = Fiber.new {
puts "1. Fiber1 Runs"
fiber2 = Fiber.new {
puts "2. Fiber2 Runs"
fiber1.transfer
puts "Never: Fiber2 is killed"
}
fiber2.transfer
fiber2.kill
puts "3. Fiber1 finishes"
}
fiber1.resume
puts "4. Exit"
# Resume/Transfer
# 1. Fiber1 Runs
# 2. Fiber2 Runs
# 3. Fiber1 finishes
# 4. Exit
puts "\n\nTransfer/Resume"
fiber1 = Fiber.new {
puts "1. Fiber1 Runs"
fiber2 = Fiber.new {
puts "2. Fiber2 Runs"
Fiber.yield
puts "Never: Fiber2 is killed"
}
fiber2.resume
fiber2.kill
puts "3. Fiber1 finishes"
}
fiber1.transfer
puts "4. Exit"
# Transfer/Resume
# 1. Fiber1 Runs
# 2. Fiber2 Runs
# 3. Fiber1 finishes
# 4. Exit
puts "\n\nResume/Resume"
fiber1 = Fiber.new {
puts "1. Fiber1 Runs"
fiber2 = Fiber.new {
puts "2. Fiber2 Runs"
Fiber.yield
puts "Never: Fiber2 is killed"
}
fiber2.resume
fiber2.kill
puts "3. Fiber1 finishes"
}
fiber1.resume
puts "4. Exit"
# Resume/Resume
# 1. Fiber1 Runs
# 2. Fiber2 Runs
# 3. Fiber1 finishes
# 4. Exit
puts "\n\nTransfer/Transfer"
fiber1 = Fiber.new {
puts "1. Fiber1 Runs"
fiber2 = Fiber.new {
puts "2. Fiber2 Runs"
fiber1.transfer
puts "Never: Fiber2 is killed"
}
fiber2.transfer
fiber2.kill
puts "3. Fiber1 finishes"
}
fiber1.transfer
puts "4. Exit"
# Transfer/Transfer
# 1. Fiber1 Runs
# 2. Fiber2 Runs
# 4. Exit
Is there a more appropriate way to terminate fiber2
in that case, while keeping control inside fiber1
, or is this a bug?
Updated by ioquatix (Samuel Williams) 4 months ago
Thanks for the great examples.
On the surface of it, it looks like a bug. I'll need to check the logic of the implementation to see if we can improve the behaviour.
Updated by ioquatix (Samuel Williams) 4 months ago
- Assignee set to ioquatix (Samuel Williams)
Updated by ioquatix (Samuel Williams) 4 months ago
- Related to Bug #20414: `Fiber#raise` should recurse to `resumed_fiber` rather than failing. added
Updated by ioquatix (Samuel Williams) 4 months ago
In the case of transfer, it may be possible to store the previous fiber, and on exiting a fiber, when no explicit transfer takes place, transfer back to the fiber that originally transferred into it. I think this is fairly compatible with existing code, but we'd need to survey the impact.