Class: Labkit::RateLimit::Limiter

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

Overview

Limiter is the primary public API for rate limiting. Instantiate once per call site (e.g. at application boot), then call #check(identifier) on every request. The internal Evaluator is reused across calls, avoiding per-request object allocation.

Examples:

limiter = Labkit::RateLimit::Limiter.new(
  name: "rack_request",
  rules: [Labkit::RateLimit::Rule.new(name: "api_user", limit: 100, period: 60, characteristics: [:user])]
)
result = limiter.check({ user: 42, ip: "1.2.3.4" })
render_429 if result.exceeded? && result.action == :block

Constant Summary collapse

NAME_PATTERN =
/\A[a-z0-9_]+\z/

Instance Method Summary collapse

Constructor Details

#initialize(name:, rules:, redis: nil, logger: nil) ⇒ Limiter

Returns a new instance of Limiter.



22
23
24
25
26
27
28
29
30
31
32
# File 'lib/labkit/rate_limit/limiter.rb', line 22

def initialize(name:, rules:, redis: nil, logger: nil)
  @logger = logger || RateLimit.config.logger || Labkit::Logging::JsonLogger.new($stdout)
  @name = validate_name!(name)

  @evaluator = Evaluator.new(
    name: @name,
    rules: prepare_rules(rules),
    redis: redis || RateLimit.config.redis,
    logger: @logger
  )
end

Instance Method Details

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

Parameters:

  • identifier (Identifier, Hash)

    caller attributes for this request

  • cost (Numeric) (defaults to: 1)

    amount to add to the counter. Defaults to 1 (count-mode). Pass a non-1 Numeric for cost-mode counters such as resource-usage limits; passing 0 reads the counter without writing.

  • rule_context (Hash, nil) (defaults to: nil)

    optional per-request context passed to one-arity callables on limit/period. Lets rules resolve dynamic configuration (e.g. per-namespace settings) without rebuilding the Rule or doing out-of-band DB queries. Zero-arity callables ignore it. The key contract is owned by the rule’s callable, not validated here: if the rule reads ctx and the caller passes ctx, the callable’s fallback branch fires silently. Keep the rule definition and the call site colocated.

Returns:



47
48
49
50
# File 'lib/labkit/rate_limit/limiter.rb', line 47

def check(identifier, cost: 1, rule_context: nil)
  id = identifier.is_a?(Identifier) ? identifier : Identifier.new(identifier)
  @evaluator.check(id, cost: cost, rule_context: rule_context)
end

#peek(identifier, rule_context: nil) ⇒ Result

Read the current rate-limit state without incrementing the counter. Mirrors #check except the underlying counter is not mutated and the TTL is not extended. Useful for “have we already throttled this caller?” checks where the caller has another path that does the actual increment (typical pattern: peek to gate a side-effect, then call #check on the path that should count).

When the underlying Redis key does not exist yet, the result reports count=0, exceeded=false, and remaining=resolved_limit; matched? is still true because the rule applied. On Redis error the result fails open identically to #check.

Parameters:

  • identifier (Identifier, Hash)

    caller attributes for this request

  • rule_context (Hash, nil) (defaults to: nil)

    see #check

Returns:



67
68
69
70
# File 'lib/labkit/rate_limit/limiter.rb', line 67

def peek(identifier, rule_context: nil)
  id = identifier.is_a?(Identifier) ? identifier : Identifier.new(identifier)
  @evaluator.peek(id, rule_context: rule_context)
end