Class: Labkit::RateLimit::Evaluator Private

Inherits:
Object
  • Object
show all
Defined in:
lib/labkit/rate_limit/evaluator.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Evaluator holds the static parts of a rate limit check (name, rules, Redis) and exposes a per-request #check(identifier) method.

Defined Under Namespace

Classes: RuleCursor

Constant Summary collapse

REDIS_KEY_PREFIX =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

"labkit:rl"
CHAR_VALUE_MAX_LENGTH =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

200
MISSING_VALUE_SENTINEL =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

"_unknown_"
INCR_SCRIPT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Atomic increment-with-TTL Lua script. The whole script runs as one operation from Redis's perspective, so there is no window between the increment and EXPIRE that can leak a key without TTL.

INCRBYFLOAT serves both count-mode (cost=1, equivalent to INCR for integer-encoded keys) and cost-mode callers, so a single script handles every rule shape. cost=0 also flows through INCRBYFLOAT; Redis treats the result as a no-op on the stored value while still observing the post-state count and TTL we return.

ttl_after < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry). The -1 case shouldn't arise with the atomic script, but self-healing recovers keys left without TTL by any prior bug.

Labkit::Redis::Script.new(<<~LUA)
  local ttl = ARGV[1]
  local cost = tonumber(ARGV[2])

  local count = redis.call('INCRBYFLOAT', KEYS[1], cost)
  local ttl_after = redis.call('TTL', KEYS[1])
  if ttl_after < 0 then
    redis.call('EXPIRE', KEYS[1], ttl)
    ttl_after = tonumber(ttl)
  end

  return {count, ttl_after}
LUA
SADD_SCRIPT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Atomic SADD + SCARD + conditional EXPIRE. SET-cardinality counterpart of INCR_SCRIPT; same shape (mutate, read TTL, set TTL when missing, return post-state TTL). count is SCARD, not the SADD return.

ttl_after < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry), so this also self-heals orphan keys left without TTL.

Labkit::Redis::Script.new(<<~LUA)
  local ttl = ARGV[1]
  local member = ARGV[2]

  redis.call('SADD', KEYS[1], member)
  local count = redis.call('SCARD', KEYS[1])
  local ttl_after = redis.call('TTL', KEYS[1])
  if ttl_after < 0 then
    redis.call('EXPIRE', KEYS[1], ttl)
    ttl_after = tonumber(ttl)
  end

  return {count, ttl_after}
LUA

Instance Method Summary collapse

Constructor Details

#initialize(name:, rules:, redis:, logger:) ⇒ Evaluator

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Evaluator.



69
70
71
72
73
74
# File 'lib/labkit/rate_limit/evaluator.rb', line 69

def initialize(name:, rules:, redis:, logger:)
  @name   = name
  @rules  = rules
  @redis  = redis
  @logger = logger
end

Instance Method Details

#check(identifier, cost: 1, rule_context: nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



76
77
78
79
80
81
82
83
84
85
# File 'lib/labkit/rate_limit/evaluator.rb', line 76

def check(identifier, cost: 1, rule_context: nil)
  cursor = RuleCursor.new
  check_rules(identifier, cost, rule_context, cursor)
rescue StandardError => e
  # Intentionally broad: fail-open applies to any unexpected error (network,
  # timeout, OOM) not only Redis protocol errors.
  report_error_metrics
  log_error(e, identifier, cursor.rule)
  Result.error
end

#peek(identifier, rule_context: nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Read-without-increment counterpart to #check. Same matching and Result shape; the underlying Redis counter is not mutated and the TTL is not extended. A missing Redis key is treated as count=0 (matched, not exceeded).



90
91
92
93
94
95
96
97
# File 'lib/labkit/rate_limit/evaluator.rb', line 90

def peek(identifier, rule_context: nil)
  cursor = RuleCursor.new
  peek_rules(identifier, rule_context, cursor)
rescue StandardError => e
  report_error_metrics
  log_error(e, identifier, cursor.rule)
  Result.error
end