class Prorate::LeakyBucket

This offers just the leaky bucket implementation with fill control, but without the timed lock. It does not raise any exceptions, it just tracks the state of a leaky bucket in Redis.

Important differences from the more full-featured Throttle class are:

It does have a few downsides compared to the Throttle though

Constants

LUA_SCRIPT_CODE
LUA_SCRIPT_HASH

Public Class Methods

new(redis_key_prefix:, leak_rate:, redis:, bucket_capacity:) click to toggle source

Creates a new LeakyBucket. The object controls 2 keys in Redis: one for the last access time, and one for the contents of the key.

@param redis_key_prefix the prefix that is going to be used for keys.

If your bucket is specific to a user, a browser or an IP address you need to mix in
those values into the key prefix as appropriate.

@param leak_rate the leak rate of the bucket, in tokens per second @param redis a Redis connection or a ConnectionPool instance

if you are using the connection_pool gem. With a connection pool Prorate will
checkout a connection using `#with` and check it in when it's done.

@param bucket_capacity how many tokens is the bucket capped at.

Filling up the bucket using `fillup()` will add to that number, but
the bucket contents will then be capped at this value. So with
bucket_capacity set to 12 and a `fillup(14)` the bucket will reach the level
of 12, and will then immediately start leaking again.
# File lib/prorate/leaky_bucket.rb, line 71
def initialize(redis_key_prefix:, leak_rate:, redis:, bucket_capacity:)
  @redis_key_prefix = redis_key_prefix
  @redis = redis.respond_to?(:with) ? redis : NullPool.new(redis)
  @leak_rate = leak_rate.to_f
  @capacity = bucket_capacity.to_f
end

Public Instance Methods

fillup(n_tokens) click to toggle source

Places `n` tokens in the bucket.

@return [BucketState] the state of the bucket after the operation

# File lib/prorate/leaky_bucket.rb, line 81
def fillup(n_tokens)
  run_lua_bucket_script(n_tokens.to_f)
end
last_updated_key() click to toggle source

Returns the Redis key under which the last updated time of the bucket gets stored. Note that the key is not guaranteed to contain a value if the bucket has not been filled up recently.

@return [String]

# File lib/prorate/leaky_bucket.rb, line 106
def last_updated_key
  "#{@redis_key_prefix}.leaky_bucket.last_updated"
end
leaky_bucket_key() click to toggle source

Returns the Redis key for the leaky bucket itself Note that the key is not guaranteed to contain a value if the bucket has not been filled up recently.

@return [String]

# File lib/prorate/leaky_bucket.rb, line 97
def leaky_bucket_key
  "#{@redis_key_prefix}.leaky_bucket.bucket_level"
end
state() click to toggle source

Returns the current state of the bucket, containing the level and whether the bucket is full

@return [BucketState] the state of the bucket after the operation

# File lib/prorate/leaky_bucket.rb, line 88
def state
  run_lua_bucket_script(0)
end

Private Instance Methods

run_lua_bucket_script(n_tokens) click to toggle source
# File lib/prorate/leaky_bucket.rb, line 112
def run_lua_bucket_script(n_tokens)
  @redis.with do |r|
    begin
      # The script returns a tuple of "whole tokens, microtokens"
      # to be able to smuggle the float across (similar to Redis TIME command)
      level_str, is_full_int = r.evalsha(
        LUA_SCRIPT_HASH,
        keys: [leaky_bucket_key, last_updated_key], argv: [@leak_rate, n_tokens, @capacity])
      BucketState.new(level_str.to_f, is_full_int == 1)
    rescue Redis::CommandError => e
      if e.message.include? "NOSCRIPT"
        # The Redis server has never seen this script before. Needs to run only once in the entire lifetime
        # of the Redis server, until the script changes - in which case it will be loaded under a different SHA
        r.script(:load, LUA_SCRIPT_CODE)
        retry
      else
        raise e
      end
    end
  end
end