ReactiveCaching

このドキュメントはreactive_caching.rb を参照しています。

ReactiveCaching 関数は、バックグラウンドでデータを取得してRailsキャッシュに保存し、リクエストされている間だけ最新の状態に保つために使用されます。データがreactive_cache_lifetime の間リクエストされなかった場合、データの更新は停止され、削除されます。

使用例

class Foo < ApplicationRecord
  include ReactiveCaching

  after_save :clear_reactive_cache!

  def calculate_reactive_cache(param1, param2)
    # Expensive operation here. The return value of this method is cached
  end

  def result
    # Any arguments can be passed to `with_reactive_cache`. `calculate_reactive_cache`
    # will be called with the same arguments.
    with_reactive_cache(param1, param2) do |data|
      # ...
    end
  end
end

この例では、#result が最初に呼ばれたとき、nil を返します。しかし、#calculate_reactive_cache を呼び出すためにバックグラウンドワーカーをキューに入れ、 最初のキャッシュの有効期間を 10 分に設定しています。

どのように動作するか

最初に#with_reactive_cache が呼び出されると、バックグラウンド・ジョブがエンキューされ、with_reactive_cachenil を返します。バックグラウンド・ジョブは#calculate_reactive_cache を呼び出し、その戻り値を保存します。また、reactive_cache_refresh_intervalの後にバックグラウンド・ジョブを再エンキューします。したがって、保存された値は常に最新の状態に保たれます。計算が同時に実行されることはありません。

#with_reactive_cache 値がキャッシュされている間に #with_reactive_cache呼び出すと、キャッシュされた値が返されます。また、キャッシュの有効期間をreactive_cache_lifetime の値だけ延長します。

有効期間が過ぎると、バックグラウンド・ジョブはキューに入れられなくなり、#with_reactive_cache を再度呼び出すと、nil が返され、処理が最初からやり直されます。

ReactiveCachingにハードリミットを設定

パフォーマンスを維持するために、ReactiveCaching を含むクラスにハードキャッシュの制限を設定する必要があります。設定方法の例を参照してください。

詳細については、内部イシューRedis (または ReactiveCache) のソフトリミットとハードリミットをお読みください。

いつ

  • 外部APIへのリクエスト(例えばk8s APIへのリクエスト)が必要な場合。外部リクエストの間、アプリケーションサーバのWorkerをブロックし続けることは望ましくありません。
  • モデルが大量のデータベース呼び出しや時間のかかる計算を行う必要がある場合。

使用方法

モデルやインテグレーションで

ReactiveCaching 懸念は、モデルだけでなくインテグレーション (app/models/integrations) でも使用できます。

  1. モデルやインテグレーションにこのコンサーンを含めてください。

    懸念事項をモデルに含めるには

    include ReactiveCaching
    

    懸念をインテグレーションに含めるには:

    include Integrations::ReactivelyCached
    
  2. モデルまたはインテグレーションにcalculate_reactive_cache メソッドを実装します。
  3. キャッシュされた値が必要なモデルまたはインテグレーション内部でwith_reactive_cache を呼び出します。
  4. それに応じてreactive_cache_work_type を設定します

コントローラ

