Class: CachedCounter

Inherits:
Object
  • Object
show all
Defined in:
lib/cached_counter.rb,
lib/cached_counter/version.rb

Defined Under Namespace

Modules: CacheStore Classes: ConcurrentCacheWriteError, RetriedJob, RollbackByMethodCallListener

Constant Summary collapse

VERSION =
"0.0.1"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_class:, id:, attribute:, cache_store: nil) ⇒ CachedCounter

Returns a new instance of CachedCounter.



54
55
56
57
58
59
# File 'lib/cached_counter.rb', line 54

def initialize(model_class:, id:, attribute:, cache_store: nil)
  @model_class = model_class
  @attribute = attribute
  @id = id
  @cache_store = cache_store
end

Instance Attribute Details

#attributeObject (readonly)

Returns the value of attribute attribute.



4
5
6
# File 'lib/cached_counter.rb', line 4

def attribute
  @attribute
end

#idObject (readonly)

Returns the value of attribute id.



4
5
6
# File 'lib/cached_counter.rb', line 4

def id
  @id
end

#model_classObject (readonly)

Returns the value of attribute model_class.



4
5
6
# File 'lib/cached_counter.rb', line 4

def model_class
  @model_class
end

Class Method Details

.builder_for_cache_store(cache_store, *args, **options) ⇒ Object

Parameters:

  • (Proc)


29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/cached_counter.rb', line 29

def self.builder_for_cache_store(cache_store, *args, **options)
  case cache_store
  when Symbol
    klass = cache_store_class_for_symbol(cache_store)
    -> { klass.create(*args, **options) }
  when Class
    -> { cache_store.create(*args, **options)}
  when Proc
    cache_store
  else
    -> { cache_store }
  end
end

.cache_key(cache_key = nil) ⇒ #call

Parameters:

  • cache_key (#call) (defaults to: nil)

    The proc which takes the single argument of the class inheriting ActiveRecord::Base and returns a cache key

Returns:

  • (#call)


10
11
12
13
14
15
16
# File 'lib/cached_counter.rb', line 10

def self.cache_key(cache_key=nil)
  if cache_key
    @cache_key = cache_key
  else
    @cache_key ||= -> c { "#{c.model_class.model_name.cache_key}/#{c.attribute}/cached_counter/#{c.id}" }
  end
end

.cache_store(cache_store = nil, *args, **options) ⇒ CacheStore::Base

Parameters:

  • cache_store (#call) (defaults to: nil)

Returns:



20
21
22
23
24
25
26
# File 'lib/cached_counter.rb', line 20

def self.cache_store(cache_store=nil, *args, **options)
  if cache_store
    @cache_store = builder_for_cache_store(cache_store, *args, **options)
  else
    @cache_store
  end
end

.cache_store_class_for_symbol(symbol) ⇒ Object

Parameters:

  • symbol (Symbol)


44
45
46
# File 'lib/cached_counter.rb', line 44

def self.cache_store_class_for_symbol(symbol)
  const_get CacheStore.name + '::' + symbol.to_s.split('_').map(&:capitalize).join + 'CacheStore'
end

.create(record:, attribute:, cache_store: nil) ⇒ Object

Parameters:

  • record (ActiveRecord::Base)
  • attribute (Symbol)


50
51
52
# File 'lib/cached_counter.rb', line 50

def self.create(record:, attribute:, cache_store: nil)
  new(model_class: record.class, id: record.id, attribute: attribute, cache_store: cache_store)
end

Instance Method Details

#cache_keyString

Returns:

  • (String)


170
171
172
# File 'lib/cached_counter.rb', line 170

def cache_key
  self.class.cache_key.call(self)
end

#decrementObject

Decrement the specified attribute of the record utilizing the cache not to lock the table row as long as possible



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/cached_counter.rb', line 108

def decrement
  cache_updated_successfully =
    with_cache_store do |store|
      begin
        store.decr(cache_key) ||
          store.add(cache_key, value_in_db - 1) ||
          raise(ConcurrentCacheWriteError, "Failing not to enter a race condition while writing a value for the key #{cache_key}")
      rescue store.error_class => e
        false
      end
    end

  begin
    if cache_updated_successfully
      on_error_rollback_by(:increment_in_cache)

      decrement_in_db_later
    else
      decrement_in_db
    end
  rescue => e
    raise e
  end
end

#decrement_in_cacheObject



155
156
157
158
159
# File 'lib/cached_counter.rb', line 155

def decrement_in_cache
  with_cache_store do |d|
    d.decr(cache_key)
  end
end

#decrement_in_dbObject



161
162
163
# File 'lib/cached_counter.rb', line 161

def decrement_in_db
  @model_class.decrement_counter(@attribute, @id)
end

#decrement_in_db_laterObject



165
166
167
# File 'lib/cached_counter.rb', line 165

def decrement_in_db_later
  call_method_later(:decrement_in_db)
end

#incrementObject

Increment the specified attribute of the record utilizing the cache not to lock the table row as long as possible



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/cached_counter.rb', line 62

def increment
  cache_updated_successfully =
    with_cache_store do |store|
      begin
        store.incr(cache_key) ||
          # When the key doesn't exit in the cache because of cache expirations/clean-ups/restarts
          store.add(cache_key, value_in_db + 1) ||
          # In rare cases, the value for the key is updated by other client and we have to fail immediately, not to
          # run into race-conditions.
          raise(ConcurrentCacheWriteError, "Failing not to enter a race condition while writing a value for the key #{cache_key}")
      rescue store.error_class => e
        false
      end
    end

  begin
    if cache_updated_successfully
      # When this database transaction failed afterward, we have to rollback the incrementation by decrementing
      # the value in the cached.
      # Without the rollback, we'll fall into an inconsistent state between the database and the cache.
      on_error_rollback_by(:decrement_in_cache)

      # As we have successfully incremented the value in the cache, we can rely on the cache in order to
      # get/show the latest value.
      # Therefore, we have no need to update the database record in realtime and we can achieve
      # incrementing the counter with a little row-lock.
      increment_in_db_later
    else
      # The cache service seems to be down, but we don't want to stop the application service.
      # That's why we fall back to increment the value in the database which requires a bigger row-lock.
      increment_in_db
    end
  rescue => e
    raise e
  end
end

#increment_in_cacheObject



141
142
143
144
145
# File 'lib/cached_counter.rb', line 141

def increment_in_cache
  with_cache_store do |d|
    d.incr(cache_key)
  end
end

#increment_in_dbObject



147
148
149
# File 'lib/cached_counter.rb', line 147

def increment_in_db
  @model_class.increment_counter(@attribute, @id)
end

#increment_in_db_laterObject



151
152
153
# File 'lib/cached_counter.rb', line 151

def increment_in_db_later
  call_method_later(:increment_in_db)
end

#invalidate_cacheObject



137
138
139
# File 'lib/cached_counter.rb', line 137

def invalidate_cache
  cache_store.delete(cache_key)
end

#valueObject



99
100
101
102
103
104
105
# File 'lib/cached_counter.rb', line 99

def value
  begin
    cache_store.get(cache_key).try(&:to_i)
  rescue cache_store.error_class => e
    nil
  end || value_in_db
end

#value_in_dbObject



133
134
135
# File 'lib/cached_counter.rb', line 133

def value_in_db
  @model_class.find(@id).send(@attribute)
end