2 minutes
Async Concurrency with Fiber Schedulers in Ruby
Introduction
Ruby 3.x introduced a Fiber Scheduler API that modernizes async I/O by letting you hook into Fibers, lightweight concurrency primitives, to perform non-blocking operations. This means you can ditch older event-driven gems and write clean, synchronous-looking code that under the hood uses async I/O.
In this post we’ll cover:
- How the Fiber Scheduler API works
- Replacing EventMachine and Celluloid
- Writing non-blocking DNS, HTTP, and DB adapters
- Integrating with the
async
gem ecosystem - Real-world: building a mini chat server
The Fiber Scheduler API
A Fiber Scheduler intercepts blocking operations (like IO.read
) and delegates them to non-blocking implementations. You register a scheduler globally:
class MyScheduler < Fiber::Scheduler
def io_read(io, buffer, size, offset)
# use non-blocking read under the hood
selector = IO::Selector.new
selector.register(io, :read)
selector.select(1) # wait until readable
io.read_nonblock(size, buffer)
end
# define io_write, etc.
end
Fiber.set_scheduler(MyScheduler.new)
# Now any IO operations run inside Fibers will use your scheduler
Ditching EventMachine and Celluloid
Before Fiber schedulers, popular async libs were EventMachine (callback-heavy) and Celluloid (actor model). With the scheduler API, you get:
- Linear code flow instead of nested callbacks
- Built-in integration without external runtimes
- Better performance since it’s part of the core
Building Non-blocking Adapters
You can wrap existing sync libraries by delegating to their non-blocking counterparts. For example, a simple HTTP GET:
require 'async/http/client'
require 'async/io/ssl_socket'
Async do
uri = URI("https://example.com")
client = Async::HTTP::Client.new(
Async::HTTP::Endpoint.parse(uri)
)
response = client.get(uri.path)
puts response.read
end
Or roll your own with Socket#read_nonblock
and your scheduler.
Integrating with async
Gems
The async
gem ecosystem provides high-level building blocks:
async-io
for filesystem and socketsasync-http
for HTTP/2 and streamingasync-postgres
for DB connections
They all run on your scheduler automatically inside Async do ... end
blocks.
Real-world: Mini Chat Server
require 'async'
require 'async/io'
Async do
server = Async::IO::Socket.tcp('0.0.0.0', 3000)
puts "Chat server running on port 3000"
server.each do |client|
Async do
client.write "Welcome to async chat!\n"
loop do
message = client.readpartial(1024)
broadcast_to_all(message)
end
rescue EOFError
client.close
end
end
end
This code looks sync but is fully non-blocking.
Conclusion
The Fiber Scheduler API in Ruby 3.x makes async I/O feel natural and integrates directly with Ruby’s Fibers. By leveraging the async
gems or writing custom schedulers, you can build high-performance services without callback hell. Give it a spin in your next project!