Rails 7 adds the ability to schedule the query on the background thread pool


Often there need to be multiple database queries executed in the controller like,

@genres = Genre.all
@videos = Video.order(published_at: :desc)

These queries get executed sequentially which in most cases isn’t a big deal. However, as our database grows in size, response times get longer and longer. A large part of the query time is often just I/O waits. Assuming that each query takes 50ms to finish, these two statements would take 100ms in total. However, if executed in parallel, the I/O waits can be reutilized to perform the next execution.

Before

A naive approach to this would be to push the first query to a background thread and then perform the second one. Finally, we would wait for the first query to return the result and then go on to perform whatever else is needed.

This is messy.

genres_future = Thread.new { Genre.all.to_s }
@videos = Video.order(published_at: :desc).to_a
@genres = genres_future.value

But then we are executing a lot of user code in a background thread which breaks lots of expectations such as CurrentAttributes or ActiveSupport::Instrumentation.

If we were to manage this background processing on our own, we’ll have to modify how Rails handles thread safety at the controller level. Clearly, this hack requires tedious effort from developers to ensure the query is thread safe. Long story short, this won’t work on most realistic scenarios!

After

Rails 7 introduces a method load_async to schedule the query to be performed asynchronously from a thread pool. If the result is accessed before a background thread had the opportunity to perform the query, it will be performed in the foreground.

This is useful for queries that can be performed long enough before their result will be needed, or for controllers which need to perform several independent queries.

@genres = Genre.all.load_async
@videos = Video.order(published_at: :desc).load_async

It performs the query in a thread pool, while the rest of the work (instantiating models, etc) is still done on the main thread. That takes care of the vast majority of the per thread/fiber context problems.

Using both in context considerably reduces the risks involved in being thread safe.

For more discussion related to this change please refer to this pull request.