DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Brian Morearty
Brian Morearty

Posted on

Using RequestStore with asynchronous I/O in Rails apps

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β€”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 gem and the 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. 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 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, 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.

Top comments (1)

Collapse
 
woto profile image
Ruslan Kornev

Hmmm, don't know if it's usable in this case. But I though that request_store is not needed anymore, because we have ActiveSupport::CurrentAttributes.

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.