Fiber Scheduler
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.
Installation
gem install fiber_scheduler
Requires Ruby 3.1.
Highlights
- Enables asynchronous (colorless) programming in Ruby.
- Killer feature: full compatibility with any other Fiber Scheduler implementation, including the async gem.
- Not a framework: no DSL or new APIs. Can be used with just built-in Ruby methods: Fiber.set_scheduler and Fiber.schedule.
- ~500 LOC of pure Ruby, no C extensions.
- No dependencies.
Setup
- With a block (recommended)
- Set
Fiber.scheduler
directly
With a block (recommended)
FiberScheduler do
# Your code here, e.g. Fiber.schedule { ... }
end
Recommended because:
- This approach has full compatibility with any other Fiber Scheduler.
-
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:
- Uses only built-in Ruby methods
Fiber.set_scheduler
andFiber.schedule
.
Cons:
- No compatibility with other fiber schedulers.
Examples
Basic example
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
Advanced example
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
Scaling example
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.
Nested Fiber.schedule example
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
Waiting Fiber.schedule example
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 Fiber.schedule example
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 Fiber.schedule example
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
Compatibility with other fiber schedulers
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
Other Fiber Scheduler implementations
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.
Performance
This basic perf benchmark looks promising.
HINT: make sure to install io-event
gem alongside fiber_scheduler
for a
performance improvement.
Credits
Samuel Williams for:
- Implementing Ruby's Fiber Scheduler hooks.
- The default selector used in this gem.