Using RequestStore with asynchronous I/O in Rails apps
Tags: rails, ruby
Published: 2021-11-22 19:37:29

Did you know it’s possible to write Rails apps using non-blocking, asynchronous I/O, the way Node apps work? Your app handles each web request in a fiber. Whenever that fiber fires off a blocking operation like a network call, it can yield the current fiber and allow another fiber to use the CPU. In fact it’s more convenient than Node because your methods are [colorless](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)—there are no issues with synchronous methods calling async methods.

Writing a Rails app with fibers has two advantages over threads: 

1. No worrying about race conditions. This is a big deal.
2. More scalable. Fibers use much less memory than threads. You can create hundreds of thousands of fibers without problems.

You can use the [Async](https://github.com/socketry/async) gem and the [Falcon](https://github.com/socketry/falcon) web server to take advantage of this capability. And starting in Ruby 3.0, async I/O is even more automatic because inside the Ruby runtime, [all socket operations will automatically yield the current fiber by default](https://www.wjwh.eu/posts/2020-12-28-ruby-fiber-scheduler-c-extension.html). It’s fully transparent to the developer. Your I/O calls appear to be blocking so they are easy to understand, consistent with Ruby’s “programmer happiness” philosophy.

# Problem

As people start using this technique more and more, some problems will come up. One I’ve noticed is the inability to use the `request_store` gem. This is a gem that helps you make per-request “global” storage that doesn’t accidentally leak from one request to the next. For example you could use it to store information about the current user without having to pass it down to every method but without worrying that it will leak into the next request. Globals aren’t exactly the best thing ever, but sometimes they’re practical and you do what you gotta do.

`request_store` was written to handle web servers that handle each request in its own thread so it stores the per-request data in thread-local storage in `Thread.current[:request_store]`. But the problem is, despite its name, `Thread.current` is actually _fiber-local_, not thread-local. So your request handler could store the current user info in the request store, then fire off a new fiber to make an I/O call concurrently, and that fiber won’t have access to the request store.

# Solution

To fix this I wrote a new [`request_store-fibers`](https://github.com/BMorearty/request_store-fibers) gem that detects whenever a new fiber is created by hooking into `Fiber.new`. Before the fiber is executed, I copy the current request_store data into a variable. Then as soon as the fiber is resumed the very first time, I copy that data into the fiber's request_store.

Presto! Thanks to Ruby’s ability to hook into core APIs, now you can use `request_store` with a Rails app that has fibers.

`request_store-fibers` is actually a thin layer on top of another more generic gem I wrote, [`fiber_hook`](https://github.com/BMorearty/fiber_hook), which does all the magic. It lets you hook into fiber creation and do anything you want right after any fiber is created and before it executes.