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.

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_before < 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 ttl_before = redis.call('TTL', KEYS[1])

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

  return {count, redis.call('TTL', KEYS[1])}
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.



41
42
43
44
45
46
# File 'lib/labkit/rate_limit/evaluator.rb', line 41

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

Instance Method Details

#check(identifier, cost: 1) ⇒ 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.



48
49
50
51
52
53
54
55
56
# File 'lib/labkit/rate_limit/evaluator.rb', line 48

def check(identifier, cost: 1)
  check_rules(identifier, cost)
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)
  Result.new(matched: false, error: true, action: :allow)
end

#peek(identifier) ⇒ 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).



61
62
63
64
65
66
67
# File 'lib/labkit/rate_limit/evaluator.rb', line 61

def peek(identifier)
  peek_rules(identifier)
rescue StandardError => e
  report_error_metrics
  log_error(e, identifier)
  Result.new(matched: false, error: true, action: :allow)
end