Class: Wurk::Limiter::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/wurk/limiter/base.rb

Overview

Shared base for every limiter type: name / options / reset / delete / fingerprint and the metadata registration. Each subclass supplies the rest of the §1.5 introspection contract — type, size, status, within_limit(...) { ... } — plus the state_keys reset/delete clear.

status is uniform across types (#16): { used:, limit:, reset_at:, available? }. Subclasses supply the three values via build_status; Concurrent additionally merges its metric counters.

Direct Known Subclasses

Bucket, Concurrent, Leaky, Points, Window

Constant Summary collapse

MIN_TTL =
86_400

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, register: true, **options) ⇒ Base

register: defaults true so constructing a limiter publishes its metadata + lmtr-list membership. The dashboard reconstructs limiters purely to read status on a GET, so it passes register: false to keep introspection side-effect-free.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/wurk/limiter/base.rb', line 26

def initialize(name, register: true, **options)
  unless name.is_a?(String) && NAME_PATTERN.match?(name)
    raise ArgumentError, "limiter name must match #{NAME_PATTERN.inspect} (got #{name.inspect})"
  end

  # Spec §1.2: ttl floor of 24h. Anything tighter risks losing the
  # metadata hash mid-job and orphaning slots that read it. Raised to
  # the floor rather than rejected: Sidekiq Ent accepts any ttl, so an
  # app passing a short one must keep booting after the gem swap.
  ttl = [options[:ttl] || DEFAULT_TTL, MIN_TTL].max

  @name = name.dup.freeze
  @options = options.merge(ttl: ttl)
  @register_pending = false
  register_or_defer if register
end

Instance Attribute Details

#name ⇒ Object (readonly)

Returns the value of attribute name.



20
21
22
# File 'lib/wurk/limiter/base.rb', line 20

def name
  @name
end

#options ⇒ Object (readonly)

Returns the value of attribute options.



20
21
22
# File 'lib/wurk/limiter/base.rb', line 20

def options
  @options
end

Instance Method Details

#build_status(used:, limit:, reset_at:) ⇒ Object (protected)

Assemble the uniform status hash. available? is derived: an unlimited (nil) limit is always available; otherwise headroom remains while used < limit. reset_at is an epoch-seconds Float (or nil when the type has no clock-driven reset).



70
71
72
73
# File 'lib/wurk/limiter/base.rb', line 70

def build_status(used:, limit:, reset_at:)
  { used: used, limit: limit, reset_at: reset_at,
    available?: limit.nil? || used < limit }
end

#delete ⇒ Object



49
50
51
52
53
54
# File 'lib/wurk/limiter/base.rb', line 49

def delete
  Wurk::Limiter.redis do |c|
    (state_keys + [meta_key]).each { |k| c.call('DEL', k) }
    c.call('SREM', LIST_KEY, @name)
  end
end

#fingerprint ⇒ Object

Stable fingerprint for the limiter — Sidekiq 8.0+ switched from SHA1 → SHA256 (~10% larger encoding). Web UI groups limiters by this so that interpolated names (stripe-#{user_id}) get a single row per shape.



60
61
62
# File 'lib/wurk/limiter/base.rb', line 60

def fingerprint
  @fingerprint ||= Digest::SHA256.hexdigest("#{type}|#{@name}|#{JSON.dump(serializable_options)}")
end

#lua(name, keys:, argv:) ⇒ Object (protected)



122
123
124
125
126
127
128
129
130
# File 'lib/wurk/limiter/base.rb', line 122

def lua(name, keys:, argv:)
  if @register_pending
    @register_pending = false
    register_or_defer
  end
  Wurk::Limiter.redis do |c|
    Wurk::Lua::Loader.eval_cached(c, name, keys: keys, argv: argv)
  end
end

#meta_key ⇒ Object (protected)



75
76
77
# File 'lib/wurk/limiter/base.rb', line 75

def meta_key
  "lmtr:#{@name}"
end

#random_id ⇒ Object (protected)

Stable random slot id (Concurrent) / nonce (Window). 16 hex chars

8 bytes — wide enough to avoid collision under any realistic

burst and short enough to keep ZSET memory bounded.



118
119
120
# File 'lib/wurk/limiter/base.rb', line 118

def random_id
  SecureRandom.hex(8)
end

#register! ⇒ Object (protected)

Metadata HSET + EXPIRE + lmtr-list SADD in a single round trip. The script also gates the SADD on the metadata being newly written, so a per-job construction (stripe-#{user_id}) stops rewriting a set that grows one member per name — see limiter_register.lua for why that gate is only safe while it is atomic with the HSET.



94
95
96
97
98
# File 'lib/wurk/limiter/base.rb', line 94

def register!
  lua(:limiter_register,
      keys: [meta_key, LIST_KEY],
      argv: [@name, type.to_s, JSON.dump(serializable_options), fingerprint, ttl])
end

#register_or_defer ⇒ Object (protected)

Limiters are commonly built at class load, and a Redis that is LOADING, out of memory or briefly gone must not turn require into a process exit. The metadata is only what the dashboard lists; the limit itself lives in the limiter's own keys, so the next operation can finish the job.



104
105
106
107
108
109
# File 'lib/wurk/limiter/base.rb', line 104

def register_or_defer
  register!
rescue ::RedisClient::Error, ::ConnectionPool::TimeoutError => e
  @register_pending = true
  Wurk.logger.warn { "Wurk limiter #{@name.inspect} not registered yet (#{e.class}: #{e.message}); will retry" }
end

#reset ⇒ Object



43
44
45
46
47
# File 'lib/wurk/limiter/base.rb', line 43

def reset
  Wurk::Limiter.redis do |c|
    state_keys.each { |k| c.call('DEL', k) }
  end
end

#serializable_options ⇒ Object (protected)



79
80
81
82
83
84
85
86
87
# File 'lib/wurk/limiter/base.rb', line 79

def serializable_options
  @options.transform_values do |v|
    case v
    when Proc then '<proc>'
    when Symbol, Numeric, String, true, false, nil then v
    else v.to_s
    end
  end
end

#ttl ⇒ Object (protected)



111
112
113
# File 'lib/wurk/limiter/base.rb', line 111

def ttl
  @options[:ttl]
end