Module: Wurk::Limiter
- Defined in:
- lib/wurk/limiter.rb,
lib/wurk/limiter/base.rb,
lib/wurk/limiter/leaky.rb,
lib/wurk/limiter/bucket.rb,
lib/wurk/limiter/points.rb,
lib/wurk/limiter/window.rb,
lib/wurk/limiter/unlimited.rb,
lib/wurk/limiter/concurrent.rb,
lib/wurk/limiter/server_middleware.rb
Overview
Sidekiq Enterprise rate limiters: concurrent, bucket, window, leaky, points, unlimited. Lua-backed; all timing inside Lua is from TIME so clock skew across hosts doesn’t matter inside one Redis. Spec: docs/target/sidekiq-ent.md §1.
Layout (one file per type under ‘lib/wurk/limiter/`):
* `Limiter::Base` owns the metadata write (lmtr:{name}) + the global
`lmtr-list` registration so the Web UI can list every limiter, and
the uniform `status` shape.
* Per-type subclasses (Concurrent / Bucket / Window / Leaky / Points)
own their acquire/wait loop. Each delegates the atomic step to a
Lua script in `lib/wurk/lua/limiter_*.lua`.
* `Unlimited` is a no-op stub for tests and the `unlimited(*)`
constructor — same `within_limit` surface, never raises.
* `ServerMiddleware` catches OverLimit, reschedules, and applies the
poison brake.
Wire-compat: every key uses the ‘lmtr-…:` prefix family from §1.7 and the limiter is added to the shared `lmtr-list` SET.
Defined Under Namespace
Classes: Base, Bucket, Concurrent, Config, Leaky, OverLimit, Points, ServerMiddleware, Unlimited, Window
Constant Summary collapse
- DEFAULT_TTL =
90 * 24 * 3600
- DEFAULT_WAIT_TIMEOUT =
5- DEFAULT_LOCK_TIMEOUT =
30- DEFAULT_RESCHEDULE =
20- DEFAULT_BACKOFF =
lambda do |_limiter, job, _exc| overrated = job.is_a?(Hash) ? job.fetch('overrated', 0).to_i : 0 (300 * overrated) + rand(300) + 1 end
- NAME_PATTERN =
/\A[\w\-:.\#@]+\z/- LIST_KEY =
'lmtr-list'- INTERVAL_UNITS =
‘:second :minute :hour :day` symbols → seconds. Window also accepts a raw Integer; bucket does not (boundary semantics require a unit).
{ second: 1, minute: 60, hour: 3600, day: 86_400 }.freeze
- TYPE_CLASSES =
Type string (as stored in the ‘lmtr:name` meta hash) → subclass. Drives `build` for dashboard introspection.
{ 'concurrent' => 'Concurrent', 'bucket' => 'Bucket', 'window' => 'Window', 'leaky' => 'Leaky', 'points' => 'Points' }.freeze
Class Method Summary collapse
- .bucket(name, count, interval, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL, reschedule: DEFAULT_RESCHEDULE) ⇒ Object
-
.build(name, type, options, register: false) ⇒ Object
Reconstruct a limiter from its persisted metadata for read-only introspection (the dashboard ‘status` column).
- .concurrent(name, limit, wait_timeout: DEFAULT_WAIT_TIMEOUT, lock_timeout: DEFAULT_LOCK_TIMEOUT, policy: :raise, backoff: nil, ttl: DEFAULT_TTL) ⇒ Object
- .config ⇒ Object
- .configure {|config| ... } ⇒ Object
- .interval_seconds(interval, allow_integer:) ⇒ Object
- .leaky(name, bucket_size, drain, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL) ⇒ Object
- .points(name, initial_points, refill_per_second, backoff: nil, ttl: DEFAULT_TTL) ⇒ Object
-
.redis ⇒ Object
Redis access: caller-supplied pool (Limiter.configure.redis = …) wins, else fall back to the default Wurk pool.
-
.reset_config! ⇒ Object
Test helper: blow away config + cached pool so a test that mutates ‘config.backoff` doesn’t leak into the next one.
- .unlimited(*_args, **_opts) ⇒ Object
- .window(name, count, interval, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL, reschedule: DEFAULT_RESCHEDULE) ⇒ Object
Class Method Details
.bucket(name, count, interval, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL, reschedule: DEFAULT_RESCHEDULE) ⇒ Object
154 155 156 157 158 159 160 161 162 163 |
# File 'lib/wurk/limiter.rb', line 154 def bucket(name, count, interval, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL, reschedule: DEFAULT_RESCHEDULE) Bucket.new(name, count: count, interval: interval, wait_timeout: wait_timeout, backoff: backoff, ttl: ttl, reschedule: reschedule) end |
.build(name, type, options, register: false) ⇒ Object
Reconstruct a limiter from its persisted metadata for read-only introspection (the dashboard ‘status` column). `register: false` keeps the GET side-effect-free. Returns nil for an unknown type.
200 201 202 203 204 205 206 207 |
# File 'lib/wurk/limiter.rb', line 200 def build(name, type, , register: false) return Unlimited.new if type.to_s == 'unlimited' klass_name = TYPE_CLASSES[type.to_s] return nil unless klass_name const_get(klass_name).new(name, register: register, **()) end |
.concurrent(name, limit, wait_timeout: DEFAULT_WAIT_TIMEOUT, lock_timeout: DEFAULT_LOCK_TIMEOUT, policy: :raise, backoff: nil, ttl: DEFAULT_TTL) ⇒ Object
143 144 145 146 147 148 149 150 151 152 |
# File 'lib/wurk/limiter.rb', line 143 def concurrent(name, limit, wait_timeout: DEFAULT_WAIT_TIMEOUT, lock_timeout: DEFAULT_LOCK_TIMEOUT, policy: :raise, backoff: nil, ttl: DEFAULT_TTL) Concurrent.new(name, limit: limit, wait_timeout: wait_timeout, lock_timeout: lock_timeout, policy: policy, backoff: backoff, ttl: ttl) end |
.config ⇒ Object
124 125 126 |
# File 'lib/wurk/limiter.rb', line 124 def config @config ||= Config.new end |
.configure {|config| ... } ⇒ Object
120 121 122 |
# File 'lib/wurk/limiter.rb', line 120 def configure yield config end |
.interval_seconds(interval, allow_integer:) ⇒ Object
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/wurk/limiter.rb', line 209 def interval_seconds(interval, allow_integer:) interval = interval.to_sym if interval.is_a?(String) && INTERVAL_UNITS.key?(interval.to_sym) case interval when Symbol INTERVAL_UNITS.fetch(interval) do raise ArgumentError, "interval must be one of #{INTERVAL_UNITS.keys.inspect} (got #{interval.inspect})" end when Integer unless allow_integer raise ArgumentError, "interval must be a Symbol (got Integer); use #{INTERVAL_UNITS.keys.inspect}" end interval else raise ArgumentError, "interval must be Symbol or Integer (got #{interval.class})" end end |
.leaky(name, bucket_size, drain, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL) ⇒ Object
176 177 178 179 180 181 182 183 |
# File 'lib/wurk/limiter.rb', line 176 def leaky(name, bucket_size, drain, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL) Leaky.new(name, bucket_size: bucket_size, drain: drain, wait_timeout: wait_timeout, backoff: backoff, ttl: ttl) end |
.points(name, initial_points, refill_per_second, backoff: nil, ttl: DEFAULT_TTL) ⇒ Object
185 186 187 188 189 190 191 |
# File 'lib/wurk/limiter.rb', line 185 def points(name, initial_points, refill_per_second, backoff: nil, ttl: DEFAULT_TTL) Points.new(name, initial: initial_points, refill: refill_per_second, backoff: backoff, ttl: ttl) end |
.redis ⇒ Object
Redis access: caller-supplied pool (Limiter.configure.redis = …) wins, else fall back to the default Wurk pool. This is the same hierarchy Sidekiq Ent documents — dedicated rate-limiter pool is opt-in.
138 139 140 141 |
# File 'lib/wurk/limiter.rb', line 138 def redis(&) pool = config.pool || Wurk.redis_pool pool.with(&) end |
.reset_config! ⇒ Object
Test helper: blow away config + cached pool so a test that mutates ‘config.backoff` doesn’t leak into the next one. Not part of the public Sidekiq surface.
131 132 133 |
# File 'lib/wurk/limiter.rb', line 131 def reset_config! @config = nil end |
.unlimited(*_args, **_opts) ⇒ Object
193 194 195 |
# File 'lib/wurk/limiter.rb', line 193 def unlimited(*_args, **_opts) Unlimited.new end |
.window(name, count, interval, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL, reschedule: DEFAULT_RESCHEDULE) ⇒ Object
165 166 167 168 169 170 171 172 173 174 |
# File 'lib/wurk/limiter.rb', line 165 def window(name, count, interval, wait_timeout: DEFAULT_WAIT_TIMEOUT, backoff: nil, ttl: DEFAULT_TTL, reschedule: DEFAULT_RESCHEDULE) Window.new(name, count: count, interval: interval, wait_timeout: wait_timeout, backoff: backoff, ttl: ttl, reschedule: reschedule) end |