solid_queue_web 0.7.0 → 0.9.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 +26 -15
- data/app/assets/stylesheets/solid_queue_web/_02_layout.css +29 -1
- data/app/assets/stylesheets/solid_queue_web/_04_table.css +8 -1
- data/app/assets/stylesheets/solid_queue_web/_05_badges.css +1 -0
- data/app/assets/stylesheets/solid_queue_web/_11_throughput.css +30 -1
- data/app/assets/stylesheets/solid_queue_web/_12_dark_mode.css +34 -0
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +11 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +1 -22
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -22
- data/app/controllers/solid_queue_web/history_controller.rb +20 -1
- data/app/controllers/solid_queue_web/jobs_controller.rb +38 -23
- data/app/controllers/solid_queue_web/queues/jobs_controller.rb +15 -19
- data/app/controllers/solid_queue_web/queues/pauses_controller.rb +21 -0
- data/app/controllers/solid_queue_web/queues_controller.rb +5 -31
- data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +31 -0
- data/app/controllers/solid_queue_web/search_controller.rb +1 -3
- data/app/javascript/solid_queue_web/application.js +2 -0
- data/app/javascript/solid_queue_web/theme_controller.js +26 -0
- data/app/services/solid_queue_web/dashboard_stats.rb +47 -0
- data/app/services/solid_queue_web/queue_stats.rb +52 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +11 -7
- data/app/views/solid_queue_web/dashboard/index.html.erb +89 -23
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +3 -1
- data/app/views/solid_queue_web/history/index.html.erb +8 -2
- data/app/views/solid_queue_web/jobs/index.html.erb +27 -10
- data/app/views/solid_queue_web/processes/index.html.erb +1 -1
- data/app/views/solid_queue_web/queues/index.html.erb +19 -2
- data/app/views/solid_queue_web/queues/jobs/index.html.erb +1 -1
- data/app/views/solid_queue_web/search/index.html.erb +3 -3
- data/config/importmap.rb +1 -0
- data/config/routes.rb +7 -9
- data/lib/solid_queue_web/engine.rb +4 -2
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +27 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 462f81d84dead7a68833c768775eda6cb0b55b66862d58fa1b49b504fa417644
|
|
4
|
+
data.tar.gz: cc388f882d5709e8d92a778a2c6ccaa39d04022c62c9bc7e89320e416c5148d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '087a8cb99798a1d21df1347008380c58242a98247d48b80cba0865c4bb0e46e800d37a2c21eb887fc516c6c724c5e138ebbc8a517dc62e730b1d2ea08fe6b5d3'
|
|
7
|
+
data.tar.gz: 3885a114fa3f31db4b949a3b397070734b76c6e447d82bc7cd5e67628163760e03a2b70e90c1ef532143170752430de6ba1dd5a18fa0c52ff379462e90dc8d30
|
data/README.md
CHANGED
|
@@ -33,8 +33,8 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
|
|
|
33
33
|
|
|
34
34
|
## Features
|
|
35
35
|
|
|
36
|
-
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; "Done (1h)" and "Done (24h)" throughput cards; a "Throughput — Last 12 Hours" bar chart showing
|
|
37
|
-
- **Queues** — all queues sorted by name with size; oldest ready job latency (color-coded, with UTC timestamp tooltip); Done (24h) and Failed (24h) throughput counts; pause/resume controls
|
|
36
|
+
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; "Done (1h)" and "Done (24h)" throughput cards; a "Throughput — Last 12 Hours" bar chart (blue) and a "Queue Depth — Last 12 Hours" bar chart (purple) showing hourly snapshots of active job count; pure CSS, no charting library; auto-refreshes every 5 seconds
|
|
37
|
+
- **Queues** — all queues sorted by name with size; oldest ready job latency (color-coded, with UTC timestamp tooltip); Done (24h) and Failed (24h) throughput counts; a mini 12-bar failure rate sparkline per queue showing failure % per hour over the last 12 hours; pause/resume controls
|
|
38
38
|
- **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; search by job class name with dynamic auto-submit; time-based period filter (1 h / 24 h / 7 d); discard individual or all jobs; Turbo Frame navigation so only the table updates on filter or search; auto-refreshes every 10 seconds
|
|
39
39
|
- **Failed jobs** — list of failed executions with error details; search by class name; filter by queue; time-based period filter; retry or discard individually or in bulk
|
|
40
40
|
- **Job detail** — full arguments, timestamps, blocked-until date, and error backtrace; action buttons based on job status
|
|
@@ -44,6 +44,10 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
|
|
|
44
44
|
- **Global search** — search across all job statuses at once by class name substring; results grouped by status with match count and direct links to filtered views; native datalist autocomplete pre-populated from all known job classes; auto-submits on selection
|
|
45
45
|
- **Targeted bulk actions** — checkboxes on the jobs and failed jobs lists for selecting individual rows; selection bar shows count and action buttons ("Discard Selected" for jobs, "Retry Selected" / "Discard Selected" for failed jobs); select-all checkbox in the table header
|
|
46
46
|
- **Job history** — browsable list of all finished jobs with class name, queue, duration, and finished timestamp; filterable by period (1h / 24h / 7d), queue, and class name search; Done (1h) / Done (24h) dashboard cards link directly to the filtered history view; auto-refreshes every 10 seconds
|
|
47
|
+
- **Dark mode** — ☽/☀ toggle in the header; preference persists to `localStorage` and defaults to the OS `prefers-color-scheme` on first visit; zero extra dependencies — implemented via CSS custom properties and a small Stimulus controller
|
|
48
|
+
- **Dashboard quick actions** — "Retry All Failed" and "Discard All Blocked" cards appear on the dashboard only when the respective count is non-zero; one-click bulk operations with confirm dialogs, keeping the dashboard clean when everything is healthy
|
|
49
|
+
- **CSV export** — "Export CSV" button on the jobs, failed jobs, and history pages downloads all records matching the current filters; columns are tailored per view
|
|
50
|
+
- **Slow job detection** — when `slow_job_threshold` is configured, claimed jobs running longer than the threshold are flagged with an orange row, a "slow" badge, and a "Running For" duration column on the Running tab; a "Slow Jobs" warning card appears on the dashboard with a link to the Running tab
|
|
47
51
|
|
|
48
52
|
## Screenshots
|
|
49
53
|
|
|
@@ -81,13 +85,21 @@ Add to your `config/routes.rb`:
|
|
|
81
85
|
mount SolidQueueWeb::Engine, at: "/jobs"
|
|
82
86
|
```
|
|
83
87
|
|
|
84
|
-
The dashboard will be available at `/jobs`.
|
|
88
|
+
The dashboard will be available at `/jobs`.
|
|
85
89
|
|
|
86
|
-
##
|
|
90
|
+
## Configuration
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
All settings are optional — the dashboard works with zero configuration. Create `config/initializers/solid_queue_web.rb` to customize behavior:
|
|
89
93
|
|
|
90
94
|
```ruby
|
|
95
|
+
SolidQueueWeb.configure do |config|
|
|
96
|
+
config.page_size = 50 # rows per page across all paginated views (default: 25)
|
|
97
|
+
config.dashboard_refresh_interval = 10_000 # dashboard auto-refresh in ms (default: 5_000)
|
|
98
|
+
config.default_refresh_interval = 30_000 # jobs/processes/history auto-refresh in ms (default: 10_000)
|
|
99
|
+
config.search_results_limit = 10 # max results per status in global search (default: 25)
|
|
100
|
+
config.slow_job_threshold = 5.minutes # flag claimed jobs running longer than this (default: nil = disabled)
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
SolidQueueWeb.authenticate do
|
|
92
104
|
# Called in the context of ApplicationController — use any helper available there.
|
|
93
105
|
# Return a truthy value to allow access, falsy to deny (triggers HTTP Basic prompt).
|
|
@@ -95,22 +107,21 @@ SolidQueueWeb.authenticate do
|
|
|
95
107
|
end
|
|
96
108
|
```
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback.
|
|
99
111
|
|
|
100
112
|
## Roadmap
|
|
101
113
|
|
|
102
|
-
Planned features, roughly ordered by priority:
|
|
103
|
-
|
|
104
|
-
**Near-term**
|
|
105
|
-
- Dark mode — CSS custom properties are already structured for it; toggle persists to `localStorage`
|
|
114
|
+
Planned features, roughly ordered by priority:
|
|
106
115
|
|
|
107
|
-
**
|
|
108
|
-
-
|
|
109
|
-
-
|
|
116
|
+
**Operations**
|
|
117
|
+
- Scheduled job management — reschedule a job to run immediately, or push its `scheduled_at` forward
|
|
118
|
+
- Bulk retry with delay — retry all failed jobs with a configurable stagger to avoid thundering herd
|
|
119
|
+
- Admin audit log — record who retried or discarded which jobs and when (requires host-app user identity)
|
|
110
120
|
|
|
111
|
-
**
|
|
112
|
-
- CSV export of any filtered view (jobs, failed jobs, history)
|
|
121
|
+
**Infrastructure**
|
|
113
122
|
- Webhook / alert config — POST to a URL when the failure count exceeds a threshold
|
|
123
|
+
- Multi-database support — when Solid Queue runs on a separate database from the host app
|
|
124
|
+
- Read replica support — route dashboard queries to a replica to avoid impacting the primary
|
|
114
125
|
|
|
115
126
|
Pull requests for any of these are welcome. See [Contributing](#contributing) below.
|
|
116
127
|
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
height: 56px;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
.sqd-header__controls {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 0.5rem;
|
|
20
|
+
margin-left: auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
.sqd-header__title {
|
|
17
24
|
font-size: 16px;
|
|
18
25
|
font-weight: 600;
|
|
@@ -43,6 +50,28 @@
|
|
|
43
50
|
color: var(--text);
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
.sqd-theme-toggle {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
width: 32px;
|
|
58
|
+
height: 32px;
|
|
59
|
+
padding: 0;
|
|
60
|
+
background: none;
|
|
61
|
+
border: 1px solid var(--border);
|
|
62
|
+
border-radius: 5px;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
font-size: 16px;
|
|
65
|
+
color: var(--text);
|
|
66
|
+
line-height: 1;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
transition: background 0.1s, border-color 0.1s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sqd-theme-toggle:hover {
|
|
72
|
+
background: var(--bg);
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
.sqd-nav-toggle {
|
|
47
76
|
display: none;
|
|
48
77
|
flex-direction: column;
|
|
@@ -51,7 +80,6 @@
|
|
|
51
80
|
width: 36px;
|
|
52
81
|
height: 36px;
|
|
53
82
|
padding: 6px;
|
|
54
|
-
margin-left: auto;
|
|
55
83
|
background: none;
|
|
56
84
|
border: 1px solid var(--border);
|
|
57
85
|
border-radius: 5px;
|
|
@@ -44,9 +44,16 @@ td {
|
|
|
44
44
|
|
|
45
45
|
tr:last-child td { border-bottom: none; }
|
|
46
46
|
tbody tr:hover { background: var(--bg); }
|
|
47
|
+
.sqd-table-link,
|
|
48
|
+
.sqd-table-link:hover,
|
|
49
|
+
.sqd-table-link:visited { text-decoration: none; color: var(--primary); }
|
|
47
50
|
|
|
48
51
|
.sqd-empty {
|
|
49
52
|
text-align: center;
|
|
50
53
|
padding: 3rem 1rem;
|
|
51
54
|
color: var(--muted);
|
|
52
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.sqd-row--slow { background: rgba(253, 126, 20, 0.07); }
|
|
58
|
+
.sqd-row--slow:hover { background: rgba(253, 126, 20, 0.13); }
|
|
59
|
+
.sqd-slow-duration { color: var(--warning); font-weight: 600; }
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
.sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; }
|
|
22
22
|
.sqd-badge--worker { background: #d1e7dd; color: #0f5132; }
|
|
23
23
|
.sqd-badge--dispatcher { background: #cff4fc; color: #055160; }
|
|
24
|
+
.sqd-badge--slow { background: #ffe8cc; color: #7c3d00; }
|
|
24
25
|
|
|
25
26
|
.sqd-process-meta { font-size: 12px; color: var(--muted); }
|
|
26
27
|
.sqd-process-meta span + span::before { content: " · "; }
|
|
@@ -65,4 +65,33 @@
|
|
|
65
65
|
padding: 1rem 1.25rem;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
.sqd-stat--done .sqd-stat__value { color: var(--success); }
|
|
68
|
+
.sqd-stat--done .sqd-stat__value { color: var(--success); }
|
|
69
|
+
|
|
70
|
+
.sqd-mini-sparkline {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: flex-end;
|
|
73
|
+
gap: 2px;
|
|
74
|
+
height: 28px;
|
|
75
|
+
width: 88px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.sqd-mini-sparkline__bar {
|
|
79
|
+
flex: 1;
|
|
80
|
+
background: var(--danger);
|
|
81
|
+
border-radius: 1px 1px 0 0;
|
|
82
|
+
opacity: 0.7;
|
|
83
|
+
transition: opacity 0.15s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sqd-mini-sparkline__bar:hover {
|
|
87
|
+
opacity: 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sqd-mini-sparkline__bar--empty {
|
|
91
|
+
background: var(--border);
|
|
92
|
+
opacity: 0.5;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sqd-sparkline__bar--depth {
|
|
96
|
+
background: var(--purple);
|
|
97
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[data-theme="dark"] {
|
|
2
|
+
--bg: #0d1117;
|
|
3
|
+
--surface: #161b22;
|
|
4
|
+
--border: #30363d;
|
|
5
|
+
--text: #e6edf3;
|
|
6
|
+
--muted: #8b949e;
|
|
7
|
+
--primary: #58a6ff;
|
|
8
|
+
--danger: #f85149;
|
|
9
|
+
--warning: #d29922;
|
|
10
|
+
--success: #3fb950;
|
|
11
|
+
--info: #39c5cf;
|
|
12
|
+
--purple: #bc8cff;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[data-theme="dark"] .sqd-badge--ready { background: #1b3a2b; color: #3fb950; }
|
|
16
|
+
[data-theme="dark"] .sqd-badge--scheduled { background: #0e2a33; color: #39c5cf; }
|
|
17
|
+
[data-theme="dark"] .sqd-badge--claimed { background: #112040; color: #58a6ff; }
|
|
18
|
+
[data-theme="dark"] .sqd-badge--failed { background: #3d1118; color: #f85149; }
|
|
19
|
+
[data-theme="dark"] .sqd-badge--blocked { background: #2d2010; color: #d29922; }
|
|
20
|
+
[data-theme="dark"] .sqd-badge--static { background: #1b3a2b; color: #3fb950; }
|
|
21
|
+
[data-theme="dark"] .sqd-badge--dynamic { background: #2c1f45; color: #bc8cff; }
|
|
22
|
+
[data-theme="dark"] .sqd-badge--paused { background: #2d2d2d; color: #8b949e; }
|
|
23
|
+
[data-theme="dark"] .sqd-badge--running { background: #1b3a2b; color: #3fb950; }
|
|
24
|
+
[data-theme="dark"] .sqd-badge--supervisor { background: #2c1f45; color: #bc8cff; }
|
|
25
|
+
[data-theme="dark"] .sqd-badge--worker { background: #1b3a2b; color: #3fb950; }
|
|
26
|
+
[data-theme="dark"] .sqd-badge--dispatcher { background: #0e2a33; color: #39c5cf; }
|
|
27
|
+
|
|
28
|
+
[data-theme="dark"] .sqd-flash--notice { background: #1b3a2b; color: #3fb950; border-color: #2d6a4f; }
|
|
29
|
+
[data-theme="dark"] .sqd-flash--alert { background: #3d1118; color: #f85149; border-color: #6a2030; }
|
|
30
|
+
|
|
31
|
+
[data-theme="dark"] .sqd-theme-toggle {
|
|
32
|
+
border-color: var(--border);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class BlockedJobsController < ApplicationController
|
|
3
|
+
def destroy
|
|
4
|
+
jobs = SolidQueue::BlockedExecution.includes(:job).map(&:job)
|
|
5
|
+
SolidQueue::BlockedExecution.discard_all_from_jobs(jobs)
|
|
6
|
+
redirect_to root_path, notice: "#{jobs.size} blocked #{"job".pluralize(jobs.size)} discarded."
|
|
7
|
+
rescue => e
|
|
8
|
+
redirect_to root_path, alert: "Could not discard blocked jobs: #{e.message}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class DashboardController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
-
@stats =
|
|
5
|
-
ready: SolidQueue::ReadyExecution.count,
|
|
6
|
-
scheduled: SolidQueue::ScheduledExecution.count,
|
|
7
|
-
claimed: SolidQueue::ClaimedExecution.count,
|
|
8
|
-
failed: SolidQueue::FailedExecution.count,
|
|
9
|
-
blocked: SolidQueue::BlockedExecution.count,
|
|
10
|
-
queues: SolidQueue::Job.select(:queue_name).distinct.count,
|
|
11
|
-
processes: SolidQueue::Process.count,
|
|
12
|
-
recurring: SolidQueue::RecurringTask.count
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
now = Time.current
|
|
16
|
-
finished_times = SolidQueue::Job.where(finished_at: 24.hours.ago..now).pluck(:finished_at)
|
|
17
|
-
@throughput = {
|
|
18
|
-
completed_1h: finished_times.count { |t| t >= 1.hour.ago },
|
|
19
|
-
completed_24h: finished_times.size
|
|
20
|
-
}
|
|
21
|
-
@sparkline = 12.times.map do |i|
|
|
22
|
-
from = (12 - i).hours.ago
|
|
23
|
-
to = i == 11 ? now : (11 - i).hours.ago
|
|
24
|
-
finished_times.count { |t| t >= from && t < to }
|
|
25
|
-
end
|
|
4
|
+
@stats = DashboardStats.new
|
|
26
5
|
end
|
|
27
6
|
end
|
|
28
7
|
end
|
|
@@ -1,43 +1,47 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class FailedJobsController < ApplicationController
|
|
3
|
-
before_action :set_filter_params, only: [:index, :
|
|
3
|
+
before_action :set_filter_params, only: [:index, :destroy]
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
|
|
6
|
+
respond_to do |format|
|
|
7
|
+
format.html { @pagy, @failed_jobs = pagy(filtered_scope.order(created_at: :desc)) }
|
|
8
|
+
format.csv do
|
|
9
|
+
send_data failed_jobs_csv,
|
|
10
|
+
filename: "failed-jobs-#{Date.today}.csv",
|
|
11
|
+
type: "text/csv", disposition: "attachment"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def destroy
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
redirect_to failed_jobs_path, notice: "Job discarded."
|
|
17
|
+
executions = params[:id] ? [SolidQueue::FailedExecution.find(params[:id])] : filtered_scope.to_a
|
|
18
|
+
perform_discard(executions)
|
|
21
19
|
rescue => e
|
|
22
20
|
redirect_to failed_jobs_path, alert: "Could not discard job: #{e.message}"
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def failed_jobs_csv
|
|
26
|
+
CSV.generate(headers: true) do |csv|
|
|
27
|
+
csv << %w[id class_name queue_name error_class error_message failed_at]
|
|
28
|
+
filtered_scope.order(created_at: :desc).each do |execution|
|
|
29
|
+
job = execution.job
|
|
30
|
+
error = execution.error || {}
|
|
31
|
+
csv << [job.id, job.class_name, job.queue_name,
|
|
32
|
+
error["exception_class"], error["message"],
|
|
33
|
+
execution.created_at.iso8601]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
def
|
|
33
|
-
jobs =
|
|
38
|
+
def perform_discard(executions)
|
|
39
|
+
jobs = executions.map(&:job)
|
|
34
40
|
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
|
|
35
41
|
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
36
42
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
37
43
|
end
|
|
38
44
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
45
|
def set_filter_params
|
|
42
46
|
@queue = params[:queue].presence
|
|
43
47
|
@search = params[:q].presence
|
|
@@ -10,7 +10,26 @@ module SolidQueueWeb
|
|
|
10
10
|
scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
11
|
scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
respond_to do |format|
|
|
14
|
+
format.html { @pagy, @jobs = pagy(scope.order(finished_at: :desc)) }
|
|
15
|
+
format.csv do
|
|
16
|
+
send_data history_csv(scope),
|
|
17
|
+
filename: "job-history-#{Date.today}.csv",
|
|
18
|
+
type: "text/csv", disposition: "attachment"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def history_csv(scope)
|
|
26
|
+
CSV.generate(headers: true) do |csv|
|
|
27
|
+
csv << %w[id class_name queue_name duration_seconds finished_at]
|
|
28
|
+
scope.order(finished_at: :desc).each do |job|
|
|
29
|
+
duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
|
|
30
|
+
csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
14
33
|
end
|
|
15
34
|
end
|
|
16
35
|
end
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
before_action :set_status, only: [:destroy, :
|
|
3
|
+
before_action :set_status, only: [:destroy, :discard_selected]
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
6
|
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
7
7
|
@search = params[:q].presence
|
|
8
8
|
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
scope = Job::EXECUTION_MODELS[@status].includes(:job)
|
|
10
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
12
|
+
scope = scope.order(created_at: :desc)
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html { @pagy, @jobs = pagy(scope) }
|
|
16
|
+
format.csv do
|
|
17
|
+
send_data jobs_csv(scope),
|
|
18
|
+
filename: "jobs-#{@status}-#{Date.today}.csv",
|
|
19
|
+
type: "text/csv", disposition: "attachment"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def show
|
|
@@ -21,33 +30,39 @@ module SolidQueueWeb
|
|
|
21
30
|
|
|
22
31
|
def destroy
|
|
23
32
|
model = execution_model_for!(@status)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
format
|
|
29
|
-
|
|
33
|
+
if params[:id]
|
|
34
|
+
@execution = model.find(params[:id])
|
|
35
|
+
@execution.discard
|
|
36
|
+
@remaining_count = filtered_scope(model).count
|
|
37
|
+
respond_to do |format|
|
|
38
|
+
format.turbo_stream
|
|
39
|
+
format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
jobs = filtered_scope(model).map(&:job)
|
|
43
|
+
model.discard_all_from_jobs(jobs)
|
|
44
|
+
redirect_to jobs_path(status: @status, period: @period),
|
|
45
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
30
46
|
end
|
|
31
47
|
rescue ArgumentError => e
|
|
32
48
|
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
33
49
|
rescue => e
|
|
34
|
-
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard job: #{e.message}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def discard_all
|
|
38
|
-
model = execution_model_for!(@status)
|
|
39
|
-
jobs = filtered_scope(model).map(&:job)
|
|
40
|
-
model.discard_all_from_jobs(jobs)
|
|
41
50
|
redirect_to jobs_path(status: @status, period: @period),
|
|
42
|
-
|
|
43
|
-
rescue ArgumentError => e
|
|
44
|
-
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
45
|
-
rescue => e
|
|
46
|
-
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
|
|
51
|
+
alert: "Could not discard #{params[:id] ? "job" : "jobs"}: #{e.message}"
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
private
|
|
50
55
|
|
|
56
|
+
def jobs_csv(scope)
|
|
57
|
+
CSV.generate(headers: true) do |csv|
|
|
58
|
+
csv << %w[id class_name queue_name status priority enqueued_at]
|
|
59
|
+
scope.each do |execution|
|
|
60
|
+
job = execution.job
|
|
61
|
+
csv << [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
51
66
|
def derive_status(job)
|
|
52
67
|
return "failed" if job.failed_execution.present?
|
|
53
68
|
return "claimed" if job.claimed_execution.present?
|
|
@@ -2,7 +2,7 @@ module SolidQueueWeb
|
|
|
2
2
|
module Queues
|
|
3
3
|
class JobsController < ApplicationController
|
|
4
4
|
before_action :set_queue
|
|
5
|
-
before_action :set_status, only: [:destroy
|
|
5
|
+
before_action :set_status, only: [:destroy]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
@@ -15,29 +15,25 @@ module SolidQueueWeb
|
|
|
15
15
|
|
|
16
16
|
def destroy
|
|
17
17
|
model = execution_model_for!(@status)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
format
|
|
23
|
-
|
|
18
|
+
if params[:id]
|
|
19
|
+
@execution = model.find(params[:id])
|
|
20
|
+
@execution.discard
|
|
21
|
+
@remaining_count = filtered_scope(model).count
|
|
22
|
+
respond_to do |format|
|
|
23
|
+
format.turbo_stream
|
|
24
|
+
format.html { redirect_to queue_jobs_path(queue_name: @queue, status: @status), notice: "Job discarded." }
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
jobs = filtered_scope(model).map(&:job)
|
|
28
|
+
model.discard_all_from_jobs(jobs)
|
|
29
|
+
redirect_to queue_jobs_path(queue_name: @queue, status: @status),
|
|
30
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
24
31
|
end
|
|
25
32
|
rescue ArgumentError => e
|
|
26
33
|
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
|
|
27
34
|
rescue => e
|
|
28
|
-
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: "Could not discard job: #{e.message}"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def discard_all
|
|
32
|
-
model = execution_model_for!(@status)
|
|
33
|
-
jobs = filtered_scope(model).map(&:job)
|
|
34
|
-
model.discard_all_from_jobs(jobs)
|
|
35
35
|
redirect_to queue_jobs_path(queue_name: @queue, status: @status),
|
|
36
|
-
|
|
37
|
-
rescue ArgumentError => e
|
|
38
|
-
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
|
|
39
|
-
rescue => e
|
|
40
|
-
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: "Could not discard jobs: #{e.message}"
|
|
36
|
+
alert: "Could not discard #{params[:id] ? "job" : "jobs"}: #{e.message}"
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
private
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
module Queues
|
|
3
|
+
class PausesController < ApplicationController
|
|
4
|
+
def create
|
|
5
|
+
queue = SolidQueue::Queue.find_by_name(params[:queue_name])
|
|
6
|
+
queue.pause
|
|
7
|
+
redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused."
|
|
8
|
+
rescue => e
|
|
9
|
+
redirect_to queues_path, alert: "Could not pause queue: #{e.message}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def destroy
|
|
13
|
+
queue = SolidQueue::Queue.find_by_name(params[:queue_name])
|
|
14
|
+
queue.resume
|
|
15
|
+
redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed."
|
|
16
|
+
rescue => e
|
|
17
|
+
redirect_to queues_path, alert: "Could not resume queue: #{e.message}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -2,37 +2,11 @@ module SolidQueueWeb
|
|
|
2
2
|
class QueuesController < ApplicationController
|
|
3
3
|
def index
|
|
4
4
|
@queues = SolidQueue::Queue.all.sort_by(&:name)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.count
|
|
11
|
-
@failed_24h = SolidQueue::FailedExecution
|
|
12
|
-
.joins(:job)
|
|
13
|
-
.where(created_at: 24.hours.ago..now)
|
|
14
|
-
.group("solid_queue_jobs.queue_name")
|
|
15
|
-
.count
|
|
16
|
-
@oldest_ready = SolidQueue::ReadyExecution
|
|
17
|
-
.joins(:job)
|
|
18
|
-
.group("solid_queue_jobs.queue_name")
|
|
19
|
-
.minimum("solid_queue_jobs.created_at")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def pause
|
|
23
|
-
queue = SolidQueue::Queue.find_by_name(params[:name])
|
|
24
|
-
queue.pause
|
|
25
|
-
redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused."
|
|
26
|
-
rescue => e
|
|
27
|
-
redirect_to queues_path, alert: "Could not pause queue: #{e.message}"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def resume
|
|
31
|
-
queue = SolidQueue::Queue.find_by_name(params[:name])
|
|
32
|
-
queue.resume
|
|
33
|
-
redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed."
|
|
34
|
-
rescue => e
|
|
35
|
-
redirect_to queues_path, alert: "Could not resume queue: #{e.message}"
|
|
5
|
+
stats = QueueStats.new(@queues)
|
|
6
|
+
@completed_24h = stats.completed_24h
|
|
7
|
+
@failed_24h = stats.failed_24h
|
|
8
|
+
@oldest_ready = stats.oldest_ready
|
|
9
|
+
@failure_sparklines = stats.failure_sparklines
|
|
36
10
|
end
|
|
37
11
|
end
|
|
38
12
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class RetryFailedJobsController < ApplicationController
|
|
3
|
+
before_action :set_filter_params
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
executions = params[:id] ? [SolidQueue::FailedExecution.find(params[:id])] : filtered_scope.to_a
|
|
7
|
+
jobs = executions.map(&:job)
|
|
8
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
9
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
10
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
|
|
11
|
+
rescue => e
|
|
12
|
+
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_filter_params
|
|
18
|
+
@queue = params[:queue].presence
|
|
19
|
+
@search = params[:q].presence
|
|
20
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def filtered_scope
|
|
24
|
+
scope = SolidQueue::FailedExecution.includes(:job)
|
|
25
|
+
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
|
|
26
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
27
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
28
|
+
scope
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class SearchController < ApplicationController
|
|
3
|
-
LIMIT = 25
|
|
4
|
-
|
|
5
3
|
def index
|
|
6
4
|
@query = params[:q].presence
|
|
7
5
|
@job_classes = SolidQueue::Job.distinct.order(:class_name).pluck(:class_name)
|
|
@@ -15,7 +13,7 @@ module SolidQueueWeb
|
|
|
15
13
|
.where("solid_queue_jobs.class_name LIKE ?", "%#{@query}%")
|
|
16
14
|
.order(created_at: :desc)
|
|
17
15
|
total = scope.count
|
|
18
|
-
executions = scope.limit(
|
|
16
|
+
executions = scope.limit(SolidQueueWeb.search_results_limit).to_a
|
|
19
17
|
@results[status] = { executions: executions, total: total } unless executions.empty?
|
|
20
18
|
end
|
|
21
19
|
end
|