BigW Consortium Gitlab

reactive_caching.rb 3.75 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
# The ReactiveCaching concern is used to fetch some data in the background and
# store it in the Rails cache, keeping it up-to-date for as long as it is being
# requested.  If the data hasn't been requested for +reactive_cache_lifetime+,
# it stop being refreshed, and then be removed.
#
# Example of use:
#
#    class Foo < ActiveRecord::Base
#      include ReactiveCaching
#
#      self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
#
#      after_save :clear_reactive_cache!
#
#      def calculate_reactive_cache
#        # Expensive operation here. The return value of this method is cached
#      end
#
#      def result
#        with_reactive_cache do |data|
#          # ...
#        end
#      end
#    end
#
# In this example, the first time `#result` is called, it will return `nil`.
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes.
#
# Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
# Calculations are never run concurrently.
#
# Calling `#result` while a value is in the cache will call the block given to
# `#with_reactive_cache`, yielding the cached value. It will also extend the
# lifetime by `reactive_cache_lifetime`.
#
# Once the lifetime has expired, no more background jobs will be enqueued and
# calling `#result` will again return `nil` - starting the process all over
# again
module ReactiveCaching
  extend ActiveSupport::Concern

  included do
    class_attribute :reactive_cache_lease_timeout

    class_attribute :reactive_cache_key
    class_attribute :reactive_cache_lifetime
    class_attribute :reactive_cache_refresh_interval

    # defaults
    self.reactive_cache_lease_timeout = 2.minutes

    self.reactive_cache_refresh_interval = 1.minute
    self.reactive_cache_lifetime = 10.minutes

58
    def calculate_reactive_cache(*args)
59 60 61
      raise NotImplementedError
    end

62 63 64
    def with_reactive_cache(*args, &blk)
      within_reactive_cache_lifetime(*args) do
        data = Rails.cache.read(full_reactive_cache_key(*args))
65 66 67
        yield data if data.present?
      end
    ensure
68 69
      Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
      ReactiveCachingWorker.perform_async(self.class, id, *args)
70 71
    end

72 73
    def clear_reactive_cache!(*args)
      Rails.cache.delete(full_reactive_cache_key(*args))
74 75
    end

76 77 78 79 80 81
    def exclusively_update_reactive_cache!(*args)
      locking_reactive_cache(*args) do
        within_reactive_cache_lifetime(*args) do
          enqueuing_update(*args) do
            value = calculate_reactive_cache(*args)
            Rails.cache.write(full_reactive_cache_key(*args), value)
82 83 84 85 86 87 88 89 90 91 92 93 94 95
          end
        end
      end
    end

    private

    def full_reactive_cache_key(*qualifiers)
      prefix = self.class.reactive_cache_key
      prefix = prefix.call(self) if prefix.respond_to?(:call)

      ([prefix].flatten + qualifiers).join(':')
    end

96 97 98 99 100 101
    def alive_reactive_cache_key(*qualifiers)
      full_reactive_cache_key(*(qualifiers + ['alive']))
    end

    def locking_reactive_cache(*args)
      lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout)
102 103 104
      uuid = lease.try_obtain
      yield if uuid
    ensure
105
      Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
106 107
    end

108 109
    def within_reactive_cache_lifetime(*args)
      yield if Rails.cache.read(alive_reactive_cache_key(*args))
110 111
    end

112
    def enqueuing_update(*args)
113 114
      yield
    ensure
115
      ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
116 117 118
    end
  end
end