Module: CounterAttribute
- Extended by:
- ActiveSupport::Concern, AfterCommitQueue
- Includes:
- Gitlab::ExclusiveLeaseHelpers
- Defined in:
- app/models/concerns/counter_attribute.rb
Overview
Add capabilities to increment a numeric model attribute efficiently by using Redis and flushing the increments asynchronously to the database after a period of time (10 minutes). When an attribute is incremented by a value, the increment is added to a Redis key. Then, FlushCounterIncrementsWorker will execute `flush_increments_to_database!` which removes increments from Redis for a given model attribute and updates the values in the database.
@example:
class ProjectStatistics
include CounterAttribute
counter_attribute :commit_count
counter_attribute :storage_size
end
To increment the counter we can use the method:
delayed_increment_counter(:commit_count, 3)
Constant Summary collapse
- LUA_STEAL_INCREMENT_SCRIPT =
<<~EOS.freeze local increment_key, flushed_key = KEYS[1], KEYS[2] local increment_value = redis.call("get", increment_key) or 0 local flushed_value = redis.call("incrby", flushed_key, increment_value) if flushed_value == 0 then redis.call("del", increment_key, flushed_key) else redis.call("del", increment_key) end return flushed_value EOS
- WORKER_DELAY =
10.minutes
- WORKER_LOCK_TTL =
10.minutes
Constants included from Gitlab::ExclusiveLeaseHelpers
Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
Instance Method Summary collapse
- #counter_flushed_key(attribute) ⇒ Object
- #counter_key(attribute) ⇒ Object
- #counter_lock_key(attribute) ⇒ Object
- #delayed_increment_counter(attribute, increment) ⇒ Object
-
#flush_increments_to_database!(attribute) ⇒ Object
This method must only be called by FlushCounterIncrementsWorker because it should run asynchronously and with exclusive lease.
Methods included from AfterCommitQueue
run_after_commit, run_after_commit_or_now
Methods included from Gitlab::ExclusiveLeaseHelpers
Instance Method Details
#counter_flushed_key(attribute) ⇒ Object
103 104 105 |
# File 'app/models/concerns/counter_attribute.rb', line 103 def counter_flushed_key(attribute) counter_key(attribute) + ':flushed' end |
#counter_key(attribute) ⇒ Object
99 100 101 |
# File 'app/models/concerns/counter_attribute.rb', line 99 def counter_key(attribute) "project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}" end |
#counter_lock_key(attribute) ⇒ Object
107 108 109 |
# File 'app/models/concerns/counter_attribute.rb', line 107 def counter_lock_key(attribute) counter_key(attribute) + ':lock' end |
#delayed_increment_counter(attribute, increment) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'app/models/concerns/counter_attribute.rb', line 81 def delayed_increment_counter(attribute, increment) return if increment == 0 run_after_commit_or_now do if counter_attribute_enabled?(attribute) redis_state do |redis| redis.incrby(counter_key(attribute), increment) end FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute) else legacy_increment!(attribute, increment) end end true end |
#flush_increments_to_database!(attribute) ⇒ Object
This method must only be called by FlushCounterIncrementsWorker because it should run asynchronously and with exclusive lease. This will
1. temporarily move the pending increment for a given attribute
to a relative "flushed" Redis key, delete the increment key and return
the value. If new increments are performed at this point, the increment
key is recreated as part of `delayed_increment_counter`.
The "flushed" key is used to ensure that we can keep incrementing
counters in Redis while flushing existing values.
2. then the value is used to update the counter in the database.
3. finally the "flushed" key is deleted.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'app/models/concerns/counter_attribute.rb', line 64 def flush_increments_to_database!(attribute) lock_key = counter_lock_key(attribute) with_exclusive_lease(lock_key) do increment_key = counter_key(attribute) flushed_key = counter_flushed_key(attribute) increment_value = steal_increments(increment_key, flushed_key) next if increment_value == 0 transaction do unsafe_update_counters(id, attribute => increment_value) redis_state { |redis| redis.del(flushed_key) } end end end |