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- PROBE_INTERVAL =
Liveness probes per process, at most one per this many seconds.
60.0
Class Method Summary collapse
- .filtered(names, filter) ⇒ Object
-
.known_dead(members, sweep) ⇒ Object
Names the last probe of this pool found dead, probing (and sweeping) again once PROBE_INTERVAL has passed.
-
.list(filter: nil, sweep: !Wurk::Web.config.read_only?)) ⇒ Array<String>
Listing is also the sweep.
- .live(names) ⇒ Object
- .meta_key(name) ⇒ Object
- .metadata(name) ⇒ Object
- .metadata_rows(names) ⇒ Object
-
.page(filter:, offset:, count:, sweep: !Wurk::Web.config.read_only?)) ⇒ Array(Integer, Array<Array(String, Hash)>)
The dashboard's listing:
countlive limiters fromoffsetof the sorted, filtered names, each[name, metadata], and how many names there are. - .partition_live(names) ⇒ Object
-
.rebuild(name) ⇒ Object
Read-only reconstruction (
register: false) from the persisted metadata. -
.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. -
.reset_by_prefix(name) ⇒ Object
Metadata-less fallback: wipe every type's un-suffixed state key.
-
.reset_probe! ⇒ Object
Forgets the last probe, so the next #page probes.
-
.sweep(names) ⇒ Object
Drop the names whose metadata is gone, return the ones still live.
-
.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.
- .sweep_slice(names) ⇒ Object
Class Method Details
.filtered(names, filter) ⇒ Object
112 113 114 115 116 117 |
# File 'lib/wurk/web/enterprise.rb', line 112 def filtered(names, filter) return names if filter.nil? || filter.to_s.empty? needle = filter.to_s.downcase names.select { |n| n.downcase.include?(needle) } end |
.known_dead(members, sweep) ⇒ Object
Names the last probe of this pool found dead, probing (and sweeping) again once PROBE_INTERVAL has passed.
121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/wurk/web/enterprise.rb', line 121 def known_dead(members, sweep) pool = Wurk.redis_pool now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) @probe_lock.synchronize do probe = @probe return probe[:dead] if probe && probe[:pool].equal?(pool) && now - probe[:at] < PROBE_INTERVAL dead = members - (sweep ? sweep(members) : live(members)) @probe = { pool: pool, at: now, dead: dead } dead end end |
.list(filter: nil, sweep: !Wurk::Web.config.read_only?)) ⇒ 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.
A read-only dashboard is a viewer, so it filters the dead names out of what it returns but leaves the SET alone: GET must not write.
44 45 46 47 |
# File 'lib/wurk/web/enterprise.rb', line 44 def list(filter: nil, sweep: !Wurk::Web.config.read_only?) members = Wurk.redis(idempotent: true) { |c| c.call('SMEMBERS', Wurk::Limiter::LIST_KEY) } filtered((sweep ? sweep(members) : live(members)).sort, filter) end |
.live(names) ⇒ Object
54 55 56 |
# File 'lib/wurk/web/enterprise.rb', line 54 def live(names) names.each_slice(SWEEP_BATCH).flat_map { |slice| partition_live(slice).first } end |
.meta_key(name) ⇒ Object
151 152 153 |
# File 'lib/wurk/web/enterprise.rb', line 151 def (name) "lmtr:#{name}" end |
.metadata(name) ⇒ Object
84 85 86 87 |
# File 'lib/wurk/web/enterprise.rb', line 84 def (name) raw = Wurk.redis(idempotent: true) { |c| c.call('HGETALL', (name)) } raw.is_a?(Array) ? raw.each_slice(2).to_h : raw end |
.metadata_rows(names) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/wurk/web/enterprise.rb', line 134 def (names) return [] if names.empty? raws = Wurk.redis(idempotent: true) do |c| c.pipelined { |pipe| names.each { |n| pipe.call('HGETALL', (n)) } } end names.zip(raws).filter_map do |name, raw| = raw.is_a?(Array) ? raw.each_slice(2).to_h : raw [name, ] unless .empty? end end |
.page(filter:, offset:, count:, sweep: !Wurk::Web.config.read_only?)) ⇒ Array(Integer, Array<Array(String, Hash)>)
The dashboard's listing: count live limiters from offset of the
sorted, filtered names, each [name, metadata], and how many names
there are. #list probes every member's metadata (an EXISTS each) on
every call; here that probe — and, with sweep, the removal of the
dead — runs at most once per PROBE_INTERVAL per process, and pages in
between pay one SMEMBERS and one pipelined HGETALL for their rows.
The cost of that: a limiter whose metadata expired since the last
probe counts in total and is left off its page, and a name the
probe found dead and that registers again stays hidden until the
next probe.
106 107 108 109 110 |
# File 'lib/wurk/web/enterprise.rb', line 106 def page(filter:, offset:, count:, sweep: !Wurk::Web.config.read_only?) members = Wurk.redis(idempotent: true) { |c| c.call('SMEMBERS', Wurk::Limiter::LIST_KEY) } names = filtered((members - known_dead(members, sweep)).sort, filter) [names.size, (names[offset, count] || [])] end |
.partition_live(names) ⇒ Object
64 65 66 67 68 69 |
# File 'lib/wurk/web/enterprise.rb', line 64 def partition_live(names) flags = Wurk.redis(idempotent: true) do |c| c.pipelined { |pipe| names.each { |n| pipe.call('EXISTS', (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.
172 173 174 175 176 177 178 179 180 |
# File 'lib/wurk/web/enterprise.rb', line 172 def rebuild(name) = (name) return nil if .nil? || .empty? = ['options'] ? ::JSON.parse(['options']) : {} Wurk::Limiter.build(name, ['type'], ) 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.
159 160 161 162 163 164 165 166 |
# File 'lib/wurk/web/enterprise.rb', line 159 def reset(name) 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.
183 184 185 186 187 188 |
# File 'lib/wurk/web/enterprise.rb', line 183 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 |
.reset_probe! ⇒ Object
Forgets the last probe, so the next #page probes. For tests.
147 148 149 |
# File 'lib/wurk/web/enterprise.rb', line 147 def reset_probe! @probe_lock.synchronize { @probe = nil } end |
.sweep(names) ⇒ Object
Drop the names whose metadata is gone, return the ones still live.
50 51 52 |
# File 'lib/wurk/web/enterprise.rb', line 50 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.
74 75 76 77 78 79 80 81 82 |
# File 'lib/wurk/web/enterprise.rb', line 74 def sweep_batch(names) Wurk.redis(idempotent: true) do |c| Wurk::Lua::Loader.eval_cached( c, :limiter_list_sweep, keys: [Wurk::Limiter::LIST_KEY, *names.map { |n| (n) }], argv: names ) end end |
.sweep_slice(names) ⇒ Object
58 59 60 61 62 |
# File 'lib/wurk/web/enterprise.rb', line 58 def sweep_slice(names) live, dead = partition_live(names) sweep_batch(dead) unless dead.empty? live end |