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 =
" local key = KEYS[1]\n local current_time = tonumber(ARGV[1])\n local window = tonumber(ARGV[2])\n local limit = tonumber(ARGV[3])\n\n -- Remove entries older than the window size\n redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)\n\n -- Get the current count\n local count = redis.call('ZCOUNT', key, current_time - window, '+inf')\n\n if count >= limit then\n -- Limit exceeded, do not increment\n return {0, count}\n else\n -- Increment the counter\n redis.call('ZADD', key, current_time, current_time)\n redis.call('EXPIRE', key, math.floor(window / 1000000))\n return {1, count + 1}\n end\n".freeze
- DECREMENT_LUA_SCRIPT =
" local key = KEYS[1]\n local current_time = tonumber(ARGV[1])\n local window = tonumber(ARGV[2])\n\n -- Remove entries older than the window size\n redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)\n\n -- Attempt to remove the entry with the lowest score (oldest entry)\n local entries = redis.call('ZRANGEBYSCORE', key, current_time - window, '+inf', 'LIMIT', 0, 1)\n\n if next(entries) == nil then\n -- No entries to remove, safe no-op\n local count = 0\n return {1, count}\n else\n local member = entries[1]\n redis.call('ZREM', key, member)\n local count = redis.call('ZCOUNT', key, current_time - window, '+inf')\n return {1, count}\n end\n".freeze
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 |