Class: RubyReactor::RateLimit

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

Overview

Distributed rate limiter (fixed-window counter, multi-window aware).

A ‘RateLimit` is configured with one or more (period, limit) tuples. `check_and_increment!` atomically verifies every window has headroom and, if so, increments all of them. The check uses a single Lua script so nothing slips through between read and write.

When any window is over-limit the call raises ‘ExceededError` carrying a `retry_after_seconds` hint (time until the tightest failing bucket rolls). The Sidekiq worker uses this hint to schedule a precise snooze.

Defined Under Namespace

Classes: ExceededError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key_base, limits:) ⇒ RateLimit

Returns a new instance of RateLimit.

Parameters:

  • key_base (String)

    caller-provided key (e.g. “stripe:account_42”)

  • limits (Array<Hash>)

    each hash needs :period_seconds, :limit, and :name (used in the Redis bucket key and the error message)



33
34
35
36
# File 'lib/ruby_reactor/rate_limit.rb', line 33

def initialize(key_base, limits:)
  @key_base = key_base
  @limits = limits
end

Instance Attribute Details

#key_baseObject (readonly)

Returns the value of attribute key_base.



28
29
30
# File 'lib/ruby_reactor/rate_limit.rb', line 28

def key_base
  @key_base
end

#limitsObject (readonly)

Returns the value of attribute limits.



28
29
30
# File 'lib/ruby_reactor/rate_limit.rb', line 28

def limits
  @limits
end

Instance Method Details

#check_and_increment!Object

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/ruby_reactor/rate_limit.rb', line 38

def check_and_increment!
  now = Time.now.to_i
  keys = @limits.map { |spec| bucket_key(spec, now) }
  argv = [now]
  @limits.each do |spec|
    argv << spec[:period_seconds]
    argv << spec[:limit]
    argv << spec[:period_seconds] * 2 # TTL: generous, auto-cleans stale buckets
  end

  allowed, retry_after, failed_index = adapter.rate_limit_check_and_increment(keys, argv)
  return true if allowed == 1

  failed = @limits[failed_index - 1]
  raise ExceededError.new(
    "Rate limit '#{@key_base}' exceeded (#{failed[:limit]}/#{failed[:name]}); " \
    "retry in #{retry_after}s",
    retry_after_seconds: retry_after,
    key_base: @key_base,
    limit: failed[:limit],
    period_seconds: failed[:period_seconds],
    period_name: failed[:name]
  )
end