Class: CMDx::RateLimit

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

Overview

Declared via Task.rate_limit. After inputs resolve, consults a shared limiter once per execution (retries do not consume additional quota) and short-circuits before the circuit breaker when the bucket is full.

Store keys come from StoreKey#task_key with class_scoped: true — either namespace:TaskName for {StoreKey::CLASS_KEY} (:class) or namespace:TaskName:segment when a resolver yields a segment.

Defined Under Namespace

Classes: Admission, Quota

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, options = EMPTY_HASH) ⇒ RateLimit

Returns a new instance of RateLimit.

Parameters:

  • key (Symbol, String, Proc, #call, nil) —

    :class, context Symbol, static String, or callable; nil disables rate limiting

  • options (Hash{Symbol => Object}) (defaults to: EMPTY_HASH)

Options Hash (options):

  • :max (Integer) —

    maximum admissions per window or bucket capacity

  • :per (Integer) —

    window length in seconds (:fixed_window) or seconds to leak one unit (:leaky_bucket)

  • :limiter (Symbol, #call) — default: `:fixed_window`, `:leaky_bucket`, or custom
  • :cost (Numeric) — default: 1 —

    units charged per admitted execution

  • :namespace (String) — default: "cmdx:rl" —

    first segment of store keys

  • :on_limit (Symbol) — default: :fail —

    :fail or :skip when over limit

  • :if (Symbol, Proc, #call) —

    gate (task) before checking

  • :unless (Symbol, Proc, #call) —

    gate (task) before checking



65
66
67
68
# File 'lib/cmdx/rate_limit.rb', line 65

def initialize(key, options = EMPTY_HASH)
  @key     = key
  @options = options.freeze
end

Instance Attribute Details

#key ⇒ Object (readonly)

Returns the value of attribute key.



51
52
53
# File 'lib/cmdx/rate_limit.rb', line 51

def key
  @key
end

Class Method Details

.disabled ⇒ RateLimit

Returns disabled configuration (no key).

Returns:

  • (RateLimit) —

    disabled configuration (no key)



45
46
47
# File 'lib/cmdx/rate_limit.rb', line 45

def disabled
  DISABLED
end

Instance Method Details

#build(new_key, new_options) ⇒ RateLimit

Returns a new RateLimit layering new_options onto the current one.

Parameters:

  • new_key (Symbol, String, Proc, #call, nil)
  • new_options (Hash{Symbol => Object})

Returns:



123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/cmdx/rate_limit.rb', line 123

def build(new_key, new_options)
  return self if new_key.nil? && new_options.empty?

  merged_key =
    if new_key.nil?
      enabled? || new_options.empty? ? @key : StoreKey::CLASS_KEY
    else
      new_key
    end
  merged_options = @options.merge(new_options)

  self.class.new(merged_key, merged_options)
end

#condition_if ⇒ Symbol, ...

Returns:

  • (Symbol, Proc, #call, nil)


104
105
106
# File 'lib/cmdx/rate_limit.rb', line 104

def condition_if
  @options[:if]
end

#condition_unless ⇒ Symbol, ...

Returns:

  • (Symbol, Proc, #call, nil)


109
110
111
# File 'lib/cmdx/rate_limit.rb', line 109

def condition_unless
  @options[:unless]
end

#cost ⇒ Numeric

Returns:

  • (Numeric)


91
92
93
94
95
96
# File 'lib/cmdx/rate_limit.rb', line 91

def cost
  raw = @options[:cost]
  return 1 if raw.nil?

  Float(raw)
end

#enabled? ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/cmdx/rate_limit.rb', line 114

def enabled?
  !key.nil?
end

#limiter ⇒ Symbol

Returns:

  • (Symbol)


86
87
88
# File 'lib/cmdx/rate_limit.rb', line 86

def limiter
  @options[:limiter] || :fixed_window
end

#max ⇒ Integer?

Returns:

  • (Integer, nil)


76
77
78
# File 'lib/cmdx/rate_limit.rb', line 76

def max
  @options[:max]
end

#namespace ⇒ String

Returns:

  • (String)


71
72
73
# File 'lib/cmdx/rate_limit.rb', line 71

def namespace
  @options[:namespace] || "cmdx:rl"
end

#on_limit ⇒ Symbol

Returns :fail or :skip.

Returns:

  • (Symbol) —

    :fail or :skip



99
100
101
# File 'lib/cmdx/rate_limit.rb', line 99

def on_limit
  @options[:on_limit] || :fail
end

#per ⇒ Integer?

Returns:

  • (Integer, nil)


81
82
83
# File 'lib/cmdx/rate_limit.rb', line 81

def per
  @options[:per]
end

#process(task) { ... } ⇒ Object

Note:

When over limit, signals skip! or fail! on task — control does not return to the caller of #process.

Runs the limiter once, then yields when admitted or inactive.

Parameters:

Yields:

  • invoked when admitted or rate limiting is inactive

Yield Returns:

  • (Object) —

    return value from the wrapped work

Returns:

  • (Object) —

    the block's return value

Raises:

  • (ArgumentError) —

    when max / per are missing or invalid, or on_limit is not :fail or :skip



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/cmdx/rate_limit.rb', line 147

def process(task)
  return yield unless enabled?
  return yield unless Util.satisfied?(condition_if, condition_unless, task)

  store_key = StoreKey.task_key(task, namespace:, resolver: key, class_scoped: true)
  return yield if store_key.nil?

  quota = Quota.new(
    task:,
    store_key:,
    max: Integer(max),
    per: Integer(per),
    cost:,
    limiter:,
    options: @options
  )

  admission = invoke_limiter(task, quota)
  if admission.admitted?
    admitted!(task, store_key, admission, quota)
    yield
  else
    exceeded!(task, store_key, admission, quota)
  end
end