ReactiveCaching を使用するモデルやサービスメソッドをコールするコントローラのエンドポイントは、 バックグラウンドワーカーが完了するまで待つべきではありません。

  • ReactiveCaching を使用するモデルやサービスメソッドをコールする API は、キャッシュの計算中 (#with_reactive_cachenil を返すとき) に202 accepted を返すべきです。
  • また、Gitlab::PollingInterval.set_headerポーリング間隔ヘッダを設定する必要があります。
  • APIのコンシューマはAPIをポーリングすることが期待されます。
  • ポーリングによるサーバ負荷を軽減するために、ETagキャッシュの実装を検討することもできます。

モデルやサービスに実装するメソッド

ReactiveCaching を含むモデル/サービスで実装すべきメソッドです。

#calculate_reactive_cache (必須)

  • このメソッドは実装する必要があります。戻り値はキャッシュされます。
  • このメソッドはReactiveCaching によって呼び出されます。
  • with_reactive_cache に渡された引数はすべてcalculate_reactive_cache にも渡されます。

#reactive_cache_updated (オプション)

  • このメソッドは必要に応じて実装することができます。
  • このメソッドは、キャッシュが更新されるたびにReactiveCaching から呼び出されます。キャッシュが更新され、新しいキャッシュ値が古いキャッシュ値と同じ場合、このメソッドは呼び出されません。このメソッドは、新しい値がキャッシュに格納されている場合にのみ呼び出されます。
  • これは、キャッシュが更新されるたびにアクションを実行するために使用できます。

モデルもしくはサービスによって呼び出されるメソッド

これらはReactiveCaching によって提供されるメソッドで、モデル/サービスの内部で呼び出されるべきものです。

#with_reactive_cache (必須)

  • with_reactive_cache は、calculate_reactive_cache の結果が必要な場合に呼び出されなければなりません。
  • ブロックは、任意の数の引数を取ることもwith_reactive_cacheできます with_reactive_cachewith_reactive_cache に渡された引数はすべてcalculate_reactive_cache に渡されます。with_reactive_cache に渡された引数は、キャッシュ・キー名に付加されます。
  • 結果がすでにキャッシュされているときにwith_reactive_cache が呼び出された場合、ブロックが呼び出されてキャッシュされた値が返され、ブロックの返り値はwith_reactive_cache によって返されます。また、キャッシュのタイムアウトもreactive_cache_lifetime の値にリセットされます。
  • 結果がまだキャッシュされていない場合、with_reactive_cachenil を返します。また、calculate_reactive_cache を呼び出し、結果をキャッシュするバックグラウンド・ジョブもキューに入れます。
  • バックグラウンド・ジョブが完了し、結果がキャッシュされると、with_reactive_cache への次の呼び出しがキャッシュされた値をピックアップします。
  • 以下の例では、data がキャッシュされた値で、with_reactive_cache に与えられたブロックに渡されます。

     class Foo < ApplicationRecord
       include ReactiveCaching
       
       def calculate_reactive_cache(param1, param2)
         # Expensive operation here. The return value of this method is cached
       end
       
       def result
         with_reactive_cache(param1, param2) do |data|
           # ...
         end
       end
     end
    

#clear_reactive_cache! (オプション)

  • このメソッドはキャッシュを期限切れ/クリアする必要があるときに呼び出されます。例えば、モデルが修正された後にキャッシュがクリアされるように、モデルのafter_save コールバックで呼び出すことができます。
  • パラメータはキャッシュキーの一部なので、このメソッドはwith_reactive_cache に渡されるのと同じパラメータで呼び出されなければなりません。

#without_reactive_cache (オプション)

  • これは、デバッグの目的で使用できる便利なメソッドです。
  • このメソッドはバックグラウンドワーカーではなく、現在のプロセスでcalculate_reactive_cache を呼び出します。

設定可能なオプション

class_attribute 、調整可能なオプションがいくつかあります。

self.reactive_cache_key

  • この属性の値は、data およびalive キャッシュ・キー名のプレフィックスです。with_reactive_cache に渡されたパラメータが残りのキャッシュ・キー名となります。
  • デフォルトでは、このキーはモデル名とレコードの ID を使用します。

     self.reactive_cache_key = -> (record) { [model_name.singular, record.id] }
    
  • data キャッシュキーは"ExampleModel:1:arg1:arg2" で、alive キャッシュキーは"ExampleModel:1:arg1:arg2:alive"です。ExampleModel はモデルの名前、1 はレコードの ID、arg1arg2with_reactive_cacheに渡されるパラメータです。
  • この問題をインテグレーション (app/models/integrations/) に含める場合は、インテグレーションに以下を追加してデフォルトをオーバーライドする必要があります:

     self.reactive_cache_key = ->(integration) { [integration.class.model_name.singular, integration.project_id] }
    

    reactive_cache_key が上記とまったく同じであれば、代わりに既存のIntegrations::ReactivelyCached を使用することができます。

self.reactive_cache_lease_timeout

  • ReactiveCachingGitlab::ExclusiveLease を使って、キャッシュ計算が複数のワーカーによって同時に実行されないようにします。
  • この属性はGitlab::ExclusiveLeaseのタイムアウトです。
  • デフォルトは 2 分ですが、別のタイムアウトが必要な場合はオーバーライドできます。
self.reactive_cache_lease_timeout = 2.minutes

self.reactive_cache_refresh_interval

  • これはキャッシュが更新される間隔です。
  • デフォルトは1分です。
self.reactive_cache_refresh_interval = 1.minute

self.reactive_cache_lifetime

  • これはリクエストがなければキャッシュがクリアされるまでの時間です。
  • デフォルトは 10 分です。このキャッシュ値に対するリクエストが 10 分間ない場合、キャッシュは失効します。
  • 有効期限が切れる前にキャッシュ値が要求された場合、キャッシュのタイムアウトはreactive_cache_lifetime にリセットされます。
self.reactive_cache_lifetime = 10.minutes

self.reactive_cache_hard_limit

  • これはReactiveCaching がキャッシュを許可する最大データサイズです。
  • デフォルトは1メガバイトです。この値を超えるデータはキャッシュされず、Sentry上でReactiveCaching::ExceededReactiveCacheLimit
self.reactive_cache_hard_limit = 5.megabytes

self.reactive_cache_work_type

  • これはcalculate_reactive_cache メソッドで実行される作業の種類です。この属性に基づいて、キャッシュジョブを処理する適切なワーカーを選ぶことができます。外部からのリクエスト (Kubernetes や Sentry など) を処理する場合は、必ず:external_dependency に設定してください。そうでない場合は:no_dependency に設定してください。

self.reactive_cache_worker_finder

  • これは、calculate_reactive_cache が呼び出されるオブジェクトを見つける、または生成するためにバックグラウンドワーカーが使用するメソッドです。
  • デフォルトでは、オブジェクトを見つけるためにモデルの主キーを使用します:

     self.reactive_cache_worker_finder = ->(id, *_args) do
       find_by(primary_key => id)
     end
    
  • デフォルトの動作は、カスタムreactive_cache_worker_finder を定義することでオーバーライドできます。

     class Foo < ApplicationRecord
       include ReactiveCaching
       
       self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
       
       def self.from_cache(var1, var2)
         # This method will be called by the background worker with "bar1" and
         # "bar2" as arguments.
         new(var1, var2)
       end
       
       def initialize(var1, var2)
         # ...
       end
       
       def calculate_reactive_cache(var1, var2)
         # Expensive operation here. The return value of this method is cached
       end
       
       def result
         with_reactive_cache("bar1", "bar2") do |data|
           # ...
         end
       end
     end
    
    • この例では、主キーIDはwith_reactive_cache に渡されたパラメータとともにreactive_cache_worker_finder に渡されます。
    • カスタムreactive_cache_worker_finderwith_reactive_cache に渡されたパラメータで.from_cache を呼び出します。