ReactiveCaching

ReactiveCaching 관심사는 일부 데이터를 백그라운드에서 가져와 Rails 캐시에 저장하고, 이 데이터가 요청되는 동안 최신 상태를 유지하는 데 사용됩니다. reactive_cache_lifetime 동안 데이터가 요청되지 않으면, 갱신이 중지되고 제거됩니다.

예제

class Foo < ApplicationRecord
  include ReactiveCaching

  after_save :clear_reactive_cache!

  def calculate_reactive_cache(param1, param2)
    # 여기에서 비싼 연산 수행. 이 메서드의 반환 값은 캐시됩니다.
  end

  def result
    # `with_reactive_cache`에 전달할 수 있는 인수는 아무거나 가능합니다. `calculate_reactive_cache`
    # 는 동일한 인수로 호출됩니다.
    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를 호출하면 지정된 블록이 호출되어 캐시된 값이 반환됩니다. 또한, reactive_cache_lifetime 값만큼 캐시의 유효 기간이 연장됩니다.

유효 기간이 만료된 후에는 더 이상 백그라운드 작업이 큐에 추가되지 않으며, #with_reactive_cache를 다시 호출하면 nil이 반환되어 모든 프로세스가 다시 시작됩니다.

ReactiveCaching에 대한 하드 제한 설정

성능을 보존하기 위해 ReactiveCaching을 포함하는 클래스에 하드 캐시 제한을 설정해야 합니다. 설정하는 방법의 예를 참조하세요.

더 많은 정보를 원하시면 내부 이슈 Redis (또는 ReactiveCache) 소프트 및 하드 제한을 읽어보세요.

사용 시기

  • 외부 API에 요청해야 할 경우 (예: k8s API에 대한 요청). 외부 요청이 진행되는 동안 애플리케이션 서버 작업자를 차단하는 것은 바람직하지 않습니다.

  • 모델이 많은 데이터베이스 호출이나 다른 시간 소모적인 계산을 수행해야 할 경우.

사용하는 방법

모델 및 통합에서

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 (필수)

  • calculate_reactive_cache의 결과가 필요한 곳에서
    with_reactive_cache를 호출해야 합니다.

  • 블록을 with_reactive_cache에 전달할 수 있습니다. with_reactive_cache
    임의의 수의 인수를 취할 수 있습니다. with_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 호출에서 캐시된 값을 가져옵니다.

  • 아래의 예에서 datawith_reactive_cache에 전달된 블록에
    전달되는 캐시된 값입니다.

    class Foo < ApplicationRecord
      include ReactiveCaching
    
      def calculate_reactive_cache(param1, param2)
        # 비용이 많이 드는 작업입니다. 이 메서드의 반환 값은 캐시됩니다.
      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

  • 이 속성의 값은 dataalive 캐시 키 이름의 접두사입니다. 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)
        # 이 메서드는 백그라운드 작업자에 의해 "bar1"과
        # "bar2"를 인수로 호출됩니다.
        new(var1, var2)
      end
    
      def initialize(var1, var2)
        # ...
      end
    
      def calculate_reactive_cache(var1, var2)
        # 비용이 많이 드는 작업이 여기에 있습니다. 이 메서드의 반환 값이 캐시됩니다.
      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를 호출합니다.

캐시 키 변경

반응형 캐싱이 작동하는 방식 때문에, calculate_reactive_cache 메서드의 매개변수를 변경하는 것은 Sidekiq 작업자의 매개변수를 변경하는 것과 같습니다.

따라서 동일한 규칙을 준수해야 합니다.

예를 들어, calculate_reactive_cache 메서드에 새로운 매개변수가 추가되는 경우;

  1. 기본값이 있는 인수를 calculate_reactive_cache 메서드에 추가합니다 (Release M).

  2. 새로운 인수를 모든 with_reactive_cache 메서드 호출에 추가합니다 (Release M+1).

  3. 기본값을 제거합니다 (Release M+2).