sidekiq-assured-jobs 1.0.0 → 1.1.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/CHANGELOG.md +35 -0
- data/README.md +67 -0
- data/examples/web_demo.rb +151 -0
- data/lib/sidekiq/assured_jobs/version.rb +1 -1
- data/lib/sidekiq/assured_jobs/web.rb +258 -0
- data/lib/sidekiq-assured-jobs.rb +139 -10
- data/web/assets/orphaned_jobs.css +313 -0
- data/web/views/orphaned_job.erb +296 -0
- data/web/views/orphaned_jobs.erb +301 -0
- metadata +21 -2
@@ -0,0 +1,296 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<div class="col-sm-8">
|
3
|
+
<h3>
|
4
|
+
Orphaned Job Details
|
5
|
+
<small class="text-muted"><%= @job['jid'] %></small>
|
6
|
+
</h3>
|
7
|
+
</div>
|
8
|
+
<div class="col-sm-4 text-right">
|
9
|
+
<div class="btn-group" role="group">
|
10
|
+
<a href="<%= root_path %>orphaned-jobs" class="btn btn-secondary btn-sm">
|
11
|
+
← Back to List
|
12
|
+
</a>
|
13
|
+
<form method="post" action="<%= root_path %>orphaned-jobs/<%= @job['jid'] %>/retry" style="display: inline;">
|
14
|
+
<%= csrf_tag %>
|
15
|
+
<button type="submit" class="btn btn-warning btn-sm">
|
16
|
+
↻ Retry Job
|
17
|
+
</button>
|
18
|
+
</form>
|
19
|
+
<form method="post" action="<%= root_path %>orphaned-jobs/<%= @job['jid'] %>/delete" style="display: inline;"
|
20
|
+
onsubmit="return confirm('Are you sure you want to delete this orphaned job?')">
|
21
|
+
<%= csrf_tag %>
|
22
|
+
<button type="submit" class="btn btn-danger btn-sm">
|
23
|
+
✕ Delete Job
|
24
|
+
</button>
|
25
|
+
</form>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
</header>
|
29
|
+
|
30
|
+
<div class="row">
|
31
|
+
<div class="col-sm-12">
|
32
|
+
<div class="alert alert-warning">
|
33
|
+
<strong>Orphaned Job:</strong> This job was being processed when its worker instance
|
34
|
+
(<code><%= @job['instance_id'] %></code>) crashed or was terminated.
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div class="row">
|
40
|
+
<div class="col-md-6">
|
41
|
+
<div class="card">
|
42
|
+
<div class="card-header">
|
43
|
+
<h5 class="card-title mb-0">Job Information</h5>
|
44
|
+
</div>
|
45
|
+
<div class="card-body">
|
46
|
+
<table class="table table-sm">
|
47
|
+
<tbody>
|
48
|
+
<tr>
|
49
|
+
<th width="30%">Job ID:</th>
|
50
|
+
<td><code><%= @job['jid'] %></code></td>
|
51
|
+
</tr>
|
52
|
+
<tr>
|
53
|
+
<th>Class:</th>
|
54
|
+
<td><code><%= @job['class'] %></code></td>
|
55
|
+
</tr>
|
56
|
+
<tr>
|
57
|
+
<th>Queue:</th>
|
58
|
+
<td><span class="badge badge-secondary"><%= @job['queue'] %></span></td>
|
59
|
+
</tr>
|
60
|
+
<tr>
|
61
|
+
<th>Created At:</th>
|
62
|
+
<td>
|
63
|
+
<% if @job['created_at'] %>
|
64
|
+
<%= Time.at(@job['created_at'].to_f).strftime('%Y-%m-%d %H:%M:%S %Z') %>
|
65
|
+
<br><small class="text-muted"><%= relative_time(Time.at(@job['created_at'].to_f)) %></small>
|
66
|
+
<% else %>
|
67
|
+
<span class="text-muted">Unknown</span>
|
68
|
+
<% end %>
|
69
|
+
</td>
|
70
|
+
</tr>
|
71
|
+
<tr>
|
72
|
+
<th>Enqueued At:</th>
|
73
|
+
<td>
|
74
|
+
<% if @job['enqueued_at'] %>
|
75
|
+
<%= Time.at(@job['enqueued_at'].to_f).strftime('%Y-%m-%d %H:%M:%S %Z') %>
|
76
|
+
<br><small class="text-muted"><%= relative_time(Time.at(@job['enqueued_at'].to_f)) %></small>
|
77
|
+
<% else %>
|
78
|
+
<span class="text-muted">Unknown</span>
|
79
|
+
<% end %>
|
80
|
+
</td>
|
81
|
+
</tr>
|
82
|
+
<% if @job['retry_count'] %>
|
83
|
+
<tr>
|
84
|
+
<th>Retry Count:</th>
|
85
|
+
<td><%= @job['retry_count'] %></td>
|
86
|
+
</tr>
|
87
|
+
<% end %>
|
88
|
+
</tbody>
|
89
|
+
</table>
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
</div>
|
93
|
+
|
94
|
+
<div class="col-md-6">
|
95
|
+
<div class="card">
|
96
|
+
<div class="card-header">
|
97
|
+
<h5 class="card-title mb-0">Orphan Status</h5>
|
98
|
+
</div>
|
99
|
+
<div class="card-body">
|
100
|
+
<table class="table table-sm">
|
101
|
+
<tbody>
|
102
|
+
<tr>
|
103
|
+
<th width="30%">Instance ID:</th>
|
104
|
+
<td><code><%= @job['instance_id'] %></code></td>
|
105
|
+
</tr>
|
106
|
+
<tr>
|
107
|
+
<th>Instance Status:</th>
|
108
|
+
<td><span class="badge badge-danger">DEAD</span></td>
|
109
|
+
</tr>
|
110
|
+
<tr>
|
111
|
+
<th>Orphaned At:</th>
|
112
|
+
<td>
|
113
|
+
<% if @job['orphaned_at'] %>
|
114
|
+
<%= Time.at(@job['orphaned_at'].to_f).strftime('%Y-%m-%d %H:%M:%S %Z') %>
|
115
|
+
<br><small class="text-muted"><%= relative_time(Time.at(@job['orphaned_at'].to_f)) %></small>
|
116
|
+
<% else %>
|
117
|
+
<span class="text-muted">Unknown</span>
|
118
|
+
<% end %>
|
119
|
+
</td>
|
120
|
+
</tr>
|
121
|
+
<tr>
|
122
|
+
<th>Orphaned Duration:</th>
|
123
|
+
<td>
|
124
|
+
<% if @job['orphaned_duration'] %>
|
125
|
+
<span class="badge badge-warning">
|
126
|
+
<%= distance_of_time_in_words(@job['orphaned_duration']) %>
|
127
|
+
</span>
|
128
|
+
<% else %>
|
129
|
+
<span class="text-muted">Unknown</span>
|
130
|
+
<% end %>
|
131
|
+
</td>
|
132
|
+
</tr>
|
133
|
+
</tbody>
|
134
|
+
</table>
|
135
|
+
</div>
|
136
|
+
</div>
|
137
|
+
</div>
|
138
|
+
</div>
|
139
|
+
|
140
|
+
<div class="row mt-3">
|
141
|
+
<div class="col-sm-12">
|
142
|
+
<div class="card">
|
143
|
+
<div class="card-header">
|
144
|
+
<h5 class="card-title mb-0">Job Arguments</h5>
|
145
|
+
</div>
|
146
|
+
<div class="card-body">
|
147
|
+
<% if @job['args'] && @job['args'].any? %>
|
148
|
+
<pre class="bg-light p-3 rounded"><%= JSON.pretty_generate(@job['args']) %></pre>
|
149
|
+
<% else %>
|
150
|
+
<p class="text-muted">No arguments</p>
|
151
|
+
<% end %>
|
152
|
+
</div>
|
153
|
+
</div>
|
154
|
+
</div>
|
155
|
+
</div>
|
156
|
+
|
157
|
+
<% if @job['error_message'] || @job['error_class'] %>
|
158
|
+
<div class="row mt-3">
|
159
|
+
<div class="col-sm-12">
|
160
|
+
<div class="card border-danger">
|
161
|
+
<div class="card-header bg-danger text-white">
|
162
|
+
<h5 class="card-title mb-0">Error Information</h5>
|
163
|
+
</div>
|
164
|
+
<div class="card-body">
|
165
|
+
<% if @job['error_class'] %>
|
166
|
+
<p><strong>Error Class:</strong> <code><%= @job['error_class'] %></code></p>
|
167
|
+
<% end %>
|
168
|
+
<% if @job['error_message'] %>
|
169
|
+
<p><strong>Error Message:</strong></p>
|
170
|
+
<pre class="bg-light p-3 rounded"><%= @job['error_message'] %></pre>
|
171
|
+
<% end %>
|
172
|
+
<% if @job['error_backtrace'] %>
|
173
|
+
<p><strong>Backtrace:</strong></p>
|
174
|
+
<pre class="bg-light p-3 rounded"><%= @job['error_backtrace'].join("\n") if @job['error_backtrace'].is_a?(Array) %></pre>
|
175
|
+
<% end %>
|
176
|
+
</div>
|
177
|
+
</div>
|
178
|
+
</div>
|
179
|
+
</div>
|
180
|
+
<% end %>
|
181
|
+
|
182
|
+
<div class="row mt-3">
|
183
|
+
<div class="col-sm-12">
|
184
|
+
<div class="card">
|
185
|
+
<div class="card-header">
|
186
|
+
<h5 class="card-title mb-0">Raw Job Data</h5>
|
187
|
+
</div>
|
188
|
+
<div class="card-body">
|
189
|
+
<pre class="bg-light p-3 rounded" style="max-height: 400px; overflow-y: auto;"><%= JSON.pretty_generate(@job) %></pre>
|
190
|
+
</div>
|
191
|
+
</div>
|
192
|
+
</div>
|
193
|
+
</div>
|
194
|
+
|
195
|
+
<div class="row mt-3">
|
196
|
+
<div class="col-sm-12">
|
197
|
+
<div class="alert alert-info">
|
198
|
+
<h6 class="alert-heading">What are orphaned jobs?</h6>
|
199
|
+
<p class="mb-0">
|
200
|
+
Orphaned jobs are jobs that were being processed when their worker instance crashed, was killed, or lost network connectivity.
|
201
|
+
The sidekiq-assured-jobs gem tracks these jobs and can automatically recover them during startup, or you can manually
|
202
|
+
retry or remove them using this interface.
|
203
|
+
</p>
|
204
|
+
</div>
|
205
|
+
</div>
|
206
|
+
</div>
|
207
|
+
|
208
|
+
<style>
|
209
|
+
.card {
|
210
|
+
border: 1px solid #dee2e6;
|
211
|
+
border-radius: 0.375rem;
|
212
|
+
margin-bottom: 1rem;
|
213
|
+
}
|
214
|
+
|
215
|
+
.card-header {
|
216
|
+
background-color: #f8f9fa;
|
217
|
+
border-bottom: 1px solid #dee2e6;
|
218
|
+
padding: 0.75rem 1.25rem;
|
219
|
+
}
|
220
|
+
|
221
|
+
.card-body {
|
222
|
+
padding: 1.25rem;
|
223
|
+
}
|
224
|
+
|
225
|
+
.table-sm th,
|
226
|
+
.table-sm td {
|
227
|
+
padding: 0.3rem;
|
228
|
+
border-top: 1px solid #dee2e6;
|
229
|
+
}
|
230
|
+
|
231
|
+
.badge {
|
232
|
+
font-size: 0.75em;
|
233
|
+
}
|
234
|
+
|
235
|
+
.badge-warning {
|
236
|
+
background-color: #ffc107;
|
237
|
+
color: #212529;
|
238
|
+
}
|
239
|
+
|
240
|
+
.badge-danger {
|
241
|
+
background-color: #dc3545;
|
242
|
+
}
|
243
|
+
|
244
|
+
.badge-secondary {
|
245
|
+
background-color: #6c757d;
|
246
|
+
}
|
247
|
+
|
248
|
+
.text-muted {
|
249
|
+
color: #6c757d !important;
|
250
|
+
}
|
251
|
+
|
252
|
+
.bg-light {
|
253
|
+
background-color: #f8f9fa !important;
|
254
|
+
}
|
255
|
+
|
256
|
+
.border-danger {
|
257
|
+
border-color: #dc3545 !important;
|
258
|
+
}
|
259
|
+
|
260
|
+
.bg-danger {
|
261
|
+
background-color: #dc3545 !important;
|
262
|
+
}
|
263
|
+
|
264
|
+
.text-white {
|
265
|
+
color: #fff !important;
|
266
|
+
}
|
267
|
+
|
268
|
+
.alert {
|
269
|
+
border-radius: 0.375rem;
|
270
|
+
}
|
271
|
+
|
272
|
+
.alert-info {
|
273
|
+
color: #0c5460;
|
274
|
+
background-color: #d1ecf1;
|
275
|
+
border-color: #bee5eb;
|
276
|
+
}
|
277
|
+
|
278
|
+
.alert-warning {
|
279
|
+
color: #856404;
|
280
|
+
background-color: #fff3cd;
|
281
|
+
border-color: #ffeaa7;
|
282
|
+
}
|
283
|
+
|
284
|
+
pre {
|
285
|
+
font-size: 0.875rem;
|
286
|
+
line-height: 1.4;
|
287
|
+
}
|
288
|
+
|
289
|
+
.btn-group .btn {
|
290
|
+
margin-left: 0.25rem;
|
291
|
+
}
|
292
|
+
|
293
|
+
.btn-group .btn:first-child {
|
294
|
+
margin-left: 0;
|
295
|
+
}
|
296
|
+
</style>
|
@@ -0,0 +1,301 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<div class="col-sm-5">
|
3
|
+
<h3>
|
4
|
+
Orphaned Jobs
|
5
|
+
<small class="text-muted"><%= @total_count %></small>
|
6
|
+
</h3>
|
7
|
+
</div>
|
8
|
+
<div class="col-sm-7 text-right">
|
9
|
+
<div class="btn-group" role="group">
|
10
|
+
<button type="button" class="btn btn-primary btn-sm" onclick="refreshData()">
|
11
|
+
↻ Refresh
|
12
|
+
</button>
|
13
|
+
<button type="button" class="btn btn-warning btn-sm" onclick="bulkRetry()" id="bulk-retry-btn" disabled>
|
14
|
+
↻ Retry Selected
|
15
|
+
</button>
|
16
|
+
<button type="button" class="btn btn-danger btn-sm" onclick="bulkDelete()" id="bulk-delete-btn" disabled>
|
17
|
+
✕ Delete Selected
|
18
|
+
</button>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</header>
|
22
|
+
|
23
|
+
<div class="row">
|
24
|
+
<div class="col-sm-12">
|
25
|
+
<div class="alert alert-info">
|
26
|
+
<strong>Orphaned Jobs Monitor:</strong> These jobs were being processed when their worker instances crashed or were terminated.
|
27
|
+
They are automatically recovered during startup, but you can manually retry or remove them here.
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<% if @instances.any? %>
|
33
|
+
<div class="row mb-3">
|
34
|
+
<div class="col-sm-12">
|
35
|
+
<div class="card">
|
36
|
+
<div class="card-header">
|
37
|
+
<h5 class="card-title mb-0">Instance Status</h5>
|
38
|
+
</div>
|
39
|
+
<div class="card-body">
|
40
|
+
<div class="row">
|
41
|
+
<% @instances.each do |instance_id, info| %>
|
42
|
+
<div class="col-md-4 mb-2">
|
43
|
+
<div class="d-flex align-items-center">
|
44
|
+
<span class="badge badge-<%= info[:status] == 'alive' ? 'success' : 'danger' %> mr-2">
|
45
|
+
<%= info[:status].upcase %>
|
46
|
+
</span>
|
47
|
+
<div>
|
48
|
+
<strong><%= instance_id %></strong><br>
|
49
|
+
<small class="text-muted">
|
50
|
+
<% if info[:last_heartbeat] %>
|
51
|
+
Last seen: <%= relative_time(Time.at(info[:last_heartbeat].to_f)) %>
|
52
|
+
<% else %>
|
53
|
+
No heartbeat data
|
54
|
+
<% end %>
|
55
|
+
<% if info[:orphaned_job_count] && info[:orphaned_job_count] > 0 %>
|
56
|
+
<br>Orphaned jobs: <%= info[:orphaned_job_count] %>
|
57
|
+
<% end %>
|
58
|
+
</small>
|
59
|
+
</div>
|
60
|
+
</div>
|
61
|
+
</div>
|
62
|
+
<% end %>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
<% end %>
|
69
|
+
|
70
|
+
<div class="row">
|
71
|
+
<div class="col-sm-12">
|
72
|
+
<% if @orphaned_jobs.empty? %>
|
73
|
+
<div class="alert alert-success">
|
74
|
+
<h4 class="alert-heading">No Orphaned Jobs!</h4>
|
75
|
+
<p>All tracked jobs are running on live worker instances. This is the ideal state.</p>
|
76
|
+
</div>
|
77
|
+
<% else %>
|
78
|
+
<form id="bulk-action-form" method="post" action="<%= root_path %>orphaned-jobs/bulk-action">
|
79
|
+
<%= csrf_tag %>
|
80
|
+
<input type="hidden" name="action" id="bulk-action-type">
|
81
|
+
|
82
|
+
<div class="table-responsive">
|
83
|
+
<table class="table table-striped table-bordered table-white">
|
84
|
+
<thead>
|
85
|
+
<tr>
|
86
|
+
<th width="3%">
|
87
|
+
<input type="checkbox" id="select-all" onchange="toggleSelectAll()">
|
88
|
+
</th>
|
89
|
+
<th width="12%">Job ID</th>
|
90
|
+
<th width="15%">Class</th>
|
91
|
+
<th width="20%">Arguments</th>
|
92
|
+
<th width="10%">Queue</th>
|
93
|
+
<th width="12%">Instance</th>
|
94
|
+
<th width="10%">Orphaned</th>
|
95
|
+
<th width="8%">Duration</th>
|
96
|
+
<th width="10%">Actions</th>
|
97
|
+
</tr>
|
98
|
+
</thead>
|
99
|
+
<tbody>
|
100
|
+
<% @orphaned_jobs.each do |job| %>
|
101
|
+
<tr>
|
102
|
+
<td>
|
103
|
+
<input type="checkbox" name="jids[]" value="<%= job['jid'] %>" class="job-checkbox" onchange="updateBulkButtons()">
|
104
|
+
</td>
|
105
|
+
<td>
|
106
|
+
<a href="<%= root_path %>orphaned-jobs/<%= job['jid'] %>" class="text-info">
|
107
|
+
<%= truncate(job['jid'], 12) %>
|
108
|
+
</a>
|
109
|
+
</td>
|
110
|
+
<td>
|
111
|
+
<code><%= job['class'] %></code>
|
112
|
+
</td>
|
113
|
+
<td>
|
114
|
+
<% if job['args'] && job['args'].any? %>
|
115
|
+
<span class="text-muted" title="<%= job['args'].inspect %>">
|
116
|
+
<%= truncate(job['args'].inspect, 40) %>
|
117
|
+
</span>
|
118
|
+
<% else %>
|
119
|
+
<span class="text-muted">No args</span>
|
120
|
+
<% end %>
|
121
|
+
</td>
|
122
|
+
<td>
|
123
|
+
<span class="badge badge-secondary"><%= job['queue'] %></span>
|
124
|
+
</td>
|
125
|
+
<td>
|
126
|
+
<span class="text-muted"><%= truncate(job['instance_id'], 12) %></span>
|
127
|
+
</td>
|
128
|
+
<td>
|
129
|
+
<% if job['orphaned_at'] %>
|
130
|
+
<span class="text-muted" title="<%= Time.at(job['orphaned_at'].to_f).strftime('%Y-%m-%d %H:%M:%S %Z') %>">
|
131
|
+
<%= relative_time(Time.at(job['orphaned_at'].to_f)) %>
|
132
|
+
</span>
|
133
|
+
<% else %>
|
134
|
+
<span class="text-muted">Unknown</span>
|
135
|
+
<% end %>
|
136
|
+
</td>
|
137
|
+
<td>
|
138
|
+
<% if job['orphaned_duration'] %>
|
139
|
+
<span class="badge badge-warning">
|
140
|
+
<%= distance_of_time_in_words(job['orphaned_duration']) %>
|
141
|
+
</span>
|
142
|
+
<% else %>
|
143
|
+
<span class="text-muted">-</span>
|
144
|
+
<% end %>
|
145
|
+
</td>
|
146
|
+
<td>
|
147
|
+
<div class="btn-group btn-group-sm" role="group">
|
148
|
+
<form method="post" action="<%= root_path %>orphaned-jobs/<%= job['jid'] %>/retry" style="display: inline;">
|
149
|
+
<%= csrf_tag %>
|
150
|
+
<button type="submit" class="btn btn-warning btn-sm" title="Retry Job">
|
151
|
+
Retry
|
152
|
+
</button>
|
153
|
+
</form>
|
154
|
+
<form method="post" action="<%= root_path %>orphaned-jobs/<%= job['jid'] %>/delete" style="display: inline;"
|
155
|
+
onsubmit="return confirm('Are you sure you want to delete this orphaned job?')">
|
156
|
+
<%= csrf_tag %>
|
157
|
+
<button type="submit" class="btn btn-danger btn-sm" title="Delete Job">
|
158
|
+
Delete
|
159
|
+
</button>
|
160
|
+
</form>
|
161
|
+
</div>
|
162
|
+
</td>
|
163
|
+
</tr>
|
164
|
+
<% end %>
|
165
|
+
</tbody>
|
166
|
+
</table>
|
167
|
+
</div>
|
168
|
+
</form>
|
169
|
+
<% end %>
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
|
173
|
+
<script>
|
174
|
+
function toggleSelectAll() {
|
175
|
+
const selectAll = document.getElementById('select-all');
|
176
|
+
const checkboxes = document.querySelectorAll('.job-checkbox');
|
177
|
+
|
178
|
+
checkboxes.forEach(checkbox => {
|
179
|
+
checkbox.checked = selectAll.checked;
|
180
|
+
});
|
181
|
+
|
182
|
+
updateBulkButtons();
|
183
|
+
}
|
184
|
+
|
185
|
+
function updateBulkButtons() {
|
186
|
+
const checkedBoxes = document.querySelectorAll('.job-checkbox:checked');
|
187
|
+
const retryBtn = document.getElementById('bulk-retry-btn');
|
188
|
+
const deleteBtn = document.getElementById('bulk-delete-btn');
|
189
|
+
|
190
|
+
const hasSelection = checkedBoxes.length > 0;
|
191
|
+
retryBtn.disabled = !hasSelection;
|
192
|
+
deleteBtn.disabled = !hasSelection;
|
193
|
+
|
194
|
+
// Update select-all checkbox state
|
195
|
+
const allCheckboxes = document.querySelectorAll('.job-checkbox');
|
196
|
+
const selectAllCheckbox = document.getElementById('select-all');
|
197
|
+
|
198
|
+
if (checkedBoxes.length === 0) {
|
199
|
+
selectAllCheckbox.indeterminate = false;
|
200
|
+
selectAllCheckbox.checked = false;
|
201
|
+
} else if (checkedBoxes.length === allCheckboxes.length) {
|
202
|
+
selectAllCheckbox.indeterminate = false;
|
203
|
+
selectAllCheckbox.checked = true;
|
204
|
+
} else {
|
205
|
+
selectAllCheckbox.indeterminate = true;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
function bulkRetry() {
|
210
|
+
const checkedBoxes = document.querySelectorAll('.job-checkbox:checked');
|
211
|
+
if (checkedBoxes.length === 0) return;
|
212
|
+
|
213
|
+
if (confirm(`Are you sure you want to retry ${checkedBoxes.length} orphaned job(s)?`)) {
|
214
|
+
document.getElementById('bulk-action-type').value = 'retry';
|
215
|
+
document.getElementById('bulk-action-form').submit();
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
function bulkDelete() {
|
220
|
+
const checkedBoxes = document.querySelectorAll('.job-checkbox:checked');
|
221
|
+
if (checkedBoxes.length === 0) return;
|
222
|
+
|
223
|
+
if (confirm(`Are you sure you want to delete ${checkedBoxes.length} orphaned job(s)? This action cannot be undone.`)) {
|
224
|
+
document.getElementById('bulk-action-type').value = 'delete';
|
225
|
+
document.getElementById('bulk-action-form').submit();
|
226
|
+
}
|
227
|
+
}
|
228
|
+
|
229
|
+
function refreshData() {
|
230
|
+
window.location.reload();
|
231
|
+
}
|
232
|
+
|
233
|
+
// Auto-refresh every 30 seconds
|
234
|
+
setInterval(function() {
|
235
|
+
// Only auto-refresh if no jobs are selected to avoid disrupting user actions
|
236
|
+
const checkedBoxes = document.querySelectorAll('.job-checkbox:checked');
|
237
|
+
if (checkedBoxes.length === 0) {
|
238
|
+
refreshData();
|
239
|
+
}
|
240
|
+
}, 30000);
|
241
|
+
|
242
|
+
// Initialize bulk button states
|
243
|
+
document.addEventListener('DOMContentLoaded', function() {
|
244
|
+
updateBulkButtons();
|
245
|
+
});
|
246
|
+
</script>
|
247
|
+
|
248
|
+
<style>
|
249
|
+
.table-white {
|
250
|
+
background-color: white;
|
251
|
+
}
|
252
|
+
|
253
|
+
.btn-group-sm .btn {
|
254
|
+
padding: 0.25rem 0.5rem;
|
255
|
+
font-size: 0.75rem;
|
256
|
+
}
|
257
|
+
|
258
|
+
.badge {
|
259
|
+
font-size: 0.75em;
|
260
|
+
}
|
261
|
+
|
262
|
+
.card {
|
263
|
+
border: 1px solid #dee2e6;
|
264
|
+
border-radius: 0.375rem;
|
265
|
+
}
|
266
|
+
|
267
|
+
.card-header {
|
268
|
+
background-color: #f8f9fa;
|
269
|
+
border-bottom: 1px solid #dee2e6;
|
270
|
+
padding: 0.75rem 1.25rem;
|
271
|
+
}
|
272
|
+
|
273
|
+
.alert {
|
274
|
+
border-radius: 0.375rem;
|
275
|
+
}
|
276
|
+
|
277
|
+
.text-info {
|
278
|
+
color: #17a2b8 !important;
|
279
|
+
}
|
280
|
+
|
281
|
+
.text-muted {
|
282
|
+
color: #6c757d !important;
|
283
|
+
}
|
284
|
+
|
285
|
+
.badge-warning {
|
286
|
+
background-color: #ffc107;
|
287
|
+
color: #212529;
|
288
|
+
}
|
289
|
+
|
290
|
+
.badge-danger {
|
291
|
+
background-color: #dc3545;
|
292
|
+
}
|
293
|
+
|
294
|
+
.badge-success {
|
295
|
+
background-color: #28a745;
|
296
|
+
}
|
297
|
+
|
298
|
+
.badge-secondary {
|
299
|
+
background-color: #6c757d;
|
300
|
+
}
|
301
|
+
</style>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-assured-jobs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manikanta Gopi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-07-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|
@@ -86,6 +86,20 @@ dependencies:
|
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '1.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rack-test
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '2.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '2.0'
|
89
103
|
description: Ensures Sidekiq jobs are never lost due to worker crashes or restarts
|
90
104
|
by tracking in-flight jobs and automatically recovering orphaned work
|
91
105
|
email:
|
@@ -97,10 +111,15 @@ files:
|
|
97
111
|
- CHANGELOG.md
|
98
112
|
- LICENSE.txt
|
99
113
|
- README.md
|
114
|
+
- examples/web_demo.rb
|
100
115
|
- lib/sidekiq-assured-jobs.rb
|
101
116
|
- lib/sidekiq/assured_jobs/middleware.rb
|
102
117
|
- lib/sidekiq/assured_jobs/version.rb
|
118
|
+
- lib/sidekiq/assured_jobs/web.rb
|
103
119
|
- lib/sidekiq/assured_jobs/worker.rb
|
120
|
+
- web/assets/orphaned_jobs.css
|
121
|
+
- web/views/orphaned_job.erb
|
122
|
+
- web/views/orphaned_jobs.erb
|
104
123
|
homepage: https://github.com/praja/sidekiq-assured-jobs
|
105
124
|
licenses:
|
106
125
|
- MIT
|