Class: Pecorino::Adapters::RedisAdapter

Inherits:
BaseAdapter show all
Defined in:
lib/pecorino/adapters/redis_adapter.rb

Overview

An adapter for storing Pecorino leaky buckets and blocks in Redis. It uses Lua to enforce atomicity for leaky bucket operations

Defined Under Namespace

Classes: RedisScript

Constant Summary collapse

ADD_TOKENS_SCRIPT =
RedisScript.new("add_tokens_conditionally.lua")

Instance Method Summary collapse

Methods inherited from BaseAdapter

#create_tables, #prune

Constructor Details

#initialize(redis_connection_or_connection_pool, key_prefix: "pecorino") ⇒ RedisAdapter

Returns a new instance of RedisAdapter.



30
31
32
33
# File 'lib/pecorino/adapters/redis_adapter.rb', line 30

def initialize(redis_connection_or_connection_pool, key_prefix: "pecorino")
  @redis_pool = redis_connection_or_connection_pool
  @key_prefix = key_prefix
end

Instance Method Details

#add_tokens(key:, capacity:, leak_rate:, n_tokens:) ⇒ Object

Adds tokens to the leaky bucket. The return value is a tuple of two values: the current level (Float) and whether the bucket is now at capacity (Boolean)



43
44
45
46
47
48
49
50
# File 'lib/pecorino/adapters/redis_adapter.rb', line 43

def add_tokens(key:, capacity:, leak_rate:, n_tokens:)
  keys = ["#{@key_prefix}:leaky_bucket:#{key}:level", "#{@key_prefix}:leaky_bucket:#{key}:last_touched"]
  argv = [leak_rate, n_tokens, capacity, _conditional = 0]
  decimal_float_level, at_capacity_int, _ = with_redis do |redis|
    ADD_TOKENS_SCRIPT.load_and_eval(redis, keys, argv)
  end
  [decimal_float_level.to_f, at_capacity_int == 1]
end

#add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:) ⇒ Object

Adds tokens to the leaky bucket conditionally. If there is capacity, the tokens will be added. If there isn’t - the fillup will be rejected. The return value is a triplet of the current level (Float), whether the bucket is now at capacity (Boolean) and whether the fillup was accepted (Boolean)



56
57
58
59
60
61
62
63
# File 'lib/pecorino/adapters/redis_adapter.rb', line 56

def add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:)
  keys = ["#{@key_prefix}:leaky_bucket:#{key}:level", "#{@key_prefix}:leaky_bucket:#{key}:last_touched"]
  argv = [leak_rate, n_tokens, capacity, _conditional = 1]
  decimal_float_level, at_capacity_int, did_accept_int = with_redis do |redis|
    ADD_TOKENS_SCRIPT.load_and_eval(redis, keys, argv)
  end
  [decimal_float_level.to_f, at_capacity_int == 1, did_accept_int == 1]
end

#blocked_until(key:) ⇒ Object

Returns the time until which a block for a given key is in effect. If there is no block in effect, the method should return ‘nil`. The return value is either a `Time` or `nil`



78
79
80
81
82
83
84
# File 'lib/pecorino/adapters/redis_adapter.rb', line 78

def blocked_until(key:)
  seconds_from_epoch = with_redis do |r|
    r.get("#{@key_prefix}:leaky_bucket:#{key}:block")
  end
  return unless seconds_from_epoch
  Time.at(seconds_from_epoch.to_f).utc
end

#set_block(key:, block_for:) ⇒ Object

Sets a timed block for the given key - this is used when a throttle fires. The return value is not defined - the call should always succeed.

Raises:

  • (ArgumentError)


67
68
69
70
71
72
73
74
# File 'lib/pecorino/adapters/redis_adapter.rb', line 67

def set_block(key:, block_for:)
  raise ArgumentError, "block_for must be positive" unless block_for > 0
  blocked_until = Time.now + block_for
  with_redis do |r|
    r.setex("#{@key_prefix}:leaky_bucket:#{key}:block", block_for.to_f.ceil, blocked_until.to_f)
  end
  blocked_until
end

#state(key:, capacity:, leak_rate:) ⇒ Object

Returns the state of a leaky bucket. The state should be a tuple of two values: the current level (Float) and whether the bucket is now at capacity (Boolean)



37
38
39
# File 'lib/pecorino/adapters/redis_adapter.rb', line 37

def state(key:, capacity:, leak_rate:)
  add_tokens(key: key, capacity: capacity, leak_rate: leak_rate, n_tokens: 0)
end