roundhouse_ui 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +620 -46
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +246 -12
- data/app/controllers/roundhouse_ui/application_controller.rb +74 -0
- data/app/controllers/roundhouse_ui/audit_controller.rb +1 -0
- data/app/controllers/roundhouse_ui/busy_controller.rb +25 -5
- data/app/controllers/roundhouse_ui/dashboard_controller.rb +27 -3
- data/app/controllers/roundhouse_ui/dead_controller.rb +27 -10
- data/app/controllers/roundhouse_ui/errors_controller.rb +60 -1
- data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
- data/app/controllers/roundhouse_ui/queues_controller.rb +57 -6
- data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +34 -10
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +4 -7
- data/app/controllers/roundhouse_ui/settings_controller.rb +15 -0
- data/app/controllers/roundhouse_ui/snapshots_controller.rb +5 -6
- data/app/controllers/roundhouse_ui/workers_controller.rb +2 -7
- data/app/helpers/roundhouse_ui/application_helper.rb +293 -0
- data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
- data/app/helpers/roundhouse_ui/observability_helper.rb +121 -12
- data/app/helpers/roundhouse_ui/tags_helper.rb +128 -0
- data/app/views/layouts/roundhouse_ui/application.html.erb +1277 -84
- data/app/views/roundhouse_ui/busy/index.html.erb +30 -6
- data/app/views/roundhouse_ui/dashboard/show.html.erb +62 -17
- data/app/views/roundhouse_ui/dead/index.html.erb +54 -32
- data/app/views/roundhouse_ui/errors/index.html.erb +51 -11
- data/app/views/roundhouse_ui/jobs/show.html.erb +8 -3
- data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
- data/app/views/roundhouse_ui/queues/index.html.erb +54 -10
- data/app/views/roundhouse_ui/queues/show.html.erb +55 -0
- data/app/views/roundhouse_ui/recurring/index.html.erb +71 -0
- data/app/views/roundhouse_ui/retries/index.html.erb +30 -17
- data/app/views/roundhouse_ui/scheduled/index.html.erb +17 -11
- data/app/views/roundhouse_ui/settings/show.html.erb +91 -0
- data/app/views/roundhouse_ui/shared/_filter_error.html.erb +23 -0
- data/app/views/roundhouse_ui/shared/_pager.html.erb +2 -2
- data/app/views/roundhouse_ui/shared/_search_bar.html.erb +55 -0
- data/app/views/roundhouse_ui/shared/_search_help.html.erb +37 -0
- data/app/views/roundhouse_ui/shared/_tag_filter.html.erb +28 -0
- data/app/views/roundhouse_ui/shared/bulk_preview.html.erb +69 -0
- data/config/routes.rb +9 -0
- data/lib/roundhouse_ui/backends/sidekiq.rb +124 -5
- data/lib/roundhouse_ui/backends/solid_queue.rb +54 -3
- data/lib/roundhouse_ui/demo.rb +130 -0
- data/lib/roundhouse_ui/error_groups.rb +25 -4
- data/lib/roundhouse_ui/filter_query.rb +504 -0
- data/lib/roundhouse_ui/health.rb +43 -4
- data/lib/roundhouse_ui/history.rb +54 -0
- data/lib/roundhouse_ui/icons.rb +104 -0
- data/lib/roundhouse_ui/marks/datadog-lockup-white.svg +30 -0
- data/lib/roundhouse_ui/marks/datadog-lockup.svg +41 -0
- data/lib/roundhouse_ui/observability.rb +55 -7
- data/lib/roundhouse_ui/pause.rb +6 -11
- data/lib/roundhouse_ui/queue_summary.rb +11 -0
- data/lib/roundhouse_ui/recurring.rb +137 -0
- data/lib/roundhouse_ui/runbooks.rb +75 -0
- data/lib/roundhouse_ui/snapshots.rb +13 -2
- data/lib/roundhouse_ui/tags.rb +112 -0
- data/lib/roundhouse_ui/theme.rb +298 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +235 -5
- data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
- metadata +32 -7
data/lib/roundhouse_ui.rb
CHANGED
|
@@ -7,6 +7,14 @@ require "roundhouse_ui/snapshots"
|
|
|
7
7
|
require "roundhouse_ui/observability"
|
|
8
8
|
require "roundhouse_ui/audit"
|
|
9
9
|
require "roundhouse_ui/redaction"
|
|
10
|
+
require "roundhouse_ui/queue_summary"
|
|
11
|
+
require "roundhouse_ui/theme"
|
|
12
|
+
require "roundhouse_ui/tags"
|
|
13
|
+
require "roundhouse_ui/filter_query"
|
|
14
|
+
require "roundhouse_ui/runbooks"
|
|
15
|
+
require "roundhouse_ui/icons"
|
|
16
|
+
require "roundhouse_ui/history"
|
|
17
|
+
require "roundhouse_ui/recurring"
|
|
10
18
|
require "roundhouse_ui/cancellation"
|
|
11
19
|
require "roundhouse_ui/cancel_middleware"
|
|
12
20
|
require "roundhouse_ui/metrics"
|
|
@@ -19,6 +27,14 @@ require "roundhouse_ui/backends/solid_queue"
|
|
|
19
27
|
# Brand name is "Roundhouse"; the gem and Ruby namespace are RoundhouseUi
|
|
20
28
|
# (matching the published gem name `roundhouse_ui`).
|
|
21
29
|
module RoundhouseUi
|
|
30
|
+
# A well-formed Ruby constant path, and a sane bound on its length. Used to
|
|
31
|
+
# decide what may reach String#safe_constantize — see .job_class.
|
|
32
|
+
JOB_CLASS_NAME = /\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/
|
|
33
|
+
MAX_JOB_CLASS_NAME = 200
|
|
34
|
+
|
|
35
|
+
# How many distinct warning messages .warn_once remembers before resetting.
|
|
36
|
+
WARN_MEMO_CAP = 200
|
|
37
|
+
|
|
22
38
|
class << self
|
|
23
39
|
# When true, destructive actions (purge, retry, delete, …) are disabled.
|
|
24
40
|
# Mount Roundhouse read-only where operators should only observe.
|
|
@@ -56,6 +72,39 @@ module RoundhouseUi
|
|
|
56
72
|
# e.g. RoundhouseUi.redact_args = %w[password token secret]. Default: none.
|
|
57
73
|
attr_accessor :redact_args
|
|
58
74
|
|
|
75
|
+
# Host-defined job tags, resolved at read time (see ADR 0002): a callable
|
|
76
|
+
# given the job's class name and payload, returning a Hash of tags or nil.
|
|
77
|
+
# `klass` is always the real job class (the ActiveJob adapter wrapper is
|
|
78
|
+
# unwrapped first). For the class-constant convention there's a shorthand:
|
|
79
|
+
#
|
|
80
|
+
# RoundhouseUi.job_tags = RoundhouseUi::Tags.from_constant(:OWNER, as: :squad)
|
|
81
|
+
# # equivalent to:
|
|
82
|
+
# RoundhouseUi.job_tags = ->(klass:, item:) {
|
|
83
|
+
# k = klass.safe_constantize
|
|
84
|
+
# { squad: k.const_get(:OWNER) } if k&.const_defined?(:OWNER)
|
|
85
|
+
# }
|
|
86
|
+
#
|
|
87
|
+
# Tag values render in the UI and pass through redact_args masking (by tag
|
|
88
|
+
# key). Must be cheap: by default it's memoized per class per request and
|
|
89
|
+
# called with item: nil. Default: nil (no tags anywhere).
|
|
90
|
+
attr_accessor :job_tags
|
|
91
|
+
|
|
92
|
+
# Set true when job_tags derives tags from the payload (args, tenant, …):
|
|
93
|
+
# the resolver is then called once per job with the full item, and nothing
|
|
94
|
+
# is cached — a 1,000-entry scan means 1,000 calls, so keep it fast. Leave
|
|
95
|
+
# false for class-derived tags (OWNER-style constants). Default: false.
|
|
96
|
+
attr_accessor :job_tags_per_job
|
|
97
|
+
|
|
98
|
+
# Optional declared filter vocabulary, so tag filter dropdowns are stable
|
|
99
|
+
# instead of discovered from whatever jobs happen to be visible:
|
|
100
|
+
#
|
|
101
|
+
# RoundhouseUi.tag_filters = { squad: %w[core training growth platform ops ai] }
|
|
102
|
+
#
|
|
103
|
+
# Values (or the whole setting) may be callables for dynamic vocabularies.
|
|
104
|
+
# When set, filtering by an undeclared key matches nothing (fail-closed).
|
|
105
|
+
# Default: nil — the filter UI discovers values from the entries it scans.
|
|
106
|
+
attr_accessor :tag_filters
|
|
107
|
+
|
|
59
108
|
# Opt-in: fold failures recorded by the `sidekiq-failures` gem (its `failed`
|
|
60
109
|
# sorted set) into the grouped Errors view. Off by default, and a no-op
|
|
61
110
|
# unless sidekiq-failures is loaded. Jobs with `retry: false` never enter
|
|
@@ -73,13 +122,60 @@ module RoundhouseUi
|
|
|
73
122
|
# load. Default 5s; raise it if polling shows up in your traces.
|
|
74
123
|
attr_accessor :poll_interval
|
|
75
124
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
# be enforced — set this to false to hide the pause controls and the
|
|
80
|
-
# "not enforced" warning entirely. Default: true.
|
|
125
|
+
# On OSS Sidekiq, pause is enforced only with RoundhouseUi::Fetch installed as the
|
|
126
|
+
# server's fetch strategy; the UI warns until it is. Sidekiq Pro and Solid Queue
|
|
127
|
+
# enforce it natively with no fetcher, so leave this on there. Default: true.
|
|
81
128
|
attr_accessor :pause_enabled
|
|
82
129
|
|
|
130
|
+
# Cancellation only does something if the host opted into it — either by
|
|
131
|
+
# installing RoundhouseUi::CancelMiddleware as server middleware, or by
|
|
132
|
+
# having long-running jobs poll RoundhouseUi.cancelled?(jid) themselves.
|
|
133
|
+
# With neither, cancel! writes a JID that nothing ever reads, so the Busy
|
|
134
|
+
# page's button would be inert. Default false: the button appears once you
|
|
135
|
+
# say the check exists. Also requires a backend that supports :cancel.
|
|
136
|
+
attr_accessor :cancel_enabled
|
|
137
|
+
|
|
138
|
+
# Override any of the UI's colour tokens, as pure CSS custom properties:
|
|
139
|
+
#
|
|
140
|
+
# RoundhouseUi.theme = { accent: "#FF2BD1", accent_2: "#00E5FF" }
|
|
141
|
+
# RoundhouseUi.theme = { dark: { bg: "#0A0511" }, light: { bg: "#FFF7FB" } }
|
|
142
|
+
# RoundhouseUi.theme = RoundhouseUi::Theme::PRESETS[:cyberpunk]
|
|
143
|
+
#
|
|
144
|
+
# Unset tokens keep their shipped values, so a partial theme is fine. Only
|
|
145
|
+
# known tokens are emitted and values are shape-checked — this is
|
|
146
|
+
# interpolated into a stylesheet, where escaping does not make arbitrary
|
|
147
|
+
# input safe. Default: nil.
|
|
148
|
+
attr_accessor :theme
|
|
149
|
+
|
|
150
|
+
# Named palettes a viewer can choose between on the Settings page:
|
|
151
|
+
#
|
|
152
|
+
# RoundhouseUi.themes = { cyberpunk: RoundhouseUi::Theme::PRESETS[:cyberpunk] }
|
|
153
|
+
#
|
|
154
|
+
# `theme` sets the default for everyone; this is the menu each person picks
|
|
155
|
+
# from in their own browser. Default: the shipped presets.
|
|
156
|
+
attr_accessor :themes
|
|
157
|
+
|
|
158
|
+
# How icons render. `:svg` (default) inlines them — no font, no request, no
|
|
159
|
+
# CSP change. `:font_awesome` or a Hash of `{ name => "class names" }` emits
|
|
160
|
+
# class names instead, for hosts that already ship an icon font; Roundhouse
|
|
161
|
+
# never loads one itself either way.
|
|
162
|
+
attr_accessor :icons
|
|
163
|
+
|
|
164
|
+
# Where the runbook for a job class lives. A callable, or a Hash keyed by
|
|
165
|
+
# class name:
|
|
166
|
+
#
|
|
167
|
+
# c.job_runbooks = RoundhouseUi::Runbooks.from_constant(:RUNBOOK)
|
|
168
|
+
# c.job_runbooks = { "Billing::SyncWorker" => "https://wiki/billing" }
|
|
169
|
+
#
|
|
170
|
+
# Resolved at read time like job_tags, so it applies to jobs already in the
|
|
171
|
+
# sets. Only http(s) URLs are rendered. Default: nil.
|
|
172
|
+
attr_accessor :job_runbooks
|
|
173
|
+
|
|
174
|
+
# Set false where an operator should not be able to recolour a production
|
|
175
|
+
# console. Settings then hides palette selection and everyone keeps the
|
|
176
|
+
# host's `theme`. Default: true.
|
|
177
|
+
attr_accessor :allow_theme_selection
|
|
178
|
+
|
|
83
179
|
# Configure in an initializer:
|
|
84
180
|
#
|
|
85
181
|
# RoundhouseUi.configure do |c|
|
|
@@ -98,6 +194,135 @@ module RoundhouseUi
|
|
|
98
194
|
@backend ||= Backends::Sidekiq.new
|
|
99
195
|
end
|
|
100
196
|
|
|
197
|
+
# ActiveJob-on-Sidekiq stores the adapter's JobWrapper in item["class"] and
|
|
198
|
+
# the real job class in item["wrapped"]. Solid Queue and raw Sidekiq workers
|
|
199
|
+
# put the real class in klass, and Solid Queue's synthetic item never carries
|
|
200
|
+
# a "wrapped" key — so this is a no-op there and safe to call unconditionally.
|
|
201
|
+
#
|
|
202
|
+
# Reading item["wrapped"] rather than matching on the wrapper's name also
|
|
203
|
+
# covers Sidekiq 7+/8's native Sidekiq::ActiveJob::Wrapper for free.
|
|
204
|
+
#
|
|
205
|
+
# Use for display, search, grouping and APM links, so all four agree on one
|
|
206
|
+
# string. NOT for re-enqueue: a payload pushed back to Redis must keep
|
|
207
|
+
# item["class"] exactly as Sidekiq stored it, or the job is re-created as a
|
|
208
|
+
# raw worker and fails on every attempt.
|
|
209
|
+
def unwrapped_class(klass, item)
|
|
210
|
+
wrapped = item["wrapped"] if item.is_a?(Hash)
|
|
211
|
+
(wrapped || klass)&.to_s
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# When a job was enqueued, as a Time, or nil if the payload does not say.
|
|
215
|
+
#
|
|
216
|
+
# Two formats exist three orders of magnitude apart: Sidekiq 8 writes integer
|
|
217
|
+
# epoch milliseconds, 6.5 and 7 write float epoch seconds. Reading one as the
|
|
218
|
+
# other does not give a slightly wrong time, it gives 1970 or the year 58000.
|
|
219
|
+
def enqueued_at(item)
|
|
220
|
+
raw = item["enqueued_at"] || item["created_at"] if item.is_a?(Hash)
|
|
221
|
+
return nil unless raw
|
|
222
|
+
|
|
223
|
+
raw.is_a?(Float) ? Time.at(raw) : Time.at(raw / 1000.0)
|
|
224
|
+
rescue StandardError
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Optional allowlist for constant resolution. When set, only these
|
|
229
|
+
# namespaces may be resolved from a job payload:
|
|
230
|
+
#
|
|
231
|
+
# c.job_class_namespaces = %w[Workers Jobs Billing]
|
|
232
|
+
#
|
|
233
|
+
# This is the only control here that actually restricts what can be loaded.
|
|
234
|
+
# Shape checks do not: Ruby rejects a malformed constant path before it
|
|
235
|
+
# attempts any lookup, so a name like "../../etc/passwd" never reaches
|
|
236
|
+
# autoloading in the first place — only well-formed names do, and those are
|
|
237
|
+
# exactly what a shape check permits.
|
|
238
|
+
#
|
|
239
|
+
# Default nil (no restriction), because most apps do not need it: production
|
|
240
|
+
# Rails sets `eager_load = true`, so every app constant is already loaded and
|
|
241
|
+
# resolving one executes no new file. Set it where the job payload is not
|
|
242
|
+
# fully trusted and you would rather bound the blast radius anyway.
|
|
243
|
+
attr_accessor :job_class_namespaces
|
|
244
|
+
|
|
245
|
+
# Resolve a job class name to its Class, for resolvers that read constants
|
|
246
|
+
# off the class (Tags.from_constant, Runbooks.from_constant).
|
|
247
|
+
#
|
|
248
|
+
# The name comes from the job payload — `item["class"]` or `item["wrapped"]`
|
|
249
|
+
# — which is data, not code. Three narrowings, and it is worth being precise
|
|
250
|
+
# about which of them is a security control and which are not:
|
|
251
|
+
#
|
|
252
|
+
# * The namespace allowlist, when configured, IS one. It runs before the
|
|
253
|
+
# constant lookup and is the only thing here that decides what may be
|
|
254
|
+
# resolved.
|
|
255
|
+
# * The shape and length checks are NOT. They reject names Ruby would
|
|
256
|
+
# reject anyway, before it attempts a lookup; they are here so the
|
|
257
|
+
# rejection is explicit and a pathological string is bounded early.
|
|
258
|
+
# * Requiring a Module back is robustness: a name resolving to an Array
|
|
259
|
+
# would otherwise be sent `const_defined?`, which it does not answer.
|
|
260
|
+
#
|
|
261
|
+
# What nothing at this layer can do is stop a chosen name for a class that
|
|
262
|
+
# genuinely exists — reading a constant requires loading the class. Hosts who
|
|
263
|
+
# want no constant resolution at all should supply a resolver that does none:
|
|
264
|
+
# a Hash keyed by class name serves both tags and runbooks and never
|
|
265
|
+
# constantizes.
|
|
266
|
+
def job_class(name)
|
|
267
|
+
str = name.to_s
|
|
268
|
+
return nil unless str.length <= MAX_JOB_CLASS_NAME && str.match?(JOB_CLASS_NAME)
|
|
269
|
+
return nil unless namespace_allowed?(str)
|
|
270
|
+
|
|
271
|
+
klass = str.safe_constantize
|
|
272
|
+
klass if klass.is_a?(Module)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def namespace_allowed?(str)
|
|
276
|
+
allowed = job_class_namespaces
|
|
277
|
+
return true if allowed.nil? || Array(allowed).empty?
|
|
278
|
+
|
|
279
|
+
Array(allowed).any? { |ns| str == ns.to_s || str.start_with?("#{ns}::") }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Durations, in one place. This lived in a view helper, which meant anything
|
|
283
|
+
# in lib/ that wanted to print a duration had to reinvent it — and did, five
|
|
284
|
+
# different ways, including the health signal that reported an hour-old queue
|
|
285
|
+
# as "3616s" (#31). Views reach this through the `duration` helper.
|
|
286
|
+
def duration(seconds)
|
|
287
|
+
return "—" if seconds.nil?
|
|
288
|
+
|
|
289
|
+
secs = seconds.to_f.abs
|
|
290
|
+
# Sub-minute keeps a decimal: 0.4s and 12s are a real distinction here.
|
|
291
|
+
return "#{secs.round(1)}s" if secs < 60
|
|
292
|
+
return "#{(secs / 60).floor}m #{(secs % 60).round}s" if secs < 3_600
|
|
293
|
+
return "#{(secs / 3_600).floor}h #{((secs % 3_600) / 60).round}m" if secs < 86_400
|
|
294
|
+
|
|
295
|
+
"#{(secs / 86_400).floor}d #{((secs % 86_400) / 3_600).round}h"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def duration_ms(ms)
|
|
299
|
+
return "—" if ms.nil?
|
|
300
|
+
return "#{ms.to_f.abs.round}ms" if ms.to_f.abs < 1_000
|
|
301
|
+
|
|
302
|
+
duration(ms.to_f / 1_000)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Log a resolver failure at most once per process per distinct message.
|
|
306
|
+
#
|
|
307
|
+
# It was named warn_once and warned every time. A resolver that raises raises
|
|
308
|
+
# for every entry in a scan, so one broken job_tags lambda wrote a line per
|
|
309
|
+
# job — a thousand identical lines in the log for a single page render, which
|
|
310
|
+
# is how a warning stops being read at all.
|
|
311
|
+
#
|
|
312
|
+
# Bounded by WARN_MEMO_CAP, because the message carries the class name and the
|
|
313
|
+
# exception's own text: a resolver whose message varies could otherwise grow
|
|
314
|
+
# this without limit. At the cap it resets rather than leaking or going silent.
|
|
315
|
+
def warn_once(message)
|
|
316
|
+
return unless defined?(Rails)
|
|
317
|
+
|
|
318
|
+
@warned ||= {}
|
|
319
|
+
return if @warned.key?(message)
|
|
320
|
+
|
|
321
|
+
@warned.clear if @warned.size >= WARN_MEMO_CAP
|
|
322
|
+
@warned[message] = true
|
|
323
|
+
Rails.logger&.warn("[roundhouse] #{message}")
|
|
324
|
+
end
|
|
325
|
+
|
|
101
326
|
# Cooperative cancellation check for long-running jobs:
|
|
102
327
|
# raise SomeStop if RoundhouseUi.cancelled?(jid)
|
|
103
328
|
def cancelled?(jid)
|
|
@@ -110,6 +335,11 @@ module RoundhouseUi
|
|
|
110
335
|
self.redact_args = []
|
|
111
336
|
self.show_sidekiq_failures = false
|
|
112
337
|
self.pause_enabled = true
|
|
338
|
+
self.cancel_enabled = false
|
|
113
339
|
self.poll_interval = 5
|
|
114
340
|
self.collect_durations = false
|
|
341
|
+
self.job_tags_per_job = false
|
|
342
|
+
self.allow_theme_selection = true
|
|
343
|
+
self.icons = :svg
|
|
344
|
+
self.themes = Theme::PRESETS
|
|
115
345
|
end
|
|
@@ -1,4 +1,84 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :roundhouse_ui do
|
|
4
|
+
namespace :demo do
|
|
5
|
+
desc "Enqueue demo jobs for N minutes (default 10, hard cap 20): rake roundhouse_ui:demo:load[10]"
|
|
6
|
+
task :load, [ :minutes ] => :environment do |_task, args|
|
|
7
|
+
require "roundhouse_ui/demo"
|
|
8
|
+
|
|
9
|
+
abort "refusing to run in #{Rails.env}" unless Rails.env.development?
|
|
10
|
+
|
|
11
|
+
# Ask the connection where it actually is. Reading configuration cannot
|
|
12
|
+
# answer that, and Sidekiq falls back to database 0 when nothing set it.
|
|
13
|
+
db = Sidekiq.redis { |c| c.call("CLIENT", "INFO") }.to_s[/\bdb=(\d+)/, 1]
|
|
14
|
+
abort "refusing to enqueue against Redis database 0" if db.nil? || db == "0"
|
|
15
|
+
|
|
16
|
+
# Capped, and capped in one place. A load generator you have to remember to
|
|
17
|
+
# stop is a load generator that runs all night.
|
|
18
|
+
minutes = [ (args[:minutes] || 10).to_f, 20.0 ].min
|
|
19
|
+
minutes = 1.0 if minutes <= 0
|
|
20
|
+
|
|
21
|
+
mono = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
22
|
+
started = mono.call
|
|
23
|
+
deadline = started + (minutes * 60)
|
|
24
|
+
enqueued = Hash.new(0)
|
|
25
|
+
tick = 0
|
|
26
|
+
|
|
27
|
+
puts "Enqueueing to Redis db=#{db} for #{minutes.round(1)} min " \
|
|
28
|
+
"(#{RoundhouseUi::Demo::ALL.size} worker classes). Ctrl-C to stop early."
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
while mono.call < deadline
|
|
32
|
+
tick += 1
|
|
33
|
+
# A rate that moves, so the dashboard's throughput line and the drain
|
|
34
|
+
# forecast have something to say. Peaks roughly every two minutes.
|
|
35
|
+
phase = Math.sin(tick / 12.0)
|
|
36
|
+
batch = (6 + (phase.abs * 18)).round
|
|
37
|
+
|
|
38
|
+
batch.times do
|
|
39
|
+
worker = RoundhouseUi::Demo::ALL.sample
|
|
40
|
+
worker.set(queue: worker::QUEUE).perform_async(
|
|
41
|
+
"account_id" => 40_000 + rand(600),
|
|
42
|
+
"user_id" => 900_000 + rand(9_000),
|
|
43
|
+
# Deliberately named to be masked by redact_args in the UI.
|
|
44
|
+
"api_token" => "sk_live_#{SecureRandom.hex(8)}"
|
|
45
|
+
)
|
|
46
|
+
enqueued[worker.name.split("::").last] += 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if (tick % 10).zero?
|
|
50
|
+
left = ((deadline - mono.call) / 60).round(1)
|
|
51
|
+
puts " +#{enqueued.values.sum} enqueued, #{left} min left"
|
|
52
|
+
end
|
|
53
|
+
sleep 1.5
|
|
54
|
+
end
|
|
55
|
+
rescue Interrupt
|
|
56
|
+
puts "\n stopped early"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts "\nDone after #{((mono.call - started) / 60).round(1)} min:"
|
|
60
|
+
enqueued.sort_by { |_k, v| -v }.each { |k, v| puts format(" %-22s %5d", k, v) }
|
|
61
|
+
puts format(" %-22s %5d", "TOTAL", enqueued.values.sum)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc "Remove everything the demo seeded or enqueued"
|
|
65
|
+
task clean: :environment do
|
|
66
|
+
require "roundhouse_ui/demo"
|
|
67
|
+
abort "refusing to run in #{Rails.env}" unless Rails.env.development?
|
|
68
|
+
removed = 0
|
|
69
|
+
%w[retry dead schedule].each do |set|
|
|
70
|
+
Sidekiq.redis do |conn|
|
|
71
|
+
conn.call("ZRANGE", set, "0", "-1").each do |raw|
|
|
72
|
+
job = JSON.parse(raw) rescue next
|
|
73
|
+
next unless job["rh_demo"] || job["class"].to_s.start_with?("RoundhouseUi::Demo::")
|
|
74
|
+
|
|
75
|
+
conn.call("ZREM", set, raw)
|
|
76
|
+
removed += 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
RoundhouseUi::Demo::ALL.each { |w| Sidekiq::Queue.new(w::QUEUE).clear }
|
|
81
|
+
puts "removed #{removed} demo entries from the retry/dead/scheduled sets"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: roundhouse_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- R.J. Robinson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -38,11 +38,12 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '6.5'
|
|
41
|
-
description: '
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
description: 'Mountable Rails engine for Sidekiq or Solid Queue: grouped errors, argument
|
|
42
|
+
search, filter-scoped bulk actions, enforced pause, snapshots, audit log. Mounts
|
|
43
|
+
alongside or instead of Sidekiq::Web. Not affiliated with or endorsed by Contributed
|
|
44
|
+
Systems LLC; Sidekiq, Sidekiq Pro and Sidekiq Enterprise are their trademarks.'
|
|
44
45
|
email:
|
|
45
|
-
-
|
|
46
|
+
- robertj.robinson@gmail.com
|
|
46
47
|
executables: []
|
|
47
48
|
extensions: []
|
|
48
49
|
extra_rdoc_files: []
|
|
@@ -64,14 +65,17 @@ files:
|
|
|
64
65
|
- app/controllers/roundhouse_ui/jobs_controller.rb
|
|
65
66
|
- app/controllers/roundhouse_ui/metrics_controller.rb
|
|
66
67
|
- app/controllers/roundhouse_ui/queues_controller.rb
|
|
68
|
+
- app/controllers/roundhouse_ui/recurring_controller.rb
|
|
67
69
|
- app/controllers/roundhouse_ui/redis_controller.rb
|
|
68
70
|
- app/controllers/roundhouse_ui/retries_controller.rb
|
|
69
71
|
- app/controllers/roundhouse_ui/scheduled_controller.rb
|
|
72
|
+
- app/controllers/roundhouse_ui/settings_controller.rb
|
|
70
73
|
- app/controllers/roundhouse_ui/snapshots_controller.rb
|
|
71
74
|
- app/controllers/roundhouse_ui/workers_controller.rb
|
|
72
75
|
- app/helpers/roundhouse_ui/application_helper.rb
|
|
73
76
|
- app/helpers/roundhouse_ui/nav_helper.rb
|
|
74
77
|
- app/helpers/roundhouse_ui/observability_helper.rb
|
|
78
|
+
- app/helpers/roundhouse_ui/tags_helper.rb
|
|
75
79
|
- app/views/layouts/roundhouse_ui/application.html.erb
|
|
76
80
|
- app/views/roundhouse_ui/audit/index.html.erb
|
|
77
81
|
- app/views/roundhouse_ui/busy/index.html.erb
|
|
@@ -85,10 +89,18 @@ files:
|
|
|
85
89
|
- app/views/roundhouse_ui/jobs/show.html.erb
|
|
86
90
|
- app/views/roundhouse_ui/metrics/show.html.erb
|
|
87
91
|
- app/views/roundhouse_ui/queues/index.html.erb
|
|
92
|
+
- app/views/roundhouse_ui/queues/show.html.erb
|
|
93
|
+
- app/views/roundhouse_ui/recurring/index.html.erb
|
|
88
94
|
- app/views/roundhouse_ui/redis/show.html.erb
|
|
89
95
|
- app/views/roundhouse_ui/retries/index.html.erb
|
|
90
96
|
- app/views/roundhouse_ui/scheduled/index.html.erb
|
|
97
|
+
- app/views/roundhouse_ui/settings/show.html.erb
|
|
98
|
+
- app/views/roundhouse_ui/shared/_filter_error.html.erb
|
|
91
99
|
- app/views/roundhouse_ui/shared/_pager.html.erb
|
|
100
|
+
- app/views/roundhouse_ui/shared/_search_bar.html.erb
|
|
101
|
+
- app/views/roundhouse_ui/shared/_search_help.html.erb
|
|
102
|
+
- app/views/roundhouse_ui/shared/_tag_filter.html.erb
|
|
103
|
+
- app/views/roundhouse_ui/shared/bulk_preview.html.erb
|
|
92
104
|
- app/views/roundhouse_ui/snapshots/index.html.erb
|
|
93
105
|
- app/views/roundhouse_ui/workers/index.html.erb
|
|
94
106
|
- config/routes.rb
|
|
@@ -98,16 +110,27 @@ files:
|
|
|
98
110
|
- lib/roundhouse_ui/backends/solid_queue.rb
|
|
99
111
|
- lib/roundhouse_ui/cancel_middleware.rb
|
|
100
112
|
- lib/roundhouse_ui/cancellation.rb
|
|
113
|
+
- lib/roundhouse_ui/demo.rb
|
|
101
114
|
- lib/roundhouse_ui/duration_collector.rb
|
|
102
115
|
- lib/roundhouse_ui/engine.rb
|
|
103
116
|
- lib/roundhouse_ui/error_groups.rb
|
|
104
117
|
- lib/roundhouse_ui/fetch.rb
|
|
118
|
+
- lib/roundhouse_ui/filter_query.rb
|
|
105
119
|
- lib/roundhouse_ui/health.rb
|
|
120
|
+
- lib/roundhouse_ui/history.rb
|
|
121
|
+
- lib/roundhouse_ui/icons.rb
|
|
122
|
+
- lib/roundhouse_ui/marks/datadog-lockup-white.svg
|
|
123
|
+
- lib/roundhouse_ui/marks/datadog-lockup.svg
|
|
106
124
|
- lib/roundhouse_ui/metrics.rb
|
|
107
125
|
- lib/roundhouse_ui/observability.rb
|
|
108
126
|
- lib/roundhouse_ui/pause.rb
|
|
127
|
+
- lib/roundhouse_ui/queue_summary.rb
|
|
128
|
+
- lib/roundhouse_ui/recurring.rb
|
|
109
129
|
- lib/roundhouse_ui/redaction.rb
|
|
130
|
+
- lib/roundhouse_ui/runbooks.rb
|
|
110
131
|
- lib/roundhouse_ui/snapshots.rb
|
|
132
|
+
- lib/roundhouse_ui/tags.rb
|
|
133
|
+
- lib/roundhouse_ui/theme.rb
|
|
111
134
|
- lib/roundhouse_ui/version.rb
|
|
112
135
|
- lib/tasks/roundhouse_ui_tasks.rake
|
|
113
136
|
homepage: https://github.com/rjrobinson/roundhouse_ui
|
|
@@ -116,6 +139,8 @@ licenses:
|
|
|
116
139
|
metadata:
|
|
117
140
|
source_code_uri: https://github.com/rjrobinson/roundhouse_ui
|
|
118
141
|
changelog_uri: https://github.com/rjrobinson/roundhouse_ui/blob/main/CHANGELOG.md
|
|
142
|
+
bug_tracker_uri: https://github.com/rjrobinson/roundhouse_ui/issues
|
|
143
|
+
documentation_uri: https://github.com/rjrobinson/roundhouse_ui#readme
|
|
119
144
|
rubygems_mfa_required: 'true'
|
|
120
145
|
post_install_message:
|
|
121
146
|
rdoc_options: []
|
|
@@ -135,5 +160,5 @@ requirements: []
|
|
|
135
160
|
rubygems_version: 3.5.22
|
|
136
161
|
signing_key:
|
|
137
162
|
specification_version: 4
|
|
138
|
-
summary: Roundhouse — a
|
|
163
|
+
summary: Roundhouse — a real-time ops UI for Sidekiq and Solid Queue.
|
|
139
164
|
test_files: []
|