Ruby's missing Fiber Scheduler implementation.
Ruby 3 has
Fiber Scheduler hooks
that enable asynchronous programming. In order to make this work you need a
Fiber Scheduler implementation, but Ruby does not provide a default one.
This gem aims to fill that void by providing a Fiber Scheduler class that makes
a great default. It’s easy to use, performant, and can be used with
just built-in Ruby methods.
fiber_scheduler
’s killer feature 💣 is full compatibility with any other
Fiber Scheduler implementation, including the
async gem. Write code using
fiber_scheduler
and it works seamlessly with async
, bsync
or whatever
other _sync
gem comes in the future.
Learn more about the Ruby’s
Fiber Scheduler feature.
gem install fiber_scheduler
Requires Ruby 3.1.
Fiber.scheduler
directlyWith a block (recommended)
FiberScheduler do
# Your code here, e.g. Fiber.schedule { ... }
end
Recommended because:
Fiber.scheduler
is automatically un-set outside the block.Set Fiber.scheduler directly
Fiber.set_scheduler(FiberScheduler.new)
# Your code here, e.g. Fiber.schedule { ... }
Fiber.scheduler
is set until the end of the current thread, unless manually
unset with Fiber.set_scheduler(nil)
.
Pros:
Fiber.set_scheduler
and Fiber.schedule
.Cons:
This example runs two HTTP requests in parallel:
require "fiber_scheduler"
require "open-uri"
FiberScheduler do
Fiber.schedule do
URI.open("https://httpbin.org/delay/2")
end
Fiber.schedule do
URI.open("https://httpbin.org/delay/2")
end
end
This example runs various operations in parallel. The example total running
time is slightly more than 2 seconds, which indicates all the operations ran in
parallel.
Note that all the operations used in Fiber.schedule
blocks below are either
common gems or built-in Ruby methods. They all work asynchronously with this
library, no monkey patching!
require "fiber_scheduler"
require "httparty"
require "open-uri"
require "redis"
require "sequel"
DB = Sequel.postgres
Sequel.extension(:fiber_concurrency)
FiberScheduler do
Fiber.schedule do
# This HTTP request takes 2 seconds (slightly more because of the latency)
URI.open("https://httpbin.org/delay/2")
end
Fiber.schedule do
# Use any HTTP library
HTTParty.get("https://httpbin.org/delay/2")
end
Fiber.schedule do
# Works with any TCP protocol library
Redis.new.blpop("abc123", 2)
end
Fiber.schedule do
# Make database queries
DB.run("SELECT pg_sleep(2)")
end
Fiber.schedule do
sleep 2
end
Fiber.schedule do
# Run system commands
`sleep 2`
end
end
Easily run thousands and thousands of blocking operations in parallel. This
program finishes in about 2.5 seconds.
require "fiber_scheduler"
FiberScheduler do
10_000.times do
Fiber.schedule do
sleep 2
end
end
end
Gotcha: be careful about the overheads when scaling things. The below snippet
runs sleep
which is an “inexpensive” operation. But, if we were to run
thousands of network requests there would be more overhead (establishing
TCP connections, SSL handshakes etc) which would prolong program running time.
It’s possible to nest Fiber.schedule
blocks arbitrarily deep.
All the sleep
operations in this snippet run in parallel and the program
finishes in 2 seconds total.
require "fiber_scheduler"
FiberScheduler do
Fiber.schedule do
Fiber.schedule do
sleep 2
end
Fiber.schedule do
sleep 2
end
sleep 2
end
Fiber.schedule do
sleep 2
end
end
Sometimes it’s conventient for the parent to wait on the child fiber to
complete. Use Fiber.schedule(:waiting)
to achieve that.
In the below example fiber labeled parent
will wait for the child
fiber to
complete. Note that only the parent
fiber waits, other fibers run as usual.
This example takes 4 seconds to finish.
require "fiber_scheduler"
FiberScheduler do
Fiber.schedule do # parent
Fiber.schedule(:waiting) do # child
sleep 2
end
# The fiber stops here until the waiting child fiber completes.
sleep 2
end
Fiber.schedule do
sleep 2
end
end
Blocking fibers “block” all the other fibers from running until they’re
finished.
This example takes 4 seconds to finish.
require "fiber_scheduler"
FiberScheduler do
Fiber.schedule do
Fiber.schedule(:blocking) do
sleep 2
end
end
Fiber.schedule do
sleep 2
end
end
Volatile fibers end when all the “durable” fibers finish.
Volatile fibers (by design) may not complete all their work.
This is useful if you have a neverending task that performs some
cleanup work that should finish when the rest of the program completes.
This example takes 2 seconds to finish.
require "fiber_scheduler"
FiberScheduler do
Fiber.schedule(:volatile) do
# This fiber will live for only 2 seconds.
loop do
cleanup_work # this method will run only once
sleep 10
end
end
Fiber.schedule do
sleep 2
end
end
async
is an awesome asynchronous programming library, if not a framework.
If async
is like Rails, then fiber_scheduler
is plain Ruby.
fiber_scheduler
is fully compatible with async
:
Async do |task|
task.async do
# code ...
end
FiberScheduler do
Fiber.schedule do
# code ...
end
end
# ...
end
Note that currently the opposite doesn’t work:
FiberScheduler do
Async do
# ...
end
Fiber.schedule do # No scheduler is available! (RuntimeError)
# ...
end
end
fiber_scheduler
gem works with any other Fiber Scheduler class (current and
future ones). Example:
Fiber.set_scheduler(AnotherScheduler.new)
# stuff
FiberScheduler do
# works just fine
end
# more stuff
fiber_scheduler
is like choosing pure Ruby: it’s a safe choice because you
know it works and will continue working with everything else in Ruby’s
asynchronous eco-system.
This basic perf benchmark looks promising.
HINT: make sure to install io-event
gem alongside fiber_scheduler
for a
performance improvement.
Samuel Williams for: