@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Rails 7のload_async

Rails 7の新機能を見ていて次の機能があることに気がつきました。

Ruby on Rails 7の主要な新機能・機能追加・変更点 - Qiita

ActiveRecord::Relation#load_asyncを使用することによって、非同期でSQLクエリを実行し、結果を取得することができるようになりました。

これによって、例えば以下のコードのようにコントローラの同じアクション内で、複数の相互に依存しないクエリを並列に実行することができるようになり、レスポンスを返す時間を短縮することができるようになります。

def index
  @articles = Article.per(params[:per]).page(params[:page]).load_async
  @categories = Category.active.load_async
end

非同期化する方法や、load_asyncというメソッドを呼ぶだけで非同期APIの待ち合わせを実現してる方法が気になります。 Implement Relation#load_async to schedule the query on the background thread pool by casperisfine · Pull Request #41372 · rails/rails · GitHub を見てみます。

@async_executorという変数があります。 RailsConcurrent Rubyを使っています。 executorと言うの名前からすると、 Class: Concurrent::ThreadPoolExecutor — Concurrent Ruby か、近いものを使っていそうです。 並列化するためにスレッドを使っていそうです。 また並列数に上限を持たせるためにスレッドプールを使っていることも予想できます。 Railsのコネクションプールはスレッドに依存しているので、この辺どうなってんだ?というのはさらなる疑問です*1

次にActiveRecord::FutureResultクラスの存在に気がつきます。 なるほど Future パターン - Wikipedia です。 次のように動きそうです。

  1. load_asyncメソッドを呼び出した瞬間に別スレッドでSQLクエリを発行する
  2. ActiveRecord::FutureResultを返す
  3. クエリの結果を参照するとき(主にビューをレンダリングするとき)に
    1. SQLクエリが完了していれば結果を使う
    2. 完了していなければ完了するまで待つ

3-2で待つところで待ち合わせしています。 3で結果を待ったり、すでにある結果を使ったりする仕組みがFutureです。 JavaScriptの世界ではPromiseと呼ばれているので、あんな感じの奴です*2

最初に次のサンプルコードを見たとき

def index
  @articles = Article.per(params[:per]).page(params[:page]).load_async
  @categories = Category.active.load_async
end

疑問を持ったのは、このサンプルコードの範囲で待ち合わせしていると考えたからです。 実際には待ち合わせを実行しているのは、このあビューをレンダリングするために値を参照するときでした。

APIはシンプルですけど、やってることはなかなかややこしいですねー。

参考

*1:https://pawelurbanek.com/rails-load-async によると異なるコネクションを使うそうです。まあ、言われてみればそうですよね。そのためload_asyncをつかうときはコネクションプールの枯渇の考慮しなければいけないことも説明されています。

*2:JavaScriptのPromiseはスレッドを考慮していないので、あれをイメージするとそれはそれで混乱する気はします。