Class: Cymometer::Counter
- Inherits:
-
Object
- Object
- Cymometer::Counter
- Defined in:
- lib/cymometer/counter.rb
Defined Under Namespace
Classes: LimitExceeded
Constant Summary collapse
- DEFAULT_WINDOW =
Default window is 1 hour
3600- INCREMENT_LUA_SCRIPT =
<<-LUA.freeze local key = KEYS[1] local current_time = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) -- Remove entries older than the window size redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window) -- Get the current count local count = redis.call('ZCOUNT', key, current_time - window, '+inf') if count >= limit then -- Limit exceeded, do not increment return {0, count} else -- Increment the counter redis.call('ZADD', key, current_time, current_time) redis.call('EXPIRE', key, math.floor(window / 1000000)) return {1, count + 1} end LUA
- DECREMENT_LUA_SCRIPT =
<<-LUA.freeze local key = KEYS[1] local current_time = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) -- Remove entries older than the window size redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window) -- Attempt to remove the entry with the lowest score (oldest entry) local entries = redis.call('ZRANGEBYSCORE', key, current_time - window, '+inf', 'LIMIT', 0, 1) if next(entries) == nil then -- No entries to remove, safe no-op local count = 0 return {1, count} else local member = entries[1] redis.call('ZREM', key, member) local count = redis.call('ZCOUNT', key, current_time - window, '+inf') return {1, count} end LUA
Class Attribute Summary collapse
-
.decrement_sha ⇒ Object
readonly
Returns the value of attribute decrement_sha.
-
.increment_sha ⇒ Object
readonly
Returns the value of attribute increment_sha.
Instance Attribute Summary collapse
-
#key ⇒ Object
readonly
Returns the value of attribute key.
-
#limit ⇒ Object
readonly
Returns the value of attribute limit.
-
#window ⇒ Object
readonly
Returns the value of attribute window.
Instance Method Summary collapse
-
#count ⇒ Object
Returns the current count.
-
#decrement! ⇒ Object
Atomically decrements the counter by removing the oldest entry.
-
#increment! ⇒ Object
Atomically increments the counter and checks the limit.
-
#initialize(key_namespace: nil, key: nil, limit: nil, window: nil, redis: nil) ⇒ Counter
constructor
A new instance of Counter.
-
#transaction(rollback: true) ⇒ Object
Executes a block if the counter can be incremented.
Constructor Details
#initialize(key_namespace: nil, key: nil, limit: nil, window: nil, redis: nil) ⇒ Counter
Returns a new instance of Counter.
65 66 67 68 69 70 |
# File 'lib/cymometer/counter.rb', line 65 def initialize(key_namespace: nil, key: nil, limit: nil, window: nil, redis: nil) @redis = redis || Cymometer.redis @key = "#{key_namespace || "cymometer"}:#{key || generate_key}" @window = window || DEFAULT_WINDOW @limit = limit || 1 end |
Class Attribute Details
.decrement_sha ⇒ Object (readonly)
Returns the value of attribute decrement_sha.
62 63 64 |
# File 'lib/cymometer/counter.rb', line 62 def decrement_sha @decrement_sha end |
.increment_sha ⇒ Object (readonly)
Returns the value of attribute increment_sha.
62 63 64 |
# File 'lib/cymometer/counter.rb', line 62 def increment_sha @increment_sha end |
Instance Attribute Details
#key ⇒ Object (readonly)
Returns the value of attribute key.
10 11 12 |
# File 'lib/cymometer/counter.rb', line 10 def key @key end |
#limit ⇒ Object (readonly)
Returns the value of attribute limit.
10 11 12 |
# File 'lib/cymometer/counter.rb', line 10 def limit @limit end |
#window ⇒ Object (readonly)
Returns the value of attribute window.
10 11 12 |
# File 'lib/cymometer/counter.rb', line 10 def window @window end |
Instance Method Details
#count ⇒ Object
Returns the current count
106 107 108 109 110 111 112 113 |
# File 'lib/cymometer/counter.rb', line 106 def count current_time = (Time.now.to_f * 1_000_000).to_i window = @window * 1_000_000 # Clean expired entries @redis.zremrangebyscore(@key, 0, current_time - window) @redis.zcount(@key, current_time - window, "+inf").to_i end |
#decrement! ⇒ Object
Atomically decrements the counter by removing the oldest entry
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/cymometer/counter.rb', line 92 def decrement! current_time = (Time.now.to_f * 1_000_000).to_i window = @window * 1_000_000 result = evalsha_with_fallback( :decrement, keys: [@key], argv: [current_time, window] ) result[1].to_i end |
#increment! ⇒ Object
Atomically increments the counter and checks the limit
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/cymometer/counter.rb', line 73 def increment! current_time = (Time.now.to_f * 1_000_000).to_i # Microseconds window = @window * 1_000_000 # Convert to microseconds result = evalsha_with_fallback( :increment, keys: [@key], argv: [current_time, window, @limit] ) success = result[0] == 1 count = result[1].to_i raise LimitExceeded, "Limit of #{@limit} exceeded with count #{count}" unless success count end |
#transaction(rollback: true) ⇒ Object
Executes a block if the counter can be incremented. If the block raises an exception, decrements the counter and re-raises the exception.
117 118 119 120 121 122 123 124 125 |
# File 'lib/cymometer/counter.rb', line 117 def transaction(rollback: true) increment! begin yield rescue => e decrement! if rollback raise e end end |