roundhouse_ui 0.10.0 → 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 +563 -50
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +190 -23
- data/app/controllers/roundhouse_ui/application_controller.rb +73 -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 +23 -4
- data/app/controllers/roundhouse_ui/dead_controller.rb +26 -12
- data/app/controllers/roundhouse_ui/errors_controller.rb +30 -8
- data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
- data/app/controllers/roundhouse_ui/queues_controller.rb +56 -7
- data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +33 -12
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +3 -8
- 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 +253 -10
- data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
- data/app/helpers/roundhouse_ui/observability_helper.rb +113 -27
- data/app/helpers/roundhouse_ui/tags_helper.rb +30 -3
- data/app/views/layouts/roundhouse_ui/application.html.erb +1181 -92
- 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 +23 -15
- data/app/views/roundhouse_ui/errors/index.html.erb +28 -9
- data/app/views/roundhouse_ui/jobs/show.html.erb +7 -3
- data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
- data/app/views/roundhouse_ui/queues/index.html.erb +53 -13
- 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 +17 -15
- data/app/views/roundhouse_ui/scheduled/index.html.erb +10 -10
- 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 +2 -4
- 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 +13 -3
- 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 +8 -11
- data/lib/roundhouse_ui/theme.rb +298 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +200 -5
- data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
- metadata +29 -7
data/README.md
CHANGED
|
@@ -1,37 +1,128 @@
|
|
|
1
1
|
# Roundhouse
|
|
2
|
-
<img width="
|
|
3
|
-
|
|
2
|
+
<img width="4446" height="3022" alt="CleanShot 2026-08-27 at 07 43 42@2x" src="https://github.com/user-attachments/assets/cc737416-bf90-4303-85a9-0dcd1316939d" />
|
|
3
|
+
|
|
4
|
+
<!-- TODO: demo GIF -->
|
|
4
5
|
|
|
5
6
|
[](https://github.com/rjrobinson/roundhouse_ui/actions/workflows/ci.yml)
|
|
6
7
|
[](https://rubygems.org/gems/roundhouse_ui)
|
|
7
8
|
[](https://www.ruby-lang.org)
|
|
8
9
|
[](https://rubyonrails.org)
|
|
9
10
|
[](MIT-LICENSE)
|
|
11
|
+
[](https://buymeacoffee.com/rjrobinson)
|
|
10
12
|
|
|
11
|
-
Roundhouse is a
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
reads through a **backend port**, so the same UI drives **Sidekiq** or **Solid
|
|
15
|
-
Queue** (see [Backends](#backends)). All server-rendered with Turbo — **no build
|
|
16
|
-
step, no frontend dependency** — and **no Sidekiq Pro required**.
|
|
13
|
+
Roundhouse is a real-time ops UI for Sidekiq and Solid Queue — grouped errors,
|
|
14
|
+
argument search, bulk actions on a filter, enforced pause, snapshots, and an
|
|
15
|
+
audit log — in one mountable engine with no build step.
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
It works on OSS Sidekiq, and it works better on [Sidekiq Pro and
|
|
18
|
+
Enterprise](#buy-sidekiq-pro-and-enterprise) — which you should buy.
|
|
19
|
+
|
|
20
|
+
## Buy Sidekiq Pro and Enterprise
|
|
21
|
+
|
|
22
|
+
[**Sidekiq Pro and Enterprise**](https://sidekiq.org/products/pro.html) are worth the
|
|
23
|
+
money. Buy them.
|
|
24
|
+
|
|
25
|
+
Sidekiq is the reason any of this exists, and the commercial tiers are what keep it
|
|
26
|
+
maintained. Pro gives you reliable fetch (jobs survive a hard crash), batches, expiring
|
|
27
|
+
jobs and native queue pause; Enterprise adds rate limiting, unique jobs, periodic jobs,
|
|
28
|
+
multi-process and historical metrics. Roundhouse detects all of it and gets better when
|
|
29
|
+
it is there — native pause with no fetch strategy to install, Enterprise periodic jobs
|
|
30
|
+
on the Recurring page.
|
|
31
|
+
|
|
32
|
+
Roundhouse is not a way to avoid paying for Sidekiq. It is a UI. If you are running
|
|
33
|
+
Sidekiq seriously enough to want this, you are running it seriously enough to buy Pro.
|
|
34
|
+
|
|
35
|
+
> Roundhouse is not affiliated with or endorsed by Contributed Systems LLC. Sidekiq,
|
|
36
|
+
> Sidekiq Pro and Sidekiq Enterprise are their trademarks.
|
|
37
|
+
|
|
38
|
+
## Support this project
|
|
39
|
+
|
|
40
|
+
If Roundhouse saved you an incident, [buy me a coffee](https://buymeacoffee.com/rjrobinson)
|
|
41
|
+
— buy Sidekiq Pro first.
|
|
19
42
|
|
|
20
|
-
|
|
43
|
+
I'd rather hear the story, though. Tell me what broke and what you were trying to find
|
|
44
|
+
out: [@_AwesomeRob](https://x.com/_AwesomeRob) on X, or open an
|
|
45
|
+
[issue](https://github.com/rjrobinson/roundhouse_ui/issues).
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
- **Grouped errors** — failures fingerprinted by `class + error`, so one bad deploy is a single issue with a count, not thousands of rows.
|
|
24
|
-
- **Smart bulk actions** — retry/delete every job matching a filter (not just the visible page), plus select-and-act on Dead.
|
|
25
|
-
- **Search** — across the dead/retry/scheduled sets by class, JID, error, or argument value.
|
|
26
|
-
- **Queue management** — pause/resume, purge with an impact count, and **snapshot → restore**.
|
|
27
|
-
- **Job inspection & editing** — full args (with redaction), error, and collapsible backtrace; edit & re-enqueue, or enqueue a new job (opt-in).
|
|
28
|
-
- **Per-class durations** (opt-in) — the slowest job classes, which Sidekiq doesn't track.
|
|
29
|
-
- **Audit log** — every state-changing action recorded and attributable.
|
|
30
|
-
- **⌘K command palette**, light/dark themes, compact/full-width toggle, read-only mode, and a strict self-contained CSP.
|
|
47
|
+
## Why
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
I wrote this during an incident where I needed to know which jobs for one
|
|
50
|
+
customer had failed, and whether I could retry only those. Sidekiq::Web gave me
|
|
51
|
+
a retry set of forty thousand rows, twenty-five at a time, with no search. So I
|
|
52
|
+
opened a Rails console at 2am and started writing `Sidekiq::RetrySet.new.select`
|
|
53
|
+
against production — which is not where anyone should be deciding what to retry.
|
|
33
54
|
|
|
34
|
-
|
|
55
|
+
Roundhouse answers that question in the browser, and records who answered it.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Gemfile
|
|
61
|
+
gem "roundhouse_ui"
|
|
62
|
+
|
|
63
|
+
# config/routes.rb — mount behind your own auth; Roundhouse ships none
|
|
64
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
65
|
+
mount RoundhouseUi::Engine => "/roundhouse"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# config/initializers/roundhouse.rb — only if you're on Solid Queue
|
|
69
|
+
RoundhouseUi.backend = RoundhouseUi::Backends::SolidQueue.new
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## What you get
|
|
73
|
+
|
|
74
|
+
- **Grouped errors** — failures fingerprinted by class + error, so one bad deploy is one row with a count, not thousands.
|
|
75
|
+
- **One filter bar** — `class=BillingWorker error=Timeout::Error stripe` in a single box. Facets match exactly, `%` wildcards, free text searches class, JID, error and redacted arguments. The whole filter is one `?q=` parameter, so a filtered view is a URL you can bookmark and share.
|
|
76
|
+
- **Bulk retry or delete scoped to a filter** — every job matching your search, not just the page you can see.
|
|
77
|
+
- **Enforced pause** — a paused queue actually stops being worked, on OSS Sidekiq too.
|
|
78
|
+
- **Snapshot → restore** — back a queue up before you purge it, and put it back if you were wrong.
|
|
79
|
+
- **Audit log** — every state-changing action, with who did it.
|
|
80
|
+
|
|
81
|
+
The same UI drives **Sidekiq** or **Solid Queue** — see [Backends](#backends);
|
|
82
|
+
running both at once is [#17](https://github.com/rjrobinson/roundhouse_ui/issues/17).
|
|
83
|
+
|
|
84
|
+
Roundhouse ships no authentication, so mount it behind yours. `read_only`
|
|
85
|
+
disables every mutating action, `redact_args` masks sensitive arguments, and
|
|
86
|
+
`job_class_namespaces` bounds which constants a job payload can make Roundhouse resolve —
|
|
87
|
+
see [Security](#security).
|
|
88
|
+
|
|
89
|
+
> Gem name is `roundhouse_ui`; the brand and mount path are **Roundhouse**.
|
|
90
|
+
|
|
91
|
+
## Contents
|
|
92
|
+
|
|
93
|
+
**Setting up** ·
|
|
94
|
+
[Requirements](#requirements) ·
|
|
95
|
+
[Installation](#installation) ·
|
|
96
|
+
[Backends](#backends) ·
|
|
97
|
+
[Mounting](#mounting) ·
|
|
98
|
+
[Configuration](#configuration) ·
|
|
99
|
+
[Security](#security)
|
|
100
|
+
|
|
101
|
+
**Operating** ·
|
|
102
|
+
[Pausing queues](#pausing-queues) ·
|
|
103
|
+
[Snapshots](#snapshots) ·
|
|
104
|
+
[Cancelling jobs](#cancelling-jobs) ·
|
|
105
|
+
[Search](#search) ·
|
|
106
|
+
[Bulk actions on a filter](#bulk-actions-on-a-filter) ·
|
|
107
|
+
[Slowest job classes](#slowest-job-classes)
|
|
108
|
+
|
|
109
|
+
**Labelling and links** ·
|
|
110
|
+
[Job tags](#job-tags) ·
|
|
111
|
+
[Runbooks](#runbooks) ·
|
|
112
|
+
[Observability deep-links](#observability-deep-links) ·
|
|
113
|
+
[Surfacing sidekiq-failures](#surfacing-sidekiq-failures)
|
|
114
|
+
|
|
115
|
+
**Appearance** ·
|
|
116
|
+
[Theming](#theming) ·
|
|
117
|
+
[Settings](#settings) ·
|
|
118
|
+
[Keyboard](#keyboard)
|
|
119
|
+
|
|
120
|
+
**Project** ·
|
|
121
|
+
[Stability](#stability) ·
|
|
122
|
+
[Development](#development) ·
|
|
123
|
+
[Roadmap](#roadmap) ·
|
|
124
|
+
[Contributing](#contributing) ·
|
|
125
|
+
[License](#license)
|
|
35
126
|
|
|
36
127
|
## Requirements
|
|
37
128
|
|
|
@@ -55,11 +146,22 @@ initializer:
|
|
|
55
146
|
RoundhouseUi.backend = RoundhouseUi::Backends::SolidQueue.new
|
|
56
147
|
```
|
|
57
148
|
|
|
58
|
-
The UI adapts to each backend's capabilities
|
|
59
|
-
native** (no fetcher, no warning), and
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
149
|
+
The UI adapts to each backend's capabilities. On Solid Queue, queue **pause is
|
|
150
|
+
native** (no fetcher, no warning), and these hide — and refuse at the route, not just
|
|
151
|
+
in the view:
|
|
152
|
+
|
|
153
|
+
| Hidden on Solid Queue | Why |
|
|
154
|
+
|---|---|
|
|
155
|
+
| Retries | no distinct retry set |
|
|
156
|
+
| Redis pressure | not Redis-backed |
|
|
157
|
+
| Capsules, Workers | processes are a follow-up |
|
|
158
|
+
| Snapshots | reads Sidekiq's queues through Redis |
|
|
159
|
+
| Audit log | needs Redis |
|
|
160
|
+
| Enqueue and Edit | no `push` |
|
|
161
|
+
| Scheduled → "Enqueue now" | no `add_to_queue` |
|
|
162
|
+
|
|
163
|
+
Dashboard, Queues, Scheduled, Dead, Busy and the grouped Errors view all work on both,
|
|
164
|
+
as do pause, resume, purge, delete, bulk-on-a-filter and Retry. See
|
|
63
165
|
[docs/adr/0001](docs/adr/0001-backend-port-multi-queue.md).
|
|
64
166
|
|
|
65
167
|
> Running **both** Sidekiq and Solid Queue in one app (e.g. mid-migration)? That's
|
|
@@ -98,7 +200,7 @@ RoundhouseUi.configure do |c|
|
|
|
98
200
|
c.actor_resolver = ->(controller) { controller.current_user&.email }
|
|
99
201
|
|
|
100
202
|
# Deep-link jobs out to your APM (see Observability).
|
|
101
|
-
c.observability = RoundhouseUi::Observability::DatadogAdapter.new(service: "
|
|
203
|
+
c.observability = RoundhouseUi::Observability::DatadogAdapter.new(service: "sidekiq")
|
|
102
204
|
|
|
103
205
|
# Where queue snapshots are stored (default: Redis). Swap for a file/S3 store.
|
|
104
206
|
# c.snapshot_store = MyS3SnapshotStore.new
|
|
@@ -112,9 +214,20 @@ RoundhouseUi.configure do |c|
|
|
|
112
214
|
# installing RoundhouseUi::Fetch enforces it. Default: true.
|
|
113
215
|
# c.pause_enabled = false
|
|
114
216
|
|
|
217
|
+
# Show the Busy page's Cancel button. Off by default because cancellation only
|
|
218
|
+
# does something once you install CancelMiddleware, or have long jobs poll
|
|
219
|
+
# RoundhouseUi.cancelled?(jid) themselves. See "Cancelling jobs".
|
|
220
|
+
# c.cancel_enabled = true
|
|
221
|
+
|
|
115
222
|
# Surface your own labels (owning team, tenant, …) on job rows, the job page,
|
|
116
223
|
# and grouped errors — and filter by them. See "Job tags" below.
|
|
117
224
|
# c.job_tags = RoundhouseUi::Tags.from_constant(:OWNER, as: :squad)
|
|
225
|
+
# c.job_runbooks = RoundhouseUi::Runbooks.from_constant(:RUNBOOK)
|
|
226
|
+
|
|
227
|
+
# Recolour the UI — pure CSS custom properties, no build step. See "Theming".
|
|
228
|
+
# c.theme = { accent: "#FF2BD1", accent_2: "#00E5FF" }
|
|
229
|
+
# c.themes = RoundhouseUi::Theme::PRESETS.slice(:catppuccin, :nord, :gruvbox)
|
|
230
|
+
# c.allow_theme_selection = false
|
|
118
231
|
|
|
119
232
|
# Seconds between dashboard stat polls (default 5). Raise it if polling shows
|
|
120
233
|
# up in your traces — each poll re-runs the host's auth/routing on the mount.
|
|
@@ -138,18 +251,27 @@ here is required to mount Roundhouse.
|
|
|
138
251
|
| `redact_args` | `[]` | **Any app whose job args carry secrets or PII** — args render in full on the job page. Matches keys case-insensitively as substrings, and walks nested hashes/arrays. | Args are all IDs and enum values. |
|
|
139
252
|
| `actor_resolver` | `"anonymous"` | You want the audit log to name *who* did something. One line: `->(c) { c.current_user&.email }`. | Single-operator app, or you already audit at another layer. |
|
|
140
253
|
| `allow_job_editing` | `false` | Development and debugging. **Sharp tool** — a bad edit creates an unrunnable job, and it lets the UI enqueue arbitrary classes. | Production, unless you specifically want that power and have `read_only` off anyway. |
|
|
141
|
-
| `observability` | no-op | You run an APM and want per-job deep links out to it. Ships a Datadog adapter; duck-type `job_url
|
|
254
|
+
| `observability` | no-op | You run an APM and want per-job deep links out to it. Ships a Datadog adapter; duck-type `job_url` and `label` for anything else — `error_url`, `icon` and `wordmark?` are optional. | No APM, or you'd rather not add links that only some people can open. |
|
|
142
255
|
| `snapshot_store` | Redis | Your snapshots are large or need to outlive Redis (S3/disk). Duck-type `write`/`read`/`delete`/`ids`. | Redis is fine — which it usually is for occasional queue snapshots. |
|
|
143
256
|
| `show_sidekiq_failures` | `false` | You use the `sidekiq-failures` gem **and** run jobs with `retry: false` — those never enter Sidekiq's retry/dead sets, so this is the only way to see them. | You don't have the gem (it's a no-op then anyway). |
|
|
144
257
|
| `poll_interval` | `5` | **Raise it** if dashboard polling shows up in your traces — every poll re-runs your app's auth and routing on the mount, so a busy console adds real load. Lower it only for a livelier demo. | Default is fine for most apps. |
|
|
145
258
|
| `collect_durations` | `false` | You want "slowest job classes" on Metrics, which Sidekiq doesn't track. **Also requires installing the `DurationCollector` middleware** — the flag alone shows nothing. Costs one pipelined Redis round-trip per job. | You already get per-job timing from your APM. |
|
|
259
|
+
| `backend` | Sidekiq | You run Solid Queue, or you want to point the UI at your own adapter. `RoundhouseUi::Backends::SolidQueue.new`, or duck-type the port. See [Backends](#backends). | You run Sidekiq — it is the default. |
|
|
146
260
|
| `job_tags` | `nil` | You already know which team, tenant or product area owns a job — usually as a constant on the class — and want that visible and filterable in the UI. See [Job tags](#job-tags). | Every job belongs to the same team. |
|
|
147
261
|
| `job_tags_per_job` | `false` | **Only** when `job_tags` reads the payload (tagging by tenant, account, …). Costs one resolver call per row rather than one per class. | Tags derive from the job class, which is the common case. |
|
|
148
262
|
| `tag_filters` | `nil` | You want stable filter dropdowns instead of ones that only list what happens to be on screen — and want filtering on an unknown key to match nothing. | The `?tag=` URL is enough. |
|
|
263
|
+
| `job_runbooks` | `nil` | Your jobs have runbooks and you'd rather not make someone find them at 3am. | There's nothing to link to yet. |
|
|
264
|
+
| `job_class_namespaces` | `nil` | You want to bound which constants a job payload can cause Roundhouse to resolve. See [Security](#security). | Your job payloads come only from your own app, which is the normal case. |
|
|
265
|
+
| `history` | — | Not a setting. Sidekiq records daily processed and failed counts itself, so the Dashboard shows a History chart with no configuration and no storage. Hidden on Solid Queue, which has no equivalent. | |
|
|
266
|
+
| `theme` | `nil` | You want Roundhouse to match your own admin's palette, or you just want it to look different. Partial themes are fine — unset tokens keep their shipped values. See [Theming](#theming). | The shipped light/dark pair is fine. |
|
|
267
|
+
| `icons` | `:svg` | You already ship FontAwesome and would rather Roundhouse used it — `:font_awesome`, or a Hash of `{ name => "class names" }`. Roundhouse never loads a font itself either way. | You want the shipped inline SVG, which needs nothing installed. |
|
|
268
|
+
| `themes` | shipped presets | You want people to pick their own palette on the Settings page. | Everyone should see the same thing — set `theme` instead, or `allow_theme_selection = false`. |
|
|
269
|
+
| `allow_theme_selection` | `true` | Leave it on. | Recolouring a production console isn't something you want an operator doing. |
|
|
149
270
|
| `pause_enabled` | `true` | Leave it on. | **Rarely set this to `false`.** Pause is enforced natively on Sidekiq Pro and Solid Queue, and on OSS Sidekiq by installing `RoundhouseUi::Fetch` — so turning it off usually just hides a working feature. Only useful if you want the controls gone entirely. |
|
|
271
|
+
| `cancel_enabled` | `false` | You installed `CancelMiddleware`, or your long-running jobs poll `RoundhouseUi.cancelled?(jid)`. **The flag alone cancels nothing** — without one of those, `cancel!` writes a JID nothing reads, which is why the button is hidden by default. Sidekiq only; Solid Queue has no cancellation path. | You haven't wired either check up yet. |
|
|
150
272
|
|
|
151
273
|
Two that pair with a middleware rather than working alone: `collect_durations`
|
|
152
|
-
(`DurationCollector`) and
|
|
274
|
+
(`DurationCollector`) and `cancel_enabled` (`CancelMiddleware`) — see
|
|
153
275
|
[Cancelling jobs](#cancelling-jobs) and [Slowest job classes](#slowest-job-classes).
|
|
154
276
|
|
|
155
277
|
## Pausing queues
|
|
@@ -175,10 +297,8 @@ Roundhouse detects whether a fetcher has reported in).
|
|
|
175
297
|
|
|
176
298
|
### Sidekiq Pro / Enterprise — nothing to install
|
|
177
299
|
|
|
178
|
-
Pro ships its own enforced pause, and Roundhouse uses it automatically
|
|
179
|
-
|
|
180
|
-
`Sidekiq::BasicFetch` (`super_fetch` honors it too), so **any Pro worker enforces
|
|
181
|
-
pauses** whether or not a fetch strategy is configured.
|
|
300
|
+
Pro ships its own enforced pause, and Roundhouse uses it automatically — **any Pro
|
|
301
|
+
worker enforces pauses** whether or not a fetch strategy is configured.
|
|
182
302
|
|
|
183
303
|
When Roundhouse detects Pro it delegates pause/resume to `Sidekiq::Queue#pause!`,
|
|
184
304
|
reads paused state from Pro's registry, advertises `native_pause`, and drops the
|
|
@@ -186,13 +306,170 @@ reads paused state from Pro's registry, advertises `native_pause`, and drops the
|
|
|
186
306
|
|
|
187
307
|
- **Don't** install `RoundhouseUi::Fetch` — it isn't needed, and on `super_fetch`
|
|
188
308
|
installs it would displace reliable fetch and lose its crash-recovery guarantees.
|
|
189
|
-
- **Don't** set `pause_enabled = false` — pause
|
|
190
|
-
|
|
309
|
+
- **Don't** set `pause_enabled = false` — pause works; disabling it only hides a
|
|
310
|
+
feature you already have.
|
|
191
311
|
|
|
192
312
|
Roundhouse always goes through `Sidekiq::Queue#pause!` rather than writing Pro's
|
|
193
|
-
Redis key directly
|
|
194
|
-
|
|
195
|
-
|
|
313
|
+
Redis key directly — a raw write does not reach already-running workers.
|
|
314
|
+
|
|
315
|
+
Pro's own behaviour here is described from its public API, and Roundhouse has no Pro
|
|
316
|
+
dependency and runs no Pro in CI. Treat it as our integration contract, not as Pro
|
|
317
|
+
documentation; [Sidekiq's own docs](https://github.com/sidekiq/sidekiq/wiki) are
|
|
318
|
+
authoritative.
|
|
319
|
+
|
|
320
|
+
## Icons and motion
|
|
321
|
+
|
|
322
|
+
Icons are inline SVG — no font, no request, no CSP change, and the same shape on
|
|
323
|
+
every platform. If you already ship an icon font, use it instead:
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
RoundhouseUi.icons = :font_awesome
|
|
327
|
+
RoundhouseUi.icons = { dashboard: "fa-solid fa-gauge-high", queues: "my-icon" }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Roundhouse never loads a font itself in either mode — it emits class names and
|
|
331
|
+
your pipeline supplies the glyphs, which is what keeps the self-contained CSP
|
|
332
|
+
intact. An unknown name renders nothing rather than raising.
|
|
333
|
+
|
|
334
|
+
Motion is limited to effects that carry information: a polled value flashes when
|
|
335
|
+
it actually changes, a queue that will not drain pulses slowly, rows settle in on
|
|
336
|
+
navigation, and the refresh arc depletes. All of it is dropped under
|
|
337
|
+
`prefers-reduced-motion`.
|
|
338
|
+
|
|
339
|
+
## Theming
|
|
340
|
+
|
|
341
|
+
The UI's colours are CSS custom properties. Override any of them from an
|
|
342
|
+
initializer — pure CSS, no build step, no stylesheet to fork:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
RoundhouseUi.theme = { accent: "#FF2BD1", accent_2: "#00E5FF" }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A colour that reads well on near-black rarely reads well on near-white, so you
|
|
349
|
+
can speak to each mode separately:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
RoundhouseUi.theme = {
|
|
353
|
+
dark: { bg: "#0A0511", panel: "#140A24", accent: "#FF2BD1" },
|
|
354
|
+
light: { bg: "#FFF7FB", panel: "#FFFFFF", accent: "#B3009E" }
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Anything you leave unset keeps its shipped value, so partial themes are fine.
|
|
359
|
+
Keys are token names with underscores for dashes — `accent_2` sets `--accent-2`.
|
|
360
|
+
|
|
361
|
+
Available tokens: `bg`, `panel`, `panel_2`, `panel_3`, `line`, `line_soft`,
|
|
362
|
+
`text`, `muted`, `faint`, `accent`, `accent_2`, `good`, `warn`, `crit`, `mono`,
|
|
363
|
+
`sans`.
|
|
364
|
+
|
|
365
|
+
### What's in the box
|
|
366
|
+
|
|
367
|
+
Eleven presets. Ten of them ship the light **and** dark variant their own
|
|
368
|
+
authors designed, so choosing a palette is never a choice to give up light mode:
|
|
369
|
+
|
|
370
|
+
| Preset | Dark | Light |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| `catppuccin` | [Catppuccin](https://github.com/catppuccin/catppuccin) Mocha | Latte |
|
|
373
|
+
| `catppuccin_macchiato` | Catppuccin Macchiato | Latte |
|
|
374
|
+
| `catppuccin_frappe` | Catppuccin Frappé | Latte |
|
|
375
|
+
| `rose_pine` | [Rosé Pine](https://github.com/rose-pine/rose-pine-theme) Main | Dawn |
|
|
376
|
+
| `rose_pine_moon` | Rosé Pine Moon | Dawn |
|
|
377
|
+
| `nord` | [Nord](https://github.com/nordtheme/nord) Polar Night | Snow Storm |
|
|
378
|
+
| `gruvbox` | [Gruvbox](https://github.com/morhetz/gruvbox) Dark | Light |
|
|
379
|
+
| `everforest` | [Everforest](https://github.com/sainnhe/everforest) Dark | Light |
|
|
380
|
+
| `kanagawa` | [Kanagawa](https://github.com/rebelot/kanagawa.nvim) Wave | Lotus |
|
|
381
|
+
| `solarized` | [Solarized](https://github.com/altercation/solarized) Dark | Light |
|
|
382
|
+
|
|
383
|
+
The eleventh is `cyberpunk` — loud, and dark-only, which Settings labels, since
|
|
384
|
+
a dark-only palette is inert in light mode.
|
|
385
|
+
|
|
386
|
+
Catppuccin and Rosé Pine each ship one light flavour and several dark ones, so
|
|
387
|
+
their entries share a light half. That's upstream's own design rather than a
|
|
388
|
+
shortcut here, which is why the preset name says which dark flavour you get.
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
RoundhouseUi.theme = RoundhouseUi::Theme::PRESETS[:kanagawa]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
> All 280 values come from each project's own palette file — `palette.json`,
|
|
395
|
+
> `gruvbox.vim`, `nord.css`, `colors.lua` — rather than transcribed by eye. The
|
|
396
|
+
> mapping onto our tokens is what can be wrong while every colour is right:
|
|
397
|
+
> `panel` must lift off `bg`, `panel_2` must carry `muted` text, and `line` must
|
|
398
|
+
> be soft — the shipped theme draws borders at 1.20:1 against their own panel.
|
|
399
|
+
> One surface step too far doesn't read as a colour bug, it reads as a broken
|
|
400
|
+
> theme: it put Nord's light border at 6.4:1 and Rosé Pine's dark at 3.2:1, a
|
|
401
|
+
> hard outline around every button and input. Tests hold every palette to
|
|
402
|
+
> contrast floors and to those structural rules, so a new one can't regress it.
|
|
403
|
+
|
|
404
|
+
### Letting people pick
|
|
405
|
+
|
|
406
|
+
`theme` is what everyone sees. If you'd rather offer a menu, name the palettes
|
|
407
|
+
and each person picks one on the Settings page:
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
RoundhouseUi.themes = {
|
|
411
|
+
cyberpunk: RoundhouseUi::Theme::PRESETS[:cyberpunk],
|
|
412
|
+
midnight: { dark: { bg: "#000000", panel: "#0A0A0A" } }
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
All eleven shipped presets are on offer by default — trim the list if that's
|
|
417
|
+
more choice than you want in a production console. A palette beats `theme`, and
|
|
418
|
+
"Default" on that page means whatever `theme` you configured, so a host palette
|
|
419
|
+
is the floor rather than something a viewer can be stranded away from.
|
|
420
|
+
|
|
421
|
+
Every offered palette is emitted as CSS on every page: all eleven cost about
|
|
422
|
+
1.5 KB gzipped.
|
|
423
|
+
|
|
424
|
+
Withdraw the control entirely where recolouring a production console isn't
|
|
425
|
+
something an operator should be doing:
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
RoundhouseUi.allow_theme_selection = false
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The browser stores which palette by name, never the colours, so a tampered
|
|
432
|
+
`localStorage` value can only select a palette you already configured.
|
|
433
|
+
|
|
434
|
+
## Settings
|
|
435
|
+
|
|
436
|
+
`/settings` holds the per-person preferences: light or dark, palette, content
|
|
437
|
+
width, and how often pages refresh. Everything there lives in that browser's
|
|
438
|
+
local storage — nothing is written server-side, so one person's choices never
|
|
439
|
+
change what anyone else sees, and there's no state to migrate or clean up. A
|
|
440
|
+
private window starts fresh.
|
|
441
|
+
|
|
442
|
+
Every refresh tick runs your app's own authentication and routing, so a faster
|
|
443
|
+
interval isn't free. Whatever you set for
|
|
444
|
+
`poll_interval` is the default and is named on the page; a viewer can go faster
|
|
445
|
+
or slower within 2–300 seconds.
|
|
446
|
+
|
|
447
|
+
## Runbooks
|
|
448
|
+
|
|
449
|
+
Whoever wrote the job knows what to do when it fails. The person paged at 3am
|
|
450
|
+
usually does not. Point Roundhouse at whatever you already have:
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
# a constant on the class, same convention as job tags
|
|
454
|
+
RoundhouseUi.job_runbooks = RoundhouseUi::Runbooks.from_constant(:RUNBOOK)
|
|
455
|
+
|
|
456
|
+
# or a plain map
|
|
457
|
+
RoundhouseUi.job_runbooks = { "Billing::SyncWorker" => "https://wiki/billing" }
|
|
458
|
+
|
|
459
|
+
# or any callable
|
|
460
|
+
RoundhouseUi.job_runbooks = ->(klass:, item:) { "https://wiki/jobs/#{klass}" }
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
A **Runbook** link appears on the job page and on each grouped error row — the
|
|
464
|
+
two places someone lands during an incident. Resolution happens at read time
|
|
465
|
+
like tags, so it covers jobs already in the sets, with no middleware and nothing
|
|
466
|
+
stored. Inherited constants count, so one base class carries a runbook for a
|
|
467
|
+
whole family, and ActiveJob-wrapped jobs resolve by their real class.
|
|
468
|
+
|
|
469
|
+
> Only `http`/`https` URLs render. The value lands in an `href`, where no
|
|
470
|
+
> escaping makes `javascript:` safe, so the scheme is checked instead — a
|
|
471
|
+
> misconfigured host gets no link rather than a link that runs. Links open in a
|
|
472
|
+
> new tab with `rel="noopener noreferrer"`.
|
|
196
473
|
|
|
197
474
|
## Job tags
|
|
198
475
|
|
|
@@ -223,10 +500,10 @@ wrapper is unwrapped before your resolver sees it. See
|
|
|
223
500
|
|
|
224
501
|
### Filtering
|
|
225
502
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
503
|
+
Type `tag=squad:growth` into the search box on Retries, Dead, Scheduled, a queue's job
|
|
504
|
+
list, or Errors. It combines with every other filter, survives pagination, and **applies
|
|
505
|
+
to bulk actions too**, so "delete all matching" acts on exactly the rows shown and never
|
|
506
|
+
more. `?tag=squad:growth` still works as a URL — see [Search](#search).
|
|
230
507
|
|
|
231
508
|
Declare a vocabulary to get stable dropdowns instead of relying on the URL:
|
|
232
509
|
|
|
@@ -266,8 +543,10 @@ job class + error (not yet as an individual-job list with per-row actions).
|
|
|
266
543
|
|
|
267
544
|
## Cancelling jobs
|
|
268
545
|
|
|
269
|
-
Cancellation is cooperative — Ruby can't safely kill a running thread
|
|
270
|
-
|
|
546
|
+
Cancellation is cooperative — Ruby can't safely kill a running thread, so something
|
|
547
|
+
has to check. Nothing checks by default, which is why the Cancel button is hidden until
|
|
548
|
+
you set `cancel_enabled = true`. Install the middleware so a cancelled job is dropped
|
|
549
|
+
before it runs:
|
|
271
550
|
|
|
272
551
|
```ruby
|
|
273
552
|
# config/initializers/sidekiq.rb
|
|
@@ -276,6 +555,8 @@ Sidekiq.configure_server do |config|
|
|
|
276
555
|
end
|
|
277
556
|
```
|
|
278
557
|
|
|
558
|
+
…then turn the button on with `c.cancel_enabled = true`.
|
|
559
|
+
|
|
279
560
|
The **Busy** page's Cancel button flags a job's JID. A queued/scheduled/retrying job
|
|
280
561
|
is then skipped when it would next run; a *currently running* job stops only if it
|
|
281
562
|
checks in — e.g. a long loop can `break if RoundhouseUi.cancelled?(jid)`.
|
|
@@ -305,6 +586,46 @@ end
|
|
|
305
586
|
It's two cheap Redis writes per job (a counter + a summed-ms float) into a single hash,
|
|
306
587
|
pipelined into **one round-trip**, and a job failure never propagates from the collector.
|
|
307
588
|
|
|
589
|
+
## Search
|
|
590
|
+
|
|
591
|
+
One box per page, and everything in it travels as a single `?q=` parameter:
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
/roundhouse/dead?q=class%3DBillingWorker+error%3DTimeout%3A%3AError+stripe
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
| | |
|
|
598
|
+
|---|---|
|
|
599
|
+
| `class=` | exact job class (the real class, not the ActiveJob wrapper) |
|
|
600
|
+
| `error=` | exact error class |
|
|
601
|
+
| `queue=` | exact queue name |
|
|
602
|
+
| `tag=` | a declared tag, as `key:value` |
|
|
603
|
+
| `%` | wildcard — `class=Roundhouse%`, `class=%Worker`, `class=%oundhouse%` |
|
|
604
|
+
| anything else | substring across class, JID, error message and **redacted** arguments |
|
|
605
|
+
|
|
606
|
+
Facets match exactly unless you use `%`, so `queue=default` never also selects
|
|
607
|
+
`default_low`. `_` is a literal, not a wildcard. Quote values with spaces
|
|
608
|
+
(`error="Net::ReadTimeout with body"`), and use `text="account_id=1234"` for free text
|
|
609
|
+
that looks like a filter.
|
|
610
|
+
|
|
611
|
+
Anything the parser does not understand is **refused whole**, with the offending token
|
|
612
|
+
named — never dropped and never silently widened, because this box sits directly above
|
|
613
|
+
"delete all matching". `class=%` is refused for the same reason: a pattern with no
|
|
614
|
+
literal characters matches everything.
|
|
615
|
+
|
|
616
|
+
Each active facet shows as a pill in the bar with its own ×. Tab completes a key or
|
|
617
|
+
value; Enter applies. The `?` beside the box lists the vocabulary for that page — Errors
|
|
618
|
+
has no `queue=` (a class+error group spans every queue), and the Queues index honours
|
|
619
|
+
only `queue=` and free text, because a queue is not a job.
|
|
620
|
+
|
|
621
|
+
One documented exception to "refused whole": a `tag=` that is not `key:value` is
|
|
622
|
+
**dropped** rather than refused, so a typo doesn't stop you browsing. The drop is named
|
|
623
|
+
in a banner above the table and the bulk controls are withdrawn until you fix it —
|
|
624
|
+
what survived the drop selects a superset of what you asked for.
|
|
625
|
+
|
|
626
|
+
Arguments are searched **as they are displayed**, i.e. redacted. Searching the raw values
|
|
627
|
+
would turn the box into an oracle for the secrets `redact_args` exists to hide.
|
|
628
|
+
|
|
308
629
|
## Bulk actions on a filter
|
|
309
630
|
|
|
310
631
|
On **Retries** and **Dead**, searching narrows the set; with a filter active you
|
|
@@ -312,33 +633,183 @@ can retry or delete **every** matching job in one action (not just the visible
|
|
|
312
633
|
page), capped at 1,000 per run. Gated to when a filter is present so it can't
|
|
313
634
|
become "retry everything", `read_only`-aware, and audit-logged.
|
|
314
635
|
|
|
636
|
+
Both go through a **dry run** first: the matched jobs are listed, with their
|
|
637
|
+
arguments and errors, and nothing is touched until you confirm. The count in the
|
|
638
|
+
toolbar tells you how many jobs match; only the dry run tells you which.
|
|
639
|
+
|
|
640
|
+
### Find more like this
|
|
641
|
+
|
|
642
|
+
Every row on **Retries**, **Dead** and **Scheduled** carries a 🔍 that narrows the
|
|
643
|
+
set to that job's class and, where the set records one, that job's error — the
|
|
644
|
+
same pair the Errors page treats as a single issue. One click turns "this one row
|
|
645
|
+
looks wrong" into "here are all 7,546 of them, and here are the bulk controls".
|
|
646
|
+
|
|
647
|
+
It sets `class=` and `error=` facets in the bar, and they match **exactly** — no
|
|
648
|
+
`%`. That matters because the button's whole purpose is to reveal
|
|
649
|
+
`Delete all matching`: a substring would also select jobs whose *arguments* merely
|
|
650
|
+
mention the class you clicked, and you would never see the difference. Add a `%`
|
|
651
|
+
yourself if you want the family rather than the class — see [Search](#search).
|
|
652
|
+
|
|
315
653
|
## Observability deep-links
|
|
316
654
|
|
|
317
655
|
The core depends on nothing — it asks the configured adapter for a URL and renders a link
|
|
318
656
|
only if one comes back. A Datadog adapter ships in the box; write your own by duck-typing
|
|
319
|
-
`job_url`
|
|
657
|
+
`job_url` and `label` (`error_url`, `icon` and `wordmark?` are optional):
|
|
320
658
|
|
|
321
659
|
```ruby
|
|
322
|
-
RoundhouseUi.observability = RoundhouseUi::Observability::DatadogAdapter.new(site: "datadoghq.com", service: "
|
|
660
|
+
RoundhouseUi.observability = RoundhouseUi::Observability::DatadogAdapter.new(site: "datadoghq.com", service: "sidekiq")
|
|
323
661
|
```
|
|
324
662
|
|
|
663
|
+
`service:` is the service your **Sidekiq spans** carry, which is frequently *not* your
|
|
664
|
+
app name — apps commonly set `c.tracing.instrument :sidekiq, service_name: "sidekiq"`,
|
|
665
|
+
and dd-trace has no default of its own. Passing your app name when the spans say
|
|
666
|
+
something else produces links that silently match nothing. Omit it if you're unsure:
|
|
667
|
+
the term is left out of the query entirely when nil.
|
|
668
|
+
|
|
325
669
|
## Snapshots
|
|
326
670
|
|
|
327
|
-
Back up a queue before purging it
|
|
328
|
-
|
|
329
|
-
|
|
671
|
+
Back up a queue before purging it, then restore if you were wrong. Both actions are
|
|
672
|
+
audit-logged.
|
|
673
|
+
|
|
674
|
+
Only a Redis store ships, and it is the default. A snapshot of a stuck queue then lives in
|
|
675
|
+
the Redis you are trying to relieve — and under `allkeys-lru` it is itself evictable, so a
|
|
676
|
+
large backup can disappear. Point `RoundhouseUi.snapshot_store` at your own store to put it
|
|
677
|
+
somewhere else; the contract is four methods:
|
|
678
|
+
|
|
679
|
+
```ruby
|
|
680
|
+
class S3SnapshotStore
|
|
681
|
+
def write(id, blob) = # persist it
|
|
682
|
+
def read(id) = # → the blob, or nil
|
|
683
|
+
def delete(id) = # remove it
|
|
684
|
+
def ids = # → array of snapshot ids
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
RoundhouseUi.snapshot_store = S3SnapshotStore.new
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Restore is not idempotent — restoring twice enqueues everything twice — and it issues one
|
|
691
|
+
push per job, so a very large snapshot is slow to put back.
|
|
692
|
+
|
|
693
|
+
## Recurring jobs
|
|
694
|
+
|
|
695
|
+
Periodic work, whichever scheduler defines it. Nothing to configure — Roundhouse
|
|
696
|
+
detects what is loaded:
|
|
697
|
+
|
|
698
|
+
| Source | Read via |
|
|
699
|
+
|---|---|
|
|
700
|
+
| [sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron) | `Sidekiq::Cron::Job.all` |
|
|
701
|
+
| [sidekiq-scheduler](https://github.com/sidekiq-scheduler/sidekiq-scheduler) | `Sidekiq.schedule` |
|
|
702
|
+
| Sidekiq Enterprise periodic | `Sidekiq::Periodic::LoopSet` |
|
|
703
|
+
| Solid Queue | `SolidQueue::RecurringTask` |
|
|
704
|
+
|
|
705
|
+
More than one can be active at once — an app mid-migration genuinely runs two —
|
|
706
|
+
and all of them show. The nav item hides when none is present.
|
|
707
|
+
|
|
708
|
+
The useful part is not the crontab. It is **"this says hourly and has not run in
|
|
709
|
+
three days"**, which needs the schedule's interval, which needs a cron parser.
|
|
710
|
+
`fugit` ships with both sidekiq-cron and sidekiq-scheduler, so it is there
|
|
711
|
+
wherever this feature is; without it, staleness reads as unknown rather than
|
|
712
|
+
being guessed. A task is flagged overdue only after missing **two** intervals — a
|
|
713
|
+
job due at :00 that runs at :00:07 is not late, and a page that says otherwise
|
|
714
|
+
gets ignored.
|
|
715
|
+
|
|
716
|
+
**Read-only.** Schedules belong in the code that declares them, and a UI that
|
|
717
|
+
silently changes a production schedule is a different risk conversation.
|
|
718
|
+
|
|
719
|
+
## History
|
|
720
|
+
|
|
721
|
+
The Dashboard carries a **History** chart — daily processed counts and the daily
|
|
722
|
+
**failure rate**, over 1 week to 6 months.
|
|
723
|
+
|
|
724
|
+
This needs no configuration and stores nothing. Sidekiq already keeps a counter
|
|
725
|
+
per day; Roundhouse just reads it. The rate is the line worth watching: counts
|
|
726
|
+
move with traffic, so a busy Monday looks worse than a quiet Sunday even when
|
|
727
|
+
nothing changed.
|
|
728
|
+
|
|
729
|
+
A dashed baseline marks the **typical** failure rate — the median across days
|
|
730
|
+
that had traffic, so one incident cannot become the new normal and quiet
|
|
731
|
+
weekends cannot drag it to zero.
|
|
732
|
+
|
|
733
|
+
Sidekiq only. Solid Queue has no equivalent counter, so the section hides rather
|
|
734
|
+
than drawing an empty chart.
|
|
330
735
|
|
|
331
736
|
## Security
|
|
332
737
|
|
|
738
|
+
**Constant resolution from job payloads.** `Tags.from_constant` and
|
|
739
|
+
`Runbooks.from_constant` read a constant off the job class, which means turning
|
|
740
|
+
`item["class"]` — a string out of Redis — into a Class:
|
|
741
|
+
|
|
742
|
+
- Malformed names never reach a lookup. Ruby rejects `"../../etc/passwd"` as a
|
|
743
|
+
constant path before attempting to resolve anything, so no autoload occurs.
|
|
744
|
+
- A **well-formed name of a class that really exists** does resolve, and
|
|
745
|
+
resolving a class loads it. Production Rails sets `eager_load = true`, so every
|
|
746
|
+
app constant is already loaded and no new file is executed.
|
|
747
|
+
- Writing a crafted payload requires Redis write access — which, on a Sidekiq
|
|
748
|
+
install, already permits enqueuing a real job, a more serious compromise than
|
|
749
|
+
this.
|
|
750
|
+
|
|
751
|
+
Two ways to tighten it if your payloads aren't fully trusted:
|
|
752
|
+
|
|
753
|
+
```ruby
|
|
754
|
+
# Bound which constants may be resolved at all
|
|
755
|
+
c.job_class_namespaces = %w[Workers Jobs Billing]
|
|
756
|
+
|
|
757
|
+
# Or resolve nothing: a Hash never constantizes
|
|
758
|
+
c.job_tags = ->(klass:, item:) { { squad: OWNER_MAP[klass] } }
|
|
759
|
+
c.job_runbooks = { "Billing::SyncWorker" => "https://wiki/billing" }
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
|
|
333
763
|
- All destructive actions are CSRF-protected `POST`s — never GET — and gated by `read_only`.
|
|
764
|
+
The engine asks for forgery protection itself rather than relying on your
|
|
765
|
+
`config.load_defaults`, so this holds on an app whose defaults predate Rails 5.2.
|
|
334
766
|
- Roundhouse sets its own strict, self-contained Content-Security-Policy on its responses
|
|
335
767
|
(nonce'd inline script, same-origin only), so it's safe even if the host sets no policy.
|
|
336
|
-
- Configure `redact_args` to keep tokens
|
|
768
|
+
- Configure `redact_args` to keep tokens and PII out of the UI, and set `actor_resolver` so
|
|
769
|
+
the audit log records who did what. Redaction is key-based, so a secret nested under an
|
|
770
|
+
unlisted key is not redacted — audit what your payloads actually carry.
|
|
337
771
|
|
|
338
772
|
## Keyboard
|
|
339
773
|
|
|
340
774
|
`⌘K` (or `Ctrl+K`) opens the command palette — jump to any view or action.
|
|
341
775
|
|
|
776
|
+
## Stability
|
|
777
|
+
|
|
778
|
+
Roundhouse follows [Semantic Versioning](https://semver.org). From 1.0 the surfaces
|
|
779
|
+
below are the ones you can build against; a breaking change to any of them needs a
|
|
780
|
+
major version, and anything scheduled for removal is deprecated for at least one
|
|
781
|
+
minor release first.
|
|
782
|
+
|
|
783
|
+
**Public — covered by semver:**
|
|
784
|
+
|
|
785
|
+
| Surface | Why it's here |
|
|
786
|
+
|---|---|
|
|
787
|
+
| Everything set in `RoundhouseUi.configure` | The whole configuration surface, documented above |
|
|
788
|
+
| `RoundhouseUi.cancelled?(jid)` | Your own jobs call it, so breaking it breaks your code |
|
|
789
|
+
| `RoundhouseUi::Fetch` | Named in your Sidekiq server config |
|
|
790
|
+
| `RoundhouseUi::CancelMiddleware`, `RoundhouseUi::DurationCollector` | Installed into your middleware chain |
|
|
791
|
+
| `Tags.from_constant`, `Runbooks.from_constant` | Documented resolver shorthands |
|
|
792
|
+
| The mounted paths (`/queues`, `/retries`, …) | People bookmark and link to them |
|
|
793
|
+
| Theme token names | You override them by name |
|
|
794
|
+
| The `roundhouse:*` Redis keys | Renaming one silently loses pause state or snapshots on upgrade |
|
|
795
|
+
|
|
796
|
+
**Not public — may change in any release:**
|
|
797
|
+
|
|
798
|
+
- **The backend port.** `RoundhouseUi::Backends::*`, `supports?`, and the shapes a
|
|
799
|
+
"set" and an "entry" must answer to. Writing your own backend is possible today
|
|
800
|
+
and genuinely useful, but the contract is still being worked out against
|
|
801
|
+
[#17](https://github.com/rjrobinson/roundhouse_ui/issues/17) and
|
|
802
|
+
[#41](https://github.com/rjrobinson/roundhouse_ui/issues/41) — pinning it now
|
|
803
|
+
would freeze it before it is right. It will be promoted when those land.
|
|
804
|
+
- Anything under `lib/` not listed above: `Health`, `Metrics`, `ErrorGroups`,
|
|
805
|
+
`History`, `QueueSummary`, and the internals of `Snapshots` and `Audit`.
|
|
806
|
+
- The rendered HTML and its `rh-` class names. Theme tokens are the supported way
|
|
807
|
+
to change how Roundhouse looks; CSS written against our markup will break.
|
|
808
|
+
- The JSON from `/stats`. It exists for our own poller and is shaped for it.
|
|
809
|
+
|
|
810
|
+
If you depend on something in the second list, open an issue — that is how things
|
|
811
|
+
move to the first.
|
|
812
|
+
|
|
342
813
|
## Development
|
|
343
814
|
|
|
344
815
|
```bash
|
|
@@ -346,9 +817,51 @@ bin/rails test # full suite, ~1s, no Redis required (Sidekiq's API is stubb
|
|
|
346
817
|
bundle exec rubocop # lint
|
|
347
818
|
```
|
|
348
819
|
|
|
820
|
+
Most of the suite runs against an in-memory stand-in for Redis, which is why it
|
|
821
|
+
finishes in about a second. The destructive paths — enforced pause, snapshot →
|
|
822
|
+
restore, and bulk-on-a-filter — also have tests that run against a **real** Redis,
|
|
823
|
+
because those features are made of Redis semantics and a fake can only confirm the
|
|
824
|
+
fake. They are opt-in and have no default target, since they `FLUSHDB` whatever they
|
|
825
|
+
are pointed at and your local Redis probably belongs to something else:
|
|
826
|
+
|
|
827
|
+
```bash
|
|
828
|
+
ROUNDHOUSE_TEST_REDIS_URL=redis://localhost:6379/10 bin/rails test
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
Pick an empty database. They refuse to run against database 0, and they verify which
|
|
832
|
+
database the connection is actually on before deleting anything. CI additionally sets
|
|
833
|
+
`ROUNDHOUSE_REQUIRE_REAL_REDIS=1`, which turns a skip into a failure — without it an
|
|
834
|
+
unreachable Redis would skip them silently and the coverage would be imaginary.
|
|
835
|
+
|
|
349
836
|
The dummy app under `test/dummy` mounts the engine at `/roundhouse`; point it at a local
|
|
350
837
|
Redis and run `bin/rails server` to click around.
|
|
351
838
|
|
|
839
|
+
## Seeing it work
|
|
840
|
+
|
|
841
|
+
The gem ships workers that do real work and really fail, so a console has
|
|
842
|
+
something to show. They are not loaded by `require "roundhouse_ui"` — ask for them
|
|
843
|
+
explicitly, in an initializer you would not ship:
|
|
844
|
+
|
|
845
|
+
```ruby
|
|
846
|
+
# config/initializers/roundhouse.rb
|
|
847
|
+
require "roundhouse_ui/demo" if Rails.env.development?
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
```bash
|
|
851
|
+
bin/rails roundhouse_ui:demo:load[15] # enqueue for 15 minutes, hard cap 20
|
|
852
|
+
bin/rails roundhouse_ui:demo:clean # remove everything it left behind
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
Six classes across six queues with different durations and failure rates — one
|
|
856
|
+
long enough to always be mid-flight on **Busy**, one flaky enough to dominate
|
|
857
|
+
**Errors** — so throughput moves, retries accumulate, and jobs reach the dead set
|
|
858
|
+
on their own. The rate rises and falls, so the dashboard's trend and drain
|
|
859
|
+
forecast have something to say.
|
|
860
|
+
|
|
861
|
+
Each worker refuses to run outside development, the task refuses any environment
|
|
862
|
+
but development, and it refuses Redis database 0 — checked by asking the
|
|
863
|
+
connection where it is, not by reading configuration.
|
|
864
|
+
|
|
352
865
|
## Roadmap
|
|
353
866
|
|
|
354
867
|
- Solid Queue: Workers view + enqueue, and the multi-DB (separate queue database) case.
|