maintenance_on_steroids 0.2.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +948 -0
  5. data/app/controllers/maintenance_on_steroids/application_controller.rb +66 -0
  6. data/app/controllers/maintenance_on_steroids/dashboard_controller.rb +18 -0
  7. data/app/controllers/maintenance_on_steroids/jobs_controller.rb +59 -0
  8. data/app/controllers/maintenance_on_steroids/runs_controller.rb +223 -0
  9. data/app/jobs/maintenance_on_steroids/run_job.rb +292 -0
  10. data/app/models/maintenance_on_steroids/application_record.rb +6 -0
  11. data/app/models/maintenance_on_steroids/artifact.rb +255 -0
  12. data/app/models/maintenance_on_steroids/run.rb +310 -0
  13. data/app/views/layouts/maintenance_on_steroids/application.html.erb +48 -0
  14. data/app/views/maintenance_on_steroids/dashboard/index.html.erb +163 -0
  15. data/app/views/maintenance_on_steroids/jobs/index.html.erb +35 -0
  16. data/app/views/maintenance_on_steroids/jobs/show.html.erb +107 -0
  17. data/app/views/maintenance_on_steroids/jobs/source.html.erb +79 -0
  18. data/app/views/maintenance_on_steroids/runs/new.html.erb +98 -0
  19. data/app/views/maintenance_on_steroids/runs/show.html.erb +410 -0
  20. data/app/views/maintenance_on_steroids/shared/_auto_refresh.html.erb +85 -0
  21. data/app/views/maintenance_on_steroids/shared/_javascript.html.erb +16 -0
  22. data/app/views/maintenance_on_steroids/shared/_pager.html.erb +20 -0
  23. data/app/views/maintenance_on_steroids/shared/_styles.html.erb +649 -0
  24. data/app/views/maintenance_on_steroids/shared/_task_list_item.html.erb +31 -0
  25. data/config/routes.rb +22 -0
  26. data/lib/generators/maintenance_on_steroids/install/install_generator.rb +56 -0
  27. data/lib/generators/maintenance_on_steroids/install/templates/create_maintenance_on_steroids_tables.rb.erb +56 -0
  28. data/lib/generators/maintenance_on_steroids/install/templates/initializer.rb +42 -0
  29. data/lib/generators/maintenance_on_steroids/job/job_generator.rb +17 -0
  30. data/lib/generators/maintenance_on_steroids/job/templates/job.rb.erb +30 -0
  31. data/lib/maintenance_on_steroids/about_dsl.rb +51 -0
  32. data/lib/maintenance_on_steroids/artifact_dsl.rb +82 -0
  33. data/lib/maintenance_on_steroids/artifacts_proxy.rb +205 -0
  34. data/lib/maintenance_on_steroids/callbacks_dsl.rb +61 -0
  35. data/lib/maintenance_on_steroids/csv_artifact.rb +88 -0
  36. data/lib/maintenance_on_steroids/engine.rb +26 -0
  37. data/lib/maintenance_on_steroids/form_dsl.rb +63 -0
  38. data/lib/maintenance_on_steroids/instrumentation.rb +33 -0
  39. data/lib/maintenance_on_steroids/job_dsl.rb +61 -0
  40. data/lib/maintenance_on_steroids/job_registry.rb +83 -0
  41. data/lib/maintenance_on_steroids/jsonb_artifact.rb +39 -0
  42. data/lib/maintenance_on_steroids/params_proxy.rb +93 -0
  43. data/lib/maintenance_on_steroids/task.rb +109 -0
  44. data/lib/maintenance_on_steroids/text_artifact.rb +74 -0
  45. data/lib/maintenance_on_steroids/version.rb +3 -0
  46. data/lib/maintenance_on_steroids.rb +187 -0
  47. metadata +125 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a7be3f77841e342ff75211412864af8f0728ac542ebe1909104f8eb4876c1a6
4
+ data.tar.gz: 64de5cb0d0278e33b2548b04c3b11b2a1491eaabcf0eee2e4d180e5f74661f5a
5
+ SHA512:
6
+ metadata.gz: 54ee12177a99ae2cf304c1fd0310d1577eb48ffaff9afcc6e1dbc4157d242b5a940d7ced4ba9f693d27adee7ea79421b261e0b91f95ab2de334e74e89a7bebf2
7
+ data.tar.gz: b0d39045b36f8ac1819f27af6e1cdc8a5b7e4a486aab60e991362e279e71cf3677dd35e1a75b8585f791159df4b191893d6895c1128646e577451b0aedbf3560
data/CHANGELOG.md ADDED
@@ -0,0 +1,267 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Security
11
+
12
+ - Require Rails 8.1.3.1 or newer within Rails 8, and add dependency auditing and
13
+ Brakeman to CI. Audit the host application's bundle as well.
14
+ - Resolve task names only against loaded task subclasses, including in workers.
15
+
16
+ ### Fixed
17
+
18
+ - Claim runs atomically and fence engine writes with a per-execution token.
19
+ Duplicate deliveries, paused runs and obsolete job IDs cannot restart work.
20
+ Stale workers cannot overwrite a resumed run's progress or artifacts.
21
+ - Detect refused queue submissions and Continuable retries. Prepare resume
22
+ metadata before dispatch so fast workers retain their new results and errors.
23
+ - Commit collection accumulator output and its cursor together. Flush final
24
+ output before committing completion; persistence failures now error the run.
25
+ - Refresh callable heartbeats at checkpoints. Add optional `enqueued_threshold:`
26
+ to stale-run recovery for abandoned dispatches.
27
+ - Check text/CSV append sizes before growing their buffers. Clarify at-least-once
28
+ side effects, callable restart behavior and artifact memory limits.
29
+
30
+ - Completion, pause and cancel callbacks produce output before the final status
31
+ is committed. Flush failures raise from `ArtifactsProxy#flush!` after attempting
32
+ the remaining artifacts. Keep artifacts small: collection checkpoints now
33
+ persist dirty output after every record.
34
+
35
+ ## [0.2.0] - 2026-08-24
36
+
37
+ First version published to RubyGems. The 0.1.1 and 0.1.0 entries below are
38
+ development milestones -- neither was tagged or released, so no one ever ran
39
+ them. The minor bump reflects the four public APIs added since 0.1.1.
40
+
41
+ ### Security
42
+
43
+ - **A blank HTTP Basic password no longer counts as configured.** The check
44
+ added in 0.1.1 compared only against the shipped `"secret"`, so a nil
45
+ password -- the result of a missing or misspelled credentials key, which is
46
+ exactly the pattern the generated initializer recommends -- passed as
47
+ configured, booted production without a warning, and let `admin` plus an
48
+ empty password through, because `secure_compare(supplied, "")` matches. Blank
49
+ is now treated as unconfigured, and the controller refuses to authenticate at
50
+ all while the configured password is blank.
51
+
52
+ ### Added
53
+
54
+ - **`Run.prune!(older_than:)`** deletes finished runs and their artifacts.
55
+ Nothing expired these rows before, so a long-lived app accumulated every run,
56
+ backtrace and stored blob indefinitely. Active and paused runs are never
57
+ pruned regardless of age.
58
+ - **`MaintenanceOnSteroids.max_artifact_size`** (default 64 MB) caps a single
59
+ stored artifact. Artifacts are buffered in worker memory and stored in one
60
+ row, so an unbounded export previously OOM-killed the worker; it now fails
61
+ the run with a message naming the artifact and its size.
62
+ - **`Task#checkpoint!`** lets a long-running callable task honour Pause and
63
+ Cancel mid-work. A callable task is a single Continuable step, so without it
64
+ a slow `call` ignored Pause until it returned.
65
+ - **`job { concurrency N }`** refuses to start a task while N of its runs are
66
+ already active -- the guard a destructive task needs against a double-clicked
67
+ New Run. Advisory, not a distributed lock.
68
+
69
+ ### Fixed
70
+
71
+ - **The task list no longer reorders itself between page loads.** `sort_by` is
72
+ not stable, so two tasks sharing a title -- or any two never-executed tasks
73
+ under `sort=last_run`, which all share the same epoch key -- swapped places
74
+ for no reason. Class name is now the tiebreaker in both orders.
75
+ - **Ghost tasks no longer reach the dashboard.** A class keeps the name it was
76
+ first assigned even after its constant is removed or rebound, and it stays in
77
+ `Task.descendants`, so the registry's sweep re-registered stale copies after
78
+ a development code reload. The sweep now skips any class its own constant no
79
+ longer resolves to.
80
+
81
+ - **Artifact previews no longer crash the run page on non-ASCII content.**
82
+ Two separate encoding faults: `data_blob` is ASCII-8BIT, so interpolating a
83
+ CSV cell holding an accented character into the UTF-8 template raised
84
+ `Encoding::CompatibilityError` at any size; and `byteslice` could cut a
85
+ multibyte character in half, after which matching the line-trim regex raised
86
+ `ArgumentError: invalid byte sequence in UTF-8`. Slices are now transcoded
87
+ and scrubbed before use.
88
+ - **A single-line artifact no longer previews as empty.** The line-trim regex
89
+ matched the entire slice when it contained no newline, blanking the preview
90
+ of any minified or unbroken payload over the cap.
91
+ - **jsonb previews respect the cap on rows with no recorded metadata.**
92
+ `byte_size` fell back to `data_blob`, which is nil for jsonb and text, so
93
+ `preview_truncated?` answered false, the whole document was generated, and
94
+ the byte slice then cut it into unparseable JSON with no truncation notice.
95
+ `byte_size` now measures the column that holds the payload, and jsonb
96
+ trimming keys off the entry count directly.
97
+ - **`database_role` no longer wraps a callable task's `call`.** It scopes the
98
+ collection scan; running a callable task's body -- mostly writes -- under
99
+ `:reading` would fail on a real replica. Callable tasks use
100
+ `Task#with_database_role` for their own reads.
101
+ - **`job_config` is resolved once per run instead of once per record.** It
102
+ allocates a fresh `JobConfig` whenever a task declares no `job` block, so the
103
+ per-record role check meant one throwaway object per processed record.
104
+ - **`Run#resume!` keeps the original failure.** It cleared `error_message` and
105
+ `error_backtrace` before attempting to enqueue, so a resume that itself
106
+ failed destroyed the only record of why the run died. They are now cleared
107
+ only once the job is really queued.
108
+ - **`Run#resume!` raises `MaintenanceOnSteroids::EnqueueFailed`** rather than
109
+ the raw exception, and the controller catches only that -- other errors reach
110
+ the host app's error reporting instead of being redirected as "Run could not
111
+ be enqueued".
112
+ - **Scalar form inputs reject nested structures.** A client posting
113
+ `task_params[name][x]=1` where a string was declared handed the task a
114
+ Parameters object; it is now rejected alongside out-of-range select values.
115
+
116
+ ### Changed
117
+
118
+ - The install migration now picks **`jsonb` on PostgreSQL** (`json` elsewhere)
119
+ for the three JSON columns, resolved against the connection at migrate time.
120
+ Existing installs are unaffected; convert with your own migration if you want
121
+ the indexable type.
122
+ - README documents the operational limits that were previously implicit:
123
+ artifact buffering and size caps, pruning, callable-task pause behaviour,
124
+ concurrent runs, the Content Security Policy requirement for live updates,
125
+ and why collections with random UUID primary keys are unsafe to resume.
126
+
127
+ - `pg` moved to an optional bundler group, so the resolved bundle no longer
128
+ depends on `DB` being set in the shell; switching between the SQLite and
129
+ PostgreSQL suites no longer needs a re-install.
130
+ - The Postgres test setup no longer drops every table in the database before
131
+ loading the schema (schema.rb already uses `force: :cascade`), which removed
132
+ a way for concurrent rspec processes to clobber each other.
133
+ - Connection values in the dummy app's `database.yml` are quoted, so a password
134
+ containing `:`, `#`, `%` or `@` no longer produces a YAML syntax error.
135
+
136
+ ## 0.1.1 - 2026-08-21 (unreleased)
137
+
138
+ ### Security
139
+
140
+ - **The engine now refuses to boot in production with no access control.**
141
+ Every layer is opt-in and the dashboard can start any task against your
142
+ database, so an unconfigured install was silently wide open. HTTP Basic left
143
+ on the shipped `"secret"` password counts as *unconfigured* -- otherwise the
144
+ most dangerous setup would be the quietest one. Hosts that gate the dashboard
145
+ elsewhere (reverse proxy, VPN, middleware) opt out explicitly with
146
+ `config.allow_insecure_dashboard = true`. Outside production this warns
147
+ instead of raising.
148
+ - **`maintenance_on_steroids:install` now generates the initializer**, with
149
+ each access-control layer laid out and commented.
150
+ - **Select inputs are validated server-side.** A `<select>` only constrains the
151
+ browser; a posted value outside the declared `options` is now rejected
152
+ instead of being handed to the task.
153
+
154
+ ### Fixed
155
+
156
+ - **Artifacts are no longer lost when a worker is interrupted.**
157
+ `ActiveJob::Continuation::Interrupt` subclasses `Exception`, so it was
158
+ invisible to `RunJob`'s `rescue` and buffered artifact rows written since the
159
+ job started were discarded on every SIGTERM/deploy while the cursor kept
160
+ advancing -- the run still reported `completed`. Flushing now happens in an
161
+ `ensure`, covering every exit path.
162
+ - **`csv` is now a declared runtime dependency.** It left Ruby's default gems
163
+ in 3.4, and `csv_artifact.rb` requires it at load time, so the gem failed to
164
+ boot in host apps on Ruby >= 3.4 that did not list `csv` themselves.
165
+ - **An errored run can be resumed from the dashboard.** A single transient
166
+ failure (deadlock, lock timeout) previously stranded a run mid-write with no
167
+ way to continue. `Run#resumable?` now covers `errored` as well as `paused`,
168
+ and resuming clears the previous error. The record that raised is retried;
169
+ records already past the cursor are not.
170
+ - **`resume_errors_after_advancing` is disabled on `RunJob`.** Continuable was
171
+ re-enqueuing a job that this gem had already marked terminal, so the retry
172
+ fired, no-opped against the guard, and left the run looking retried but never
173
+ advancing.
174
+ - **`Run#resume!` no longer strands a run in `enqueued`.** The status
175
+ compare-and-set happened before `enqueue!`, so a queue outage or a deleted
176
+ task class left the run enqueued with no job behind it (and 500'd the
177
+ controller). Enqueue failures now roll the run back to `errored`.
178
+ - **An interrupted run is marked `enqueued` while it waits to be resumed**, so
179
+ `Run.reap_stale!` no longer mistakes its frozen `updated_at` for a dead
180
+ worker and kills a run that was about to continue.
181
+ - **`RunJob` discards `ActiveRecord::RecordNotFound`** instead of retrying a
182
+ job for a deleted run ~21 times.
183
+ - **Artifact downloads 404 for non-file artifacts** instead of returning a
184
+ 200 with a zero-byte `.bin`.
185
+ - **Inline artifact previews are capped** at `Artifact::PREVIEW_BYTES` (256 KB)
186
+ and report truncation. Parsing a multi-hundred-MB export to show its first
187
+ 100 rows could exhaust the web process.
188
+ - **Reading an unwritten `:file` artifact no longer re-queries** on every
189
+ access (once per record inside a collection task).
190
+ - **`database_role` actually reaches the replica.** `collection` returns a lazy
191
+ `Relation`, so the documented `with_database_role(:read) { ... }` wrapper
192
+ restored the connection before a single row was fetched and the whole scan
193
+ ran on the primary. Declare it instead -- `job { database_role :reading }` --
194
+ and RunJob holds the role open for the entire scan, stepping back to
195
+ `:writing` for `process` and for its own cursor/progress writes.
196
+ - **`pause!` and `cancel!` are compare-and-set**, like `resume!`. A pause
197
+ clicked as the job completed could overwrite `completed` with `pausing`,
198
+ which nothing but `reap_stale!` would ever clear.
199
+ - **Oversized jsonb previews no longer generate the whole document** before
200
+ slicing it -- the structure is trimmed to `Artifact::PREVIEW_ENTRIES`
201
+ top-level entries first, which was the allocation the byte cap existed to
202
+ avoid.
203
+
204
+ ### Changed
205
+
206
+ - The engine now warns at boot when **no** access control is configured
207
+ (`error` level in production). Previously only the partially-configured case
208
+ warned, leaving the fully open dashboard silent.
209
+ - The dashboard stops polling once no run is active, instead of re-rendering
210
+ itself and its aggregate queries every 4 seconds indefinitely.
211
+ - The highlight.js theme stylesheets on the source viewer are SRI-pinned, like
212
+ the script already was.
213
+ - CI runs the suite against PostgreSQL as well as SQLite (`DB=postgres`), so
214
+ `json`/`bytea` behaviour and real row locking are covered. The install
215
+ generator has a spec.
216
+
217
+ ## 0.1.0 - 2026-07-25 (unreleased)
218
+
219
+ Initial release.
220
+
221
+ ### Added
222
+
223
+ - **Task DSL** built on `MaintenanceOnSteroids::Task`
224
+ - Collection tasks (`collection` + `process`) and callable tasks (`call`)
225
+ - `about` block for title, description and owner metadata
226
+ - `form` block with typed inputs: string, text, integer, float, boolean,
227
+ date, datetime, select and file (`:blob`) uploads
228
+ - `artifact` block for declared outputs: `:jsonb`, `:text`, `:csv`, `:file`
229
+ - `job` block for per-task queue name and priority
230
+ - Lifecycle callbacks: `after_start`, `after_pause`, `after_interrupt`,
231
+ `after_cancel`, `after_complete`, `after_error`
232
+ - `with_database_role` for reading from a replica inside a task
233
+ - **Automatic resumption** via `ActiveJob::Continuable` — a DB-backed cursor is
234
+ written after every processed record, so Sidekiq restarts and pause/resume
235
+ cycles never reprocess work already done
236
+ - **Pause / resume / cancel** with compare-and-set transitions, so concurrent
237
+ requests cannot double-enqueue or overwrite a requested stop
238
+ - **Web dashboard** mounted as a Rails engine
239
+ - Stats overview, active runs, run history with pagination
240
+ - Live progress bars and status badges via JSON polling (no build step,
241
+ no JavaScript dependencies)
242
+ - Estimated time remaining, extrapolated from the current processing rate
243
+ - Task source viewer with optional syntax highlighting (CDN + SRI pinned)
244
+ - Dark and light themes, persisted in `localStorage`
245
+ - **Artifacts** stored in the database: JSON documents, text logs, CSV exports
246
+ and binary blobs, previewable and downloadable from the UI, auto-flushed on
247
+ completion, pause and cancel
248
+ - **Authentication and access control**, layered and off by default: HTTP Basic
249
+ (constant-time comparison), a custom `authentication` hook for
250
+ Devise/Warden/etc., and `verify_access_proc` for role- or IP-based checks
251
+ - **User tracking** — records who triggered each run, with a configurable
252
+ display formatter
253
+ - **Instrumentation** — `ActiveSupport::Notifications` events for every
254
+ lifecycle transition (`enqueued`, `started`, `paused`, `resumed`,
255
+ `cancelled`, `succeeded`, `errored`); a raising subscriber can never corrupt
256
+ a run's status
257
+ - **`Run.reap_stale!`** to recover runs whose worker died mid-execution
258
+ - **Generators** — `maintenance_on_steroids:install` (migration, task
259
+ directory, engine mount) and `maintenance_on_steroids:job` (new task)
260
+
261
+ ### Requirements
262
+
263
+ - Rails >= 8.1 (`ActiveJob::Continuable` ships in 8.1)
264
+ - Ruby >= 3.2
265
+
266
+ [Unreleased]: https://github.com/igorkasyanchuk/maintenance_on_steroids/compare/v0.2.0...HEAD
267
+ [0.2.0]: https://github.com/igorkasyanchuk/maintenance_on_steroids/releases/tag/v0.2.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Igor Kasyanchuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.