Module: Wurk::Web::Enterprise::Limits

Defined in:
lib/wurk/web/enterprise.rb

Overview

Limits tab — list every registered limiter, filter by name, expose reset + delete. List/metrics already render via Wurk::Limiter::Base; this wrapper adds the name filter documented in §1.2.0+.

Constant Summary collapse

SWEEP_BATCH =

Names per sweep round trip. Bounds both halves: the probe's pipeline (whose replies the client buffers) and the removal script, which is atomic — an unbatched pass over a set that leaked for months would block every other Redis client for its whole length. A healthy set fits in one slice, so the common case still costs one probe.

512

Class Method Summary collapse

Class Method Details

.list(filter: nil) ⇒ Array<String>

Listing is also the sweep. lmtr-list membership has no TTL of its own while lmtr:<name> expires with the limiter's ttl, so every interpolated name (stripe-#{user_id}) the spec blesses would leave a permanent member behind. Names whose metadata is gone are dropped from the SET and from the result, so the dashboard also stops rendering a row with nothing behind it. The liveness probe rides along with the SMEMBERS the listing already pays for — both are O(set) — and once the sweep has run the set is back down to live limiters.

Returns:

  • (Array<String>)

    live limiter names, optionally filtered to those whose name contains the case-insensitive substring filter.



43
44
45
46
47
48
49
# File 'lib/wurk/web/enterprise.rb', line 43

def list(filter: nil)
  names = sweep(Wurk.redis(idempotent: true) { |c| c.call('SMEMBERS', Wurk::Limiter::LIST_KEY) }).sort
  return names if filter.nil? || filter.to_s.empty?

  needle = filter.to_s.downcase
  names.select { |n| n.downcase.include?(needle) }
end

.meta_key(name) ⇒ Object



87
88
89
# File 'lib/wurk/web/enterprise.rb', line 87

def meta_key(name)
  "lmtr:#{name}"
end

.metadata(name) ⇒ Object



82
83
84
85
# File 'lib/wurk/web/enterprise.rb', line 82

def (name)
  raw = Wurk.redis(idempotent: true) { |c| c.call('HGETALL', meta_key(name)) }
  raw.is_a?(Array) ? raw.each_slice(2).to_h : raw
end

.partition_live(names) ⇒ Object



62
63
64
65
66
67
# File 'lib/wurk/web/enterprise.rb', line 62

def partition_live(names)
  flags = Wurk.redis(idempotent: true) do |c|
    c.pipelined { |pipe| names.each { |n| pipe.call('EXISTS', meta_key(n)) } }
  end
  names.partition.with_index { |_, i| flags[i].to_i == 1 }
end

.rebuild(name) ⇒ Object

Read-only reconstruction (register: false) from the persisted metadata. Returns nil when the meta HASH is gone or malformed — the name a caller holds came from a page list rendered earlier, and the metadata can expire between the two.



108
109
110
111
112
113
114
115
116
# File 'lib/wurk/web/enterprise.rb', line 108

def rebuild(name)
  meta = (name)
  return nil if meta.nil? || meta.empty?

  options = meta['options'] ? ::JSON.parse(meta['options']) : {}
  Wurk::Limiter.build(name, meta['type'], options)
rescue StandardError
  nil
end

.reset(name) ⇒ Object

Stats-key reset: drops every lmtr-stats: / state key for the named limiter while keeping its metadata + LIST membership so the row remains in the UI. Mirrors the §1.5 #reset surface — the name is wire-compat, so the trailing-? rule doesn't apply here.



95
96
97
98
99
100
101
102
# File 'lib/wurk/web/enterprise.rb', line 95

def reset(name) # rubocop:disable Naming/PredicateMethod
  limiter = rebuild(name)
  # Reconstruct the limiter and delegate: only the type itself knows
  # its state keys. A bucket's counter lives at `lmtr-b:<name>:<epoch>`,
  # so the bare-prefix DEL below never touched it.
  limiter ? limiter.reset : reset_by_prefix(name)
  true
end

.reset_by_prefix(name) ⇒ Object

Metadata-less fallback: wipe every type's un-suffixed state key.



119
120
121
122
123
124
# File 'lib/wurk/web/enterprise.rb', line 119

def reset_by_prefix(name)
  Wurk.redis do |c|
    %W[lmtr-cs:#{name} lmtr-w:#{name} lmtr-l:#{name}
       lmtr-p:#{name} lmtr-stats:#{name}].each { |k| c.call('DEL', k) }
  end
end

.sweep(names) ⇒ Object

Drop the names whose metadata is gone, return the ones still live.



52
53
54
# File 'lib/wurk/web/enterprise.rb', line 52

def sweep(names)
  names.each_slice(SWEEP_BATCH).flat_map { |slice| sweep_slice(slice) }
end

.sweep_batch(names) ⇒ Object

Claims apply-safety, unlike ProcessSet's comparable prune: the script re-decides liveness inside its own atomic step, so a replay can only remove names that are still dead.



72
73
74
75
76
77
78
79
80
# File 'lib/wurk/web/enterprise.rb', line 72

def sweep_batch(names)
  Wurk.redis(idempotent: true) do |c|
    Wurk::Lua::Loader.eval_cached(
      c, :limiter_list_sweep,
      keys: [Wurk::Limiter::LIST_KEY],
      argv: ['lmtr:', *names]
    )
  end
end

.sweep_slice(names) ⇒ Object



56
57
58
59
60
# File 'lib/wurk/web/enterprise.rb', line 56

def sweep_slice(names)
  live, dead = partition_live(names)
  sweep_batch(dead) unless dead.empty?
  live
end