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.
@@ -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.0.0
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-06-30 00:00:00.000000000 Z
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