2018-03-04 10:09:32 -05:00
|
|
|
require_relative '../../spec_helper'
|
|
|
|
require_relative '../../shared/fiber/resume'
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
require 'fiber'
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
describe "Fiber#transfer" do
|
|
|
|
it_behaves_like :fiber_resume, :transfer
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "Fiber#transfer" do
|
|
|
|
it "transfers control from one Fiber to another when called from a Fiber" do
|
|
|
|
fiber1 = Fiber.new { :fiber1 }
|
|
|
|
fiber2 = Fiber.new { fiber1.transfer; :fiber2 }
|
relax Fiber#transfer's restriction
Using Fiber#transfer with Fiber#resume for a same Fiber is
limited (once Fiber#transfer is called for a fiber, the fiber
can not be resumed more). This restriction was introduced to
protect the resume/yield chain, but we realized that it is too much
to protect the chain. Instead of the current restriction, we
introduce some other protections.
(1) can not transfer to the resuming fiber.
(2) can not transfer to the yielding fiber.
(3) can not resume transferred fiber.
(4) can not yield from not-resumed fiber.
[Bug #17221]
Also at the end of a transferred fiber, it had continued on root fiber.
However, if the root fiber resumed a fiber (and that fiber can resumed
another fiber), this behavior also breaks the resume/yield chain.
So at the end of a transferred fiber, switch to the edge of resume
chain from root fiber.
For example, root fiber resumed f1 and f1 resumed f2, transferred to
f3 and f3 terminated, then continue from the fiber f2 (it was continued
from root fiber without this patch).
2020-10-07 12:15:32 -04:00
|
|
|
|
|
|
|
ruby_version_is '' ... '3.0' do
|
|
|
|
fiber2.resume.should == :fiber1
|
|
|
|
end
|
|
|
|
ruby_version_is '3.0' do
|
|
|
|
fiber2.resume.should == :fiber2
|
|
|
|
end
|
2017-05-07 08:04:49 -04:00
|
|
|
end
|
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
it "returns to the root Fiber when finished" do
|
|
|
|
f1 = Fiber.new { :fiber_1 }
|
|
|
|
f2 = Fiber.new { f1.transfer; :fiber_2 }
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
f2.transfer.should == :fiber_1
|
|
|
|
f2.transfer.should == :fiber_2
|
|
|
|
end
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
it "can be invoked from the same Fiber it transfers control to" do
|
|
|
|
states = []
|
|
|
|
fiber = Fiber.new { states << :start; fiber.transfer; states << :end }
|
|
|
|
fiber.transfer
|
|
|
|
states.should == [:start, :end]
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
states = []
|
|
|
|
fiber = Fiber.new { states << :start; fiber.transfer; states << :end }
|
|
|
|
fiber.resume
|
|
|
|
states.should == [:start, :end]
|
|
|
|
end
|
2017-05-07 08:04:49 -04:00
|
|
|
|
relax Fiber#transfer's restriction
Using Fiber#transfer with Fiber#resume for a same Fiber is
limited (once Fiber#transfer is called for a fiber, the fiber
can not be resumed more). This restriction was introduced to
protect the resume/yield chain, but we realized that it is too much
to protect the chain. Instead of the current restriction, we
introduce some other protections.
(1) can not transfer to the resuming fiber.
(2) can not transfer to the yielding fiber.
(3) can not resume transferred fiber.
(4) can not yield from not-resumed fiber.
[Bug #17221]
Also at the end of a transferred fiber, it had continued on root fiber.
However, if the root fiber resumed a fiber (and that fiber can resumed
another fiber), this behavior also breaks the resume/yield chain.
So at the end of a transferred fiber, switch to the edge of resume
chain from root fiber.
For example, root fiber resumed f1 and f1 resumed f2, transferred to
f3 and f3 terminated, then continue from the fiber f2 (it was continued
from root fiber without this patch).
2020-10-07 12:15:32 -04:00
|
|
|
ruby_version_is '' ... '3.0' do
|
|
|
|
it "can transfer control to a Fiber that has transferred to another Fiber" do
|
|
|
|
states = []
|
|
|
|
fiber1 = Fiber.new { states << :fiber1 }
|
|
|
|
fiber2 = Fiber.new { states << :fiber2_start; fiber1.transfer; states << :fiber2_end}
|
|
|
|
fiber2.resume.should == [:fiber2_start, :fiber1]
|
|
|
|
fiber2.transfer.should == [:fiber2_start, :fiber1, :fiber2_end]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
ruby_version_is '3.0' do
|
|
|
|
it "can not transfer control to a Fiber that has suspended by Fiber.yield" do
|
|
|
|
states = []
|
|
|
|
fiber1 = Fiber.new { states << :fiber1 }
|
|
|
|
fiber2 = Fiber.new { states << :fiber2_start; Fiber.yield fiber1.transfer; states << :fiber2_end}
|
|
|
|
fiber2.resume.should == [:fiber2_start, :fiber1]
|
|
|
|
-> { fiber2.transfer }.should raise_error(FiberError)
|
|
|
|
end
|
2019-04-28 17:20:11 -04:00
|
|
|
end
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-10-24 02:03:26 -04:00
|
|
|
it "raises a FiberError when transferring to a Fiber which resumes itself" do
|
|
|
|
fiber = Fiber.new { fiber.resume }
|
|
|
|
-> { fiber.transfer }.should raise_error(FiberError)
|
2019-04-28 17:20:11 -04:00
|
|
|
end
|
2017-05-07 08:04:49 -04:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
it "works if Fibers in different Threads each transfer to a Fiber in the same Thread" do
|
|
|
|
# This catches a bug where Fibers are running on a thread-pool
|
|
|
|
# and Fibers from a different Ruby Thread reuse the same native thread.
|
|
|
|
# Caching the Ruby Thread based on the native thread is not correct in that case,
|
|
|
|
# and the check for "fiber called across threads" in Fiber#transfer
|
|
|
|
# might be incorrect based on that.
|
|
|
|
2.times do
|
|
|
|
Thread.new do
|
|
|
|
io_fiber = Fiber.new do |calling_fiber|
|
|
|
|
calling_fiber.transfer
|
|
|
|
end
|
|
|
|
io_fiber.transfer(Fiber.current)
|
|
|
|
value = Object.new
|
|
|
|
io_fiber.transfer(value).should equal value
|
|
|
|
end.join
|
2017-05-07 08:04:49 -04:00
|
|
|
end
|
2019-04-28 17:20:11 -04:00
|
|
|
end
|
2018-01-29 11:08:16 -05:00
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
it "transfers control between a non-main thread's root fiber to a child fiber and back again" do
|
|
|
|
states = []
|
|
|
|
thread = Thread.new do
|
|
|
|
f1 = Fiber.new do |f0|
|
|
|
|
states << 0
|
|
|
|
value2 = f0.transfer(1)
|
|
|
|
states << value2
|
|
|
|
3
|
2018-01-29 11:08:16 -05:00
|
|
|
end
|
|
|
|
|
2019-04-28 17:20:11 -04:00
|
|
|
value1 = f1.transfer(Fiber.current)
|
|
|
|
states << value1
|
|
|
|
value3 = f1.transfer(2)
|
|
|
|
states << value3
|
2018-01-29 11:08:16 -05:00
|
|
|
end
|
2019-04-28 17:20:11 -04:00
|
|
|
thread.join
|
|
|
|
states.should == [0, 1, 2, 3]
|
2017-05-07 08:04:49 -04:00
|
|
|
end
|
relax Fiber#transfer's restriction
Using Fiber#transfer with Fiber#resume for a same Fiber is
limited (once Fiber#transfer is called for a fiber, the fiber
can not be resumed more). This restriction was introduced to
protect the resume/yield chain, but we realized that it is too much
to protect the chain. Instead of the current restriction, we
introduce some other protections.
(1) can not transfer to the resuming fiber.
(2) can not transfer to the yielding fiber.
(3) can not resume transferred fiber.
(4) can not yield from not-resumed fiber.
[Bug #17221]
Also at the end of a transferred fiber, it had continued on root fiber.
However, if the root fiber resumed a fiber (and that fiber can resumed
another fiber), this behavior also breaks the resume/yield chain.
So at the end of a transferred fiber, switch to the edge of resume
chain from root fiber.
For example, root fiber resumed f1 and f1 resumed f2, transferred to
f3 and f3 terminated, then continue from the fiber f2 (it was continued
from root fiber without this patch).
2020-10-07 12:15:32 -04:00
|
|
|
|
|
|
|
ruby_version_is "" ... "3.0" do
|
|
|
|
it "runs until Fiber.yield" do
|
|
|
|
obj = mock('obj')
|
|
|
|
obj.should_not_receive(:do)
|
|
|
|
fiber = Fiber.new { 1 + 2; Fiber.yield; obj.do }
|
|
|
|
fiber.transfer
|
|
|
|
end
|
|
|
|
|
|
|
|
it "resumes from the last call to Fiber.yield on subsequent invocations" do
|
|
|
|
fiber = Fiber.new { Fiber.yield :first; :second }
|
|
|
|
fiber.transfer.should == :first
|
|
|
|
fiber.transfer.should == :second
|
|
|
|
end
|
|
|
|
|
|
|
|
it "sets the block parameters to its arguments on the first invocation" do
|
|
|
|
first = mock('first')
|
|
|
|
first.should_receive(:arg).with(:first).twice
|
|
|
|
|
|
|
|
fiber = Fiber.new { |arg| first.arg arg; Fiber.yield; first.arg arg; }
|
|
|
|
fiber.transfer :first
|
|
|
|
fiber.transfer :second
|
|
|
|
end
|
|
|
|
end
|
2017-05-07 08:04:49 -04:00
|
|
|
end
|