class RubyRollingRateLimiter
Constants
- VERSION
Attributes
current_error[R]
Your code goes hereā¦
Public Class Methods
new(limiter_identifier, interval_in_seconds, max_calls_per_interval, min_distance_between_calls_in_milliseconds = 1000, redis_connection = $redis)
click to toggle source
# File lib/ruby_rolling_rate_limiter.rb, line 11 def initialize(limiter_identifier, interval_in_seconds, max_calls_per_interval, min_distance_between_calls_in_milliseconds = 1000, redis_connection = $redis) @limiter_identifier = limiter_identifier @interval_in_seconds = interval_in_seconds @max_calls_per_interval = max_calls_per_interval @min_distance_between_calls_in_milliseconds = min_distance_between_calls_in_milliseconds @redis_connection = redis_connection #Check to ensure args are good. validate_arguments # Check Redis is there check_redis_is_available # Setup the Lock Manager @lock_manager ||= Redlock::Client.new([redis_connection]) end
Public Instance Methods
can_call_proceed?(call_size = 1)
click to toggle source
# File lib/ruby_rolling_rate_limiter.rb, line 33 def can_call_proceed?(call_size = 1) if call_size > @max_calls_per_interval @current_error = {code: 0, result: false, error: "Call size too big. Max calls in rolling window is: #{@max_calls_per_interval}. Increase your max_calls_per_interval or decrease your call_size", retry_in: 0} return false end results = false now = DateTime.now.strftime('%s%6N').to_i # Time since EPOC in microseconds. interval = @interval_in_seconds * 1000 * 1000 # Inteval in microseconds key = "#{self.class.name}-#{@limiter_identifier}-#{@id}" clear_before = now - interval # Begin multi redis max_retry_counter = 0 begin if max_retry_counter <= 100 @lock_manager.lock("#{key}-lock", 10000) do |locked| if locked # Because the resource is locked via redlock, I'm going to remove the multi on this. @redis_connection.zremrangebyscore(key, 0, clear_before.to_s) current_range = @redis_connection.zrange(key, 0, -1) if (current_range.count <= @max_calls_per_interval) && ((current_range.count+call_size) <= @max_calls_per_interval) && ((@min_distance_between_calls_in_milliseconds * 1000) && (now - current_range.last.to_i) > (@min_distance_between_calls_in_milliseconds * 1000)) results = @redis_connection.zrange(key, 0, -1) call_size.times do @redis_connection.zadd(key, now.to_s, now.to_s) # This will allow us to make spacing between the weights. now = DateTime.now.strftime('%s%6N').to_i # Time since EPOC in microseconds. end @redis_connection.expire(key, @interval_in_seconds) else results = current_range end else raise Errors::LockWaiting, "Could not aquire lock" end end else raise Errors::MaxRetryReachedOnLockAcquire, "Unable to acquire lock for rate limit after 100 attempts" end rescue Errors::LockWaiting sleep 0.2 max_retry_counter +=1 retry end if results call_set = results too_many_in_interval = call_set.count >= @max_calls_per_interval time_since_last_request = (@min_distance_between_calls_in_milliseconds * 1000) && (now - call_set.last.to_i) if too_many_in_interval @current_error = {code: 1, result: false, error: "Too many requests", retry_in: (call_set.first.to_i - now + interval) / 1000 / 1000, retry_in_micro: (call_set.first.to_i - now + interval)} return false elsif (call_set.count+call_size) > @max_calls_per_interval @current_error = {code: 2, result: false, error: "Call Size too big for available access, trying to make #{call_size} with only #{call_set.count} calls available in window", retry_in: (call_set.first.to_i - now + interval) / 1000 / 1000, retry_in_micro: (call_set.first.to_i - now + interval)} return false elsif time_since_last_request < (@min_distance_between_calls_in_milliseconds * 1000) @current_error = {code: 3, result: false, error: "Attempting to thrash faster than the minimal distance between calls", retry_in: @min_distance_between_calls_in_milliseconds / 1000, retry_in_micro: (@min_distance_between_calls_in_milliseconds * 1000)} return false end return true end return false end
set_call_identifier(id)
click to toggle source
# File lib/ruby_rolling_rate_limiter.rb, line 28 def set_call_identifier(id) raise Errors::ArgumentInvalid, "The id must be a string or number with length greater than zero" unless id.length > 0 @id = id end
Private Instance Methods
check_redis_is_available()
click to toggle source
Checks to ensure redis is present and available,
# File lib/ruby_rolling_rate_limiter.rb, line 109 def check_redis_is_available raise Errors::RedisNotFound, "Unable to find redis connection, either declare a global $redis connection or add the connection to the last argument on #{self.class.name} initializer" unless @redis_connection.is_a? Object and @redis_connection.class == Redis end
validate_arguments()
click to toggle source
# File lib/ruby_rolling_rate_limiter.rb, line 100 def validate_arguments raise Errors::ArgumentInvalid, "limiter_identifier argument must be 1 or more characters long" unless @limiter_identifier.length > 0 raise Errors::ArgumentInvalid, "interval_in_seconds argument must be an integer, this is specified in seconds" unless @interval_in_seconds.is_a? Integer and @interval_in_seconds > 0 raise Errors::ArgumentInvalid, "max_calls_per_interval argument must be an integer, this is the amount of calls that can be made during the rolling window." unless @max_calls_per_interval.is_a? Integer and @max_calls_per_interval > 0 raise Errors::ArgumentInvalid, "min_distance_between_calls_in_milliseconds argument must be an integer, this is the buffer between each call during the rolling window" unless (@min_distance_between_calls_in_milliseconds.is_a? Integer or @min_distance_between_calls_in_milliseconds.is_a? Float)and @min_distance_between_calls_in_milliseconds > 0 end