Simple Limiter based on individual keys instead of Redis hashes which are impossible to expire individually
require 'redis' | |
require 'redis-namespace' | |
class Limiter | |
def initialize(name:, threshold:, interval:, time_span: 600, bucket_span: 5) | |
@name, @threshold, @interval, @time_span, @bucket_span = name, threshold, interval, time_span, bucket_span | |
raise ArgumentError if @interval > @time_span || @interval < @bucket_span | |
@redis ||= Redis::Namespace.new(:limiter, redis: $redis || Redis.new) | |
@all_buckets_count = (@time_span / @bucket_span).floor | |
@sliding_window_buckets_count = (@interval.to_f / @bucket_span).floor | |
end | |
def add(count: 1) | |
key = [@name, get_bucket_index].join(":") | |
@redis.multi do | |
@redis.incrby(key, count) | |
@redis.expire(key, @interval) | |
end | |
nil | |
end | |
def count | |
current_bucket_index = get_bucket_index | |
bucket_indices = @sliding_window_buckets_count.times.map do |i| | |
(current_bucket_index - i) % @all_buckets_count | |
end | |
@redis.multi do | |
bucket_indices.map do |i| | |
key = [@name, i].join(":") | |
@redis.get(key) | |
end | |
end.map(&:to_i).sum | |
end | |
def exec_within_threshold | |
sleep @bucket_span while exceeded? | |
yield | |
end | |
def exceeded? | |
count >= @threshold | |
end | |
private | |
def get_bucket_index | |
((Time.now.to_i % @all_buckets_count) / @bucket_span).floor | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment