rigid_workflow 1.0.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +648 -0
  3. data/README.md +427 -0
  4. data/app/assets/stylesheets/rigid_workflow/application.css +68 -0
  5. data/app/controllers/rigid_workflow/application_controller.rb +90 -0
  6. data/app/controllers/rigid_workflow/runs_controller.rb +133 -0
  7. data/app/controllers/rigid_workflow/workflows_controller.rb +11 -0
  8. data/app/helpers/rigid_workflow/runs_helper.rb +63 -0
  9. data/app/helpers/rigid_workflow/workflows_helper.rb +110 -0
  10. data/app/javascript/rigid_workflow/application.js +9 -0
  11. data/app/javascript/rigid_workflow/controllers/application.js +7 -0
  12. data/app/javascript/rigid_workflow/controllers/index.js +4 -0
  13. data/app/javascript/rigid_workflow/controllers/selection_controller.js +91 -0
  14. data/app/javascript/rigid_workflow/controllers/workflow_viz_controller.js +160 -0
  15. data/app/jobs/rigid_workflow/activity_job.rb +14 -0
  16. data/app/jobs/rigid_workflow/timer_job.rb +13 -0
  17. data/app/jobs/rigid_workflow/workflow_job.rb +17 -0
  18. data/app/models/rigid_workflow/run.rb +209 -0
  19. data/app/models/rigid_workflow/signal.rb +77 -0
  20. data/app/models/rigid_workflow/step.rb +182 -0
  21. data/app/models/rigid_workflow/step_attempt.rb +48 -0
  22. data/app/views/rigid_workflow/layouts/application.html.erb +77 -0
  23. data/app/views/rigid_workflow/runs/index.html.erb +113 -0
  24. data/app/views/rigid_workflow/runs/show.html.erb +130 -0
  25. data/app/views/rigid_workflow/workflows/index.html.erb +82 -0
  26. data/config/importmap.rb +11 -0
  27. data/config/routes.rb +17 -0
  28. data/lib/generators/rigid_workflow/activity_generator.rb +15 -0
  29. data/lib/generators/rigid_workflow/install_generator.rb +58 -0
  30. data/lib/generators/rigid_workflow/templates/activity.rb.erb +6 -0
  31. data/lib/generators/rigid_workflow/templates/initializer.rb.erb +24 -0
  32. data/lib/generators/rigid_workflow/templates/migration.rb.erb +54 -0
  33. data/lib/generators/rigid_workflow/templates/workflow.rb.erb +6 -0
  34. data/lib/generators/rigid_workflow/workflow_generator.rb +15 -0
  35. data/lib/rigid_workflow/activity.rb +54 -0
  36. data/lib/rigid_workflow/engine.rb +35 -0
  37. data/lib/rigid_workflow/id_generator.rb +21 -0
  38. data/lib/rigid_workflow/orchestrator.rb +83 -0
  39. data/lib/rigid_workflow/step_result.rb +50 -0
  40. data/lib/rigid_workflow/version.rb +5 -0
  41. data/lib/rigid_workflow/workflow.rb +59 -0
  42. data/lib/rigid_workflow/workflow_runner.rb +334 -0
  43. data/lib/rigid_workflow.rb +65 -0
  44. metadata +204 -0
data/README.md ADDED
@@ -0,0 +1,427 @@
1
+ # Rigid Workflow
2
+
3
+ A durable workflow orchestration engine for Rails applications. Built on top of ActiveJob and Solid Queue.
4
+
5
+ ## Why Rigid Workflow
6
+
7
+ In mature Rails applications, background jobs often become complex chains of inter-dependent tasks. Managing this complexity with raw `ActiveJob` calls is error-prone. Rigid Workflow solves this by providing:
8
+
9
+ - **Explicit State**: Workflows are defined as code, with state persisted automatically in your database.
10
+ - **Observability**: A built-in Admin UI to visualize, monitor, and retry workflows.
11
+ - **Complexity Management**: Native support for parallel execution, race conditions, signals/waits, and compensation (Saga pattern) without external dependencies (like Redis/Temporal) beyond what Rails already provides (ActiveJob).
12
+
13
+ ## Admin UI
14
+
15
+ <p align="center">
16
+ <a href="screenshot_rw.png"><img src="screenshot_rw.png" width="640" alt="Rigid Workflow Admin UI"></a>
17
+ </p>
18
+
19
+ ## Use Cases
20
+
21
+ **Order Fulfillment** — Reserve inventory, charge payment, ship. Compensate on failure
22
+ → Saga pattern via `rescue`/`raise` to release inventory if payment fails
23
+
24
+ **Content Moderation** — AI-powered content scanning with human-in-the-loop review
25
+ → `race` between auto-approve, auto-reject, and manual review signals
26
+
27
+ **Video Processing** — Parallel transcoding across formats with thumbnail generation
28
+ → `parallel` for fan-out execution, `loop` for thumbnail extraction at timestamps
29
+
30
+ **Subscription Billing** — Payment with retries and fallback to downgrade
31
+ → `step` with `max_attempts` and `retry_delay` for payment gateways
32
+
33
+ **Employee Offboarding** — Sequential multi-step business process across systems
34
+ → `step` chaining with persistent state across job enqueues
35
+
36
+ **User Onboarding** — Trial provisioning with time-gated auto-teardown
37
+ → `wait` with `timeout`, conditional branching per plan tier
38
+
39
+ **AI Pipelines** — Scrape websites, chunk content, generate embeddings, summarize with LLMs. Each step retries independently when APIs flake.
40
+ → `step` with per-step `max_attempts` and `retry_delay`
41
+
42
+ **RAG Pipelines** — Chunk documents, generate embeddings, index content. Multi-step LLM chains with persistent state.
43
+ → `step` chaining, `loop` over document collections, `memo` for caching
44
+
45
+ ## Features
46
+
47
+ - **DSL-based workflow definitions** - Define workflows with a clear, expressive syntax
48
+ - **Sequential and parallel execution** - Run steps in sequence or concurrently
49
+ - **Controlled race conditions** - First to complete wins (signal vs. timer)
50
+ - **Wait states and signals** - Pause workflows for external events, with optional timeouts
51
+ - **Retry logic with exponential backoff** - Automatic retries with configurable delay and jitter
52
+ - **Saga/Compensation pattern** - Auto-compensate completed steps in reverse order when an activity exhausts retries
53
+ - **Workflow versioning** - Track versions of workflow definitions; runs resume safely across version changes
54
+ - **Memoization** - Persist non-deterministic values across workflow resumptions
55
+ - **Looping over collections** - Iterate with persistent state tracking per item
56
+ - **Persistent state** - All workflow state is stored in the database
57
+ - **Admin UI** - Built-in dashboard with overview stats, filtering, pagination, Gantt chart visualization, and bulk actions
58
+ - **Event instrumentation** - `ActiveSupport::Notifications` integration for all key lifecycle events
59
+ - **Generators** - Rails generators for install, workflow, and activity scaffolding
60
+
61
+ ## Requirements
62
+
63
+ - Ruby >= 3.1
64
+ - Rails >= 7.0
65
+
66
+ ---
67
+
68
+ ## Security Requirements
69
+
70
+ ⚠️ **Authentication Required**: Rigid Workflow's admin UI has NO built-in authentication. You MUST secure it:
71
+
72
+ ### Option 1: Use `admin_controller` config (Recommended)
73
+
74
+ Set `admin_controller` to a controller that already has authentication (e.g., from Devise):
75
+
76
+ ```ruby
77
+ # config/initializers/rigid_workflow.rb
78
+ RigidWorkflow.configure do |config|
79
+ config.admin_controller = "Admin::BaseController"
80
+ end
81
+ ```
82
+
83
+ The engine's `ApplicationController` will inherit from this class, automatically applying your authentication.
84
+
85
+ ### Option 2: Wrap routes in host app
86
+
87
+ Authenticate at the route level in your `config/routes.rb`:
88
+
89
+ ```ruby
90
+ # For Devise:
91
+ authenticate :user do
92
+ mount RigidWorkflow::Engine => "/admin/rigid_workflow"
93
+ end
94
+
95
+ # For custom auth:
96
+ authenticate :admin_user do
97
+ mount RigidWorkflow::Engine => "/admin/rigid_workflow"
98
+ end
99
+ ```
100
+
101
+ **Consequence of skipping authentication**: All workflow data (runs, steps, signals, job details) will be publicly accessible to anyone who can reach the mounted route.
102
+
103
+ ---
104
+
105
+ ## Installation
106
+
107
+ Add to your Gemfile:
108
+
109
+ ```ruby
110
+ gem "rigid_workflow"
111
+ ```
112
+
113
+ Run the installer:
114
+
115
+ ```bash
116
+ rails generate rigid_workflow:install
117
+ rails db:migrate
118
+ ```
119
+
120
+ The install generator creates:
121
+
122
+ - `config/initializers/rigid_workflow.rb` - Configuration file
123
+ - Mounts the engine in `config/routes.rb`
124
+ - A migration to create the 4 required tables (`rigid_workflow_runs`, `rigid_workflow_steps`, `rigid_workflow_step_attempts`, `rigid_workflow_signals`)
125
+
126
+ ### Additional Generators
127
+
128
+ ```bash
129
+ # Generate a workflow class
130
+ rails generate rigid_workflow:workflow OrderProcessing
131
+
132
+ # Generate an activity class
133
+ rails generate rigid_workflow:activity ChargeCustomer
134
+ ```
135
+
136
+ ## Quick Start
137
+
138
+ ### Define a Workflow
139
+
140
+ ```ruby
141
+ # app/workflows/order_processing_workflow.rb
142
+ class OrderProcessingWorkflow < RigidWorkflow::Workflow
143
+ class ValidateOrder < RigidWorkflow::Activity
144
+ def perform(order_id:, **)
145
+ order = Order.find(order_id)
146
+ raise "Invalid order" unless order.valid?
147
+ { order_id: order_id, validated: true }
148
+ end
149
+ end
150
+
151
+ class ProcessPayment < RigidWorkflow::Activity
152
+ def perform(order_id:, **)
153
+ # Process payment...
154
+ { payment_id: "pay_123", order_id: order_id }
155
+ end
156
+
157
+ # Called if a later step fails (Saga pattern)
158
+ def compensate
159
+ refund_payment(output[:payment_id])
160
+ end
161
+ end
162
+
163
+ class SendConfirmation < RigidWorkflow::Activity
164
+ def perform(order_id:, **)
165
+ OrderMailer.confirmation(order_id).deliver_later
166
+ { confirmed: true }
167
+ end
168
+ end
169
+
170
+ version 2 # Optional: track workflow definition versions
171
+
172
+ def run
173
+ step :validate, ValidateOrder, input: { order_id: params[:order_id] }
174
+ step :payment, ProcessPayment, input: { order_id: params[:order_id] }
175
+ step :confirm, SendConfirmation, input: { order_id: params[:order_id] }
176
+ end
177
+ end
178
+ ```
179
+
180
+ ### Start a Workflow
181
+
182
+ ```ruby
183
+ run = OrderProcessingWorkflow.start!(order_id: 123)
184
+ ```
185
+
186
+ ### Emit a Signal
187
+
188
+ ```ruby
189
+ run.emit_signal(:payment_received, method: "applepay")
190
+ ```
191
+
192
+ ## DSL Reference
193
+
194
+ ### Steps
195
+
196
+ ```ruby
197
+ step :name, ActivityClass, input: { key: "value" }, async: true
198
+ ```
199
+
200
+ Options:
201
+
202
+ | Option | Description | Default |
203
+ | --------------- | --------------------------------------------- | --------------- |
204
+ | `input:` | Hash of input data passed to the activity | Workflow params |
205
+ | `async:` | Run asynchronously (enqueued as a job) | `false` |
206
+ | `wait:` | Delay before executing (e.g., `wait: 1.hour`) | `nil` |
207
+ | `wait_until:` | Execute at a specific time | `nil` |
208
+ | `max_attempts:` | Number of retry attempts | `3` |
209
+ | `retry_delay:` | Base delay for exponential backoff | `15.seconds` |
210
+
211
+ **Async restriction**: `async: true` steps will suspend the workflow. Use them inside `parallel` or `race` blocks to avoid suspension, or mark the activity class with `force_async`:
212
+
213
+ ```ruby
214
+ class SlowActivity < RigidWorkflow::Activity
215
+ force_async true
216
+
217
+ def perform(**)
218
+ # This step always runs asynchronously, even outside parallel/race
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Loops
224
+
225
+ ```ruby
226
+ loop :items, collection do |item, index|
227
+ step :process_item, ProcessItemActivity, input: { item: item }
228
+ end
229
+ ```
230
+
231
+ The loop index is persisted in workflow memory. If the workflow is interrupted and resumes, it picks up where it left off.
232
+
233
+ ### Parallel Execution
234
+
235
+ ```ruby
236
+ parallel :notifications do
237
+ step :email, SendEmailActivity
238
+ step :sms, SendSmsActivity
239
+ step :push, SendPushActivity
240
+ end
241
+ ```
242
+
243
+ All steps within a `parallel` block run concurrently via ActiveJob. The workflow suspends until all complete.
244
+
245
+ ### Controlled Race Conditions
246
+
247
+ ```ruby
248
+ race :approval do
249
+ wait :manual_approval
250
+ wait :auto_approval, timeout: 24.hours
251
+ end
252
+ ```
253
+
254
+ The first signal to arrive wins. The other branches are canceled.
255
+
256
+ ### Wait States
257
+
258
+ ```ruby
259
+ # Wait indefinitely for a signal
260
+ wait :payment_received
261
+
262
+ # Wait with timeout
263
+ wait :payment_received, timeout: 1.hour
264
+
265
+ # Emit a signal elsewhere to resume that workflow
266
+ run.emit_signal(:payment_received, method: "applepay")
267
+ ```
268
+
269
+ ### Saga / Compensation
270
+
271
+ When an activity exhausts all retries, the workflow automatically compensates all previously completed steps in reverse order:
272
+
273
+ ```ruby
274
+ class ReserveInventory < RigidWorkflow::Activity
275
+ def perform(product_id:, quantity:, **)
276
+ { reserved: true }
277
+ end
278
+
279
+ def compensate
280
+ inventory.release(output[:product_id], output[:quantity])
281
+ end
282
+ end
283
+ ```
284
+
285
+ Compensation is called on each completed step's activity. If compensation itself fails, the run stays in `compensating` status for manual intervention.
286
+
287
+ ### Memoization
288
+
289
+ ```ruby
290
+ def run
291
+ user = memo(:current_user) { User.find(params[:user_id]) }
292
+ # User is cached in workflow memory across resumptions
293
+ end
294
+ ```
295
+
296
+ ### Versioning
297
+
298
+ ```ruby
299
+ class OrderWorkflow < RigidWorkflow::Workflow
300
+ version 2
301
+
302
+ def run
303
+ if @run_version < 2
304
+ # Legacy path for runs started before v2
305
+ else
306
+ # Current logic
307
+ end
308
+ end
309
+ end
310
+ ```
311
+
312
+ Set `version` in the workflow class. Each run captures the version at start time. Access it via `@run_version` for conditional logic.
313
+
314
+ ## Admin UI
315
+
316
+ Mount the engine in your routes:
317
+
318
+ ```ruby
319
+ # config/routes.rb
320
+ mount RigidWorkflow::Engine => "/admin/rigid_workflow"
321
+ ```
322
+
323
+ ### Pages
324
+
325
+ - **Overview** (`/admin/rigid_workflow`) - Stats table per workflow class: completed/active/pending/failed counts, success percentage, P50 duration
326
+ - **All Runs** (`/admin/rigid_workflow/runs`) - Paginated list with filtering by status (pending/active/completed/failed), clickable rows, select-all checkboxes, bulk action bar (retry/cancel selected runs)
327
+ - **Run Detail** (`/admin/rigid_workflow/runs/:id`) - Run metadata, step attempt history, and an interactive vis-timeline Gantt chart
328
+
329
+ ### Tech Stack
330
+
331
+ - Tailwind CSS v4 (CDN)
332
+ - Hotwire Turbo + Stimulus (importmap-managed)
333
+ - vis-timeline for interactive Gantt charts
334
+ - LocalTime for client-side time formatting
335
+ - Kaminari for pagination
336
+
337
+ ## Event Instrumentation
338
+
339
+ All key lifecycle events emit via `ActiveSupport::Notifications`:
340
+
341
+ ```ruby
342
+ # Subscribe to events
343
+ RigidWorkflow.on("workflow.complete") do |payload|
344
+ payload # => { run_id: "..." }
345
+ end
346
+
347
+ RigidWorkflow.on("step.fail") do |payload|
348
+ payload # => { run_id: "...", step_id: "..." }
349
+ end
350
+ ```
351
+
352
+ Available events: `workflow.start`, `workflow.complete`, `workflow.fail`, `step.complete`, `step.fail`, `step.retry`, `step.canceled`
353
+
354
+ ## Configuration
355
+
356
+ ```ruby
357
+ # config/initializers/rigid_workflow.rb
358
+ RigidWorkflow.configure do |config|
359
+ # REQUIRED for admin UI security. Controller class the admin UI inherits from.
360
+ config.admin_controller = "MyAdminController"
361
+
362
+ # Maximum retry attempts for failed activities (default: 3)
363
+ config.max_attempts = 3
364
+
365
+ # Base delay for exponential backoff retries in seconds (default: 15)
366
+ config.retry_delay = 15.seconds
367
+
368
+ # Enable logging output in all environments including test (default: false)
369
+ config.logging = true
370
+ end
371
+ ```
372
+
373
+ | Option | Default | Description |
374
+ | ------------------ | ------------ | ------------------------------------------------------------- |
375
+ | `admin_controller` | `nil` | Controller class for admin UI inheritance (required for auth) |
376
+ | `max_attempts` | `3` | Maximum retry attempts for failed activities |
377
+ | `retry_delay` | `15.seconds` | Base delay for exponential backoff (±20% jitter) |
378
+ | `logging` | `nil` | Enable workflow/activity log output even in test |
379
+
380
+ ## FAQ
381
+
382
+ ### 1. How does Rigid Workflow handle "zombie" processes?
383
+
384
+ If a workflow fails or crashes, the last known state remains in the database. When the next worker picks up the job, it continues exactly where it left off. If one of the activities fails, they will be retried automatically by the underlying job queue. Upon success or max attempts reached, the workflow will continue or fail respectively.
385
+
386
+ ### 2. What happens if my database goes down?
387
+
388
+ Since all state transitions are transactional within your Rails database, if the database is down, the workflow jobs will fail and be retried by the job queue. Once the database is back up, the workers will retry processing the enqueued jobs.
389
+
390
+ ### 3. How do I handle versioning of workflows?
391
+
392
+ Workflows are code. If you change a workflow definition while a run is in progress:
393
+
394
+ - **New steps**: Will be picked up as the workflow advances.
395
+ - **Removed steps**: If already completed, their results remain in history. If not yet reached, they are skipped.
396
+ - **Changed logic**: Will apply to all future steps of the currently running workflow.
397
+ For breaking changes, we recommend creating a new workflow class (e.g., `OrderWorkflowV2`).
398
+
399
+ ### 4. Can I use this for workflows that take months?
400
+
401
+ Yes. Because state is persisted in the database and execution is driven by job scheduling, signals, and timers, a workflow can sit in a waiting state indefinitely without consuming CPU or memory.
402
+
403
+ ### 5. How do I test these workflows?
404
+
405
+ You can test individual `Activity` classes in isolation or use integration tests to verify the entire `Workflow` flow. The project itself uses RSpec with an in-memory SQLite database, DatabaseCleaner, and automatic job performance for testing. See the `spec/` directory for examples.
406
+
407
+ ### 6. Is there a performance overhead?
408
+
409
+ Definitely, every `step` results in at least one database write to persist the state. This is the trade-off for durability. For high-throughput, sub-millisecond tasks, raw `ActiveJob` with Redis might be faster, but for business-critical processes where you cannot afford to lose state, the overhead is acceptable.
410
+
411
+ ### 7. How does it compare to Temporal, Sidekiq, or State Machines?
412
+
413
+ **Temporal**: Temporal is a separate system (Go/Java server) that requires a heavy infrastructure setup. Rigid Workflow is built on top of Rails and ActiveJob. It lives in your existing DB and uses your existing workers infrastructure.
414
+
415
+ **Sidekiq**: Sidekiq is a job queue. While you can chain jobs in Sidekiq, managing state, retries, and compensation across chains is manual and complex. Rigid Workflow provides a durable workflow orchestration engine. It is ready to go.
416
+
417
+ **State Machines (AASM)**: State machines track the status of a model (e.g., an Order). Rigid Workflow tracks the _process_ of a business flow. They are often used together: a Workflow might update an Order's state machine.
418
+
419
+ **DBOS**: is a multi-language (TypeScript, Python, Java, Kotlin) durable execution library that checkpoints workflow state to Postgres via decorators/annotations. Rigid Workflow is Rails-only and uses ActiveJob + your Rails database.
420
+
421
+ **When to choose Rigid Workflow**: You're building a Rails app and want a full-featured workflow engine with Saga, signals, race conditions, and an admin dashboard out of the box — all within your existing database and worker infrastructure.
422
+
423
+ ## License
424
+
425
+ [AGPL-3.0-or-later](./LICENSE.txt)
426
+
427
+ Rigid Workflow is licensed under the GNU Affero General Public License (AGPL) v3.0 or later. This means that you are allowed to modify the code and/or provide it as a Software-as-a-Service, but you are required to make your modifications available to the users of that service.
@@ -0,0 +1,68 @@
1
+ button {
2
+ cursor: pointer;
3
+ }
4
+
5
+ /* vis-timeline */
6
+
7
+ html .vis-timeline {
8
+ border: 1px solid var(--color-gray-200);
9
+ border-radius: 0.5rem;
10
+ }
11
+
12
+ html .vis-timeline .vis-top {
13
+ background-color: color-mix(in srgb, var(--color-gray-100), transparent 50%);
14
+ font-size: 0.9rem;
15
+ }
16
+
17
+ html .vis-timeline .vis-panel {
18
+ border-color: var(--color-gray-200);
19
+ }
20
+
21
+ html .vis-timeline .vis-top .vis-major {
22
+ font-weight: 500;
23
+ }
24
+
25
+ html .vis-timeline .vis-center {
26
+ background-image:
27
+ linear-gradient(to right, transparent 39px, #f1f5f9 1px),
28
+ linear-gradient(to bottom, transparent 39px, #f1f5f9 1px);
29
+ background-size: 40px 40px;
30
+ background-color: #ffffff;
31
+ }
32
+
33
+ html .vis-item .vis-item-overflow {
34
+ overflow: visible;
35
+ }
36
+
37
+ html .vis-item .vis-item-content {
38
+ font-size: 0.8rem;
39
+ position: relative;
40
+ padding-right: 14px;
41
+ }
42
+
43
+ html .vis-item .vis-item-content::after {
44
+ content: "";
45
+ position: absolute;
46
+ right: 4px;
47
+ top: 50%;
48
+ transform: translateY(-50%);
49
+ width: 5px;
50
+ height: 5px;
51
+ }
52
+
53
+ html .vis-item.timeline-status-completed .vis-item-content::after {
54
+ background-color: #16a34a;
55
+ }
56
+
57
+ html .vis-item.timeline-status-failed .vis-item-content::after {
58
+ background-color: #dc2626;
59
+ }
60
+
61
+ html .vis-item.timeline-status-pending .vis-item-content::after,
62
+ html .vis-item.timeline-status-running .vis-item-content::after {
63
+ background-color: #9ca3af;
64
+ }
65
+
66
+ html .vis-item.timeline-status-canceled .vis-item-content::after {
67
+ background-color: #6b7280;
68
+ }
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Namespace for RigidWorkflow controllers.
4
+ module RigidWorkflow
5
+ parent_class =
6
+ begin
7
+ RigidWorkflow.config.admin_controller.constantize
8
+ rescue NameError
9
+ raise ArgumentError,
10
+ "Please set config.admin_controller in RigidWorkflow initializer."
11
+ end
12
+
13
+ # Base controller for RigidWorkflow admin interface.
14
+ # Inherits from the configured admin controller and provides shared functionality.
15
+ class ApplicationController < parent_class
16
+ include Rails.application.routes.url_helpers
17
+ include LocalTimeHelper
18
+
19
+ protect_from_forgery with: :exception
20
+
21
+ layout "rigid_workflow/layouts/application"
22
+
23
+ def default_url_options
24
+ {
25
+ host: ENV.fetch("HOST", "localhost"),
26
+ protocol: ENV.fetch("PROTOCOL", "http")
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ before_action :set_admin_sidebar
33
+
34
+ def set_admin_sidebar
35
+ @sidebar = [
36
+ {
37
+ heading: "Rigid Workflow",
38
+ items: [
39
+ { name: "Overview", path: overview_path },
40
+ {
41
+ name: "Completed runs",
42
+ count: RigidWorkflow::Run.where(status: %i[completed]).count,
43
+ path: completed_runs_path
44
+ },
45
+ {
46
+ name: "Active runs",
47
+ count: RigidWorkflow::Run.where(status: %i[running]).count,
48
+ path: active_runs_path
49
+ },
50
+ {
51
+ name: "Pending runs",
52
+ count: RigidWorkflow::Run.where(status: %i[pending]).count,
53
+ path: pending_runs_path
54
+ },
55
+ {
56
+ name: "Failed runs",
57
+ count: RigidWorkflow::Run.where(status: %i[failed]).count,
58
+ path: failed_runs_path
59
+ }
60
+ ]
61
+ }
62
+ ]
63
+ end
64
+
65
+ def self.stats_for_ever
66
+ ActiveRecord::Base.connection.execute(<<-SQL)
67
+ SELECT
68
+ r.workflow_class,
69
+ COUNT(DISTINCT CASE WHEN r.status = 'completed' THEN r.id END) as completed_runs,
70
+ COUNT(DISTINCT CASE WHEN r.status = 'running' THEN r.id END) as active_runs,
71
+ COUNT(DISTINCT CASE WHEN r.status = 'pending' THEN r.id END) as pending_runs,
72
+ COUNT(DISTINCT CASE WHEN r.status = 'failed' THEN r.id END) as failed_runs,
73
+ 100.0 * COUNT(DISTINCT CASE WHEN r.status = 'completed' THEN r.id END) /
74
+ NULLIF(COUNT(DISTINCT CASE WHEN r.status IN ('completed', 'failed') THEN r.id END), 0) as success_rate,
75
+ PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (r.finished_at - r.started_at)) as p50_duration
76
+ FROM rigid_workflow_runs r
77
+ GROUP BY r.workflow_class
78
+ SQL
79
+ end
80
+
81
+ def self.unique_workflows_in_ever
82
+ ActiveRecord::Base.connection.execute(<<-SQL)
83
+ SELECT
84
+ DISTINCT(r.workflow_class)
85
+ FROM rigid_workflow_runs r
86
+ GROUP BY r.workflow_class
87
+ SQL
88
+ end
89
+ end
90
+ end