Class: RubyReactor::RateLimit
- Inherits:
-
Object
- Object
- RubyReactor::RateLimit
- 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
-
#key_base ⇒ Object
readonly
Returns the value of attribute key_base.
-
#limits ⇒ Object
readonly
Returns the value of attribute limits.
Class Method Summary collapse
-
.normalize_specs(limit: nil, period: nil, limits: nil) ⇒ Object
Normalize the user-facing window args into the internal spec array that ‘RateLimit#initialize` expects.
Instance Method Summary collapse
- #check_and_increment! ⇒ Object
-
#initialize(key_base, limits:) ⇒ RateLimit
constructor
A new instance of RateLimit.
Constructor Details
#initialize(key_base, limits:) ⇒ RateLimit
Returns a new instance of RateLimit.
61 62 63 64 |
# File 'lib/ruby_reactor/rate_limit.rb', line 61 def initialize(key_base, limits:) @key_base = key_base @limits = limits end |
Instance Attribute Details
#key_base ⇒ Object (readonly)
Returns the value of attribute key_base.
56 57 58 |
# File 'lib/ruby_reactor/rate_limit.rb', line 56 def key_base @key_base end |
#limits ⇒ Object (readonly)
Returns the value of attribute limits.
56 57 58 |
# File 'lib/ruby_reactor/rate_limit.rb', line 56 def limits @limits end |
Class Method Details
.normalize_specs(limit: nil, period: nil, limits: nil) ⇒ Object
Normalize the user-facing window args into the internal spec array that ‘RateLimit#initialize` expects. Shared by the reactor DSL (`with_rate_limit`) and the global registry (`config.rate_limits.register`).
Accepts either a single window (‘limit:` + `period:`) or a hash of windows (`limits:`). Returns Array<Hashlimit:, name:>.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/ruby_reactor/rate_limit.rb', line 34 def self.normalize_specs(limit: nil, period: nil, limits: nil) if limits raise ArgumentError, "rate limit: use either :limits, or :limit + :period, not both" if limit || period limits.map do |period_key, limit_val| { period_seconds: RubyReactor::Period.period_seconds(period_key), limit: Integer(limit_val), name: period_key.to_s } end elsif limit && period [{ period_seconds: RubyReactor::Period.period_seconds(period), limit: Integer(limit), name: period.to_s }] else raise ArgumentError, "rate limit requires :limit + :period, or :limits" end end |
Instance Method Details
#check_and_increment! ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/ruby_reactor/rate_limit.rb', line 66 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 |