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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5ea24f7575f4e5183d9d21a0d3a59f623496e19f13e79a611db1eb286ef87b5
|
4
|
+
data.tar.gz: 5f4a107e730f7f9ae5ae4a7efb51e4789441689395cd08d8f22237b8f73acd83
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a663a11343df9331de4d3517e3a4d6d154f9b29461d016c322b4d4d96689410d48555005d49d462d90e12bd5fd5de518e23f79dda28493e2dae3ef100ac5870
|
7
|
+
data.tar.gz: 1ed718cfe35480c48feccd574ccd5d721bf70e55d9d9d060a4c8dfb99253ebd6e404f19e1358ea52b03ea00f3e61ded4dd90c1962fb85f1d5c99f86ee1ca5314
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.1.0] - 2025-01-03
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- **🖥️ Web Dashboard**: Complete web interface for monitoring and managing orphaned jobs
|
12
|
+
- **📊 Real-time Monitoring**: Live dashboard showing all orphaned jobs with detailed information
|
13
|
+
- **🔄 Interactive Actions**: Manual retry and delete operations for individual jobs
|
14
|
+
- **🎯 Bulk Operations**: Select and manage multiple orphaned jobs simultaneously
|
15
|
+
- **📈 Instance Monitoring**: Visual status tracking of worker instances (alive/dead)
|
16
|
+
- **🔍 Job Details View**: Comprehensive job information including arguments, errors, and metadata
|
17
|
+
- **⏱️ Auto-refresh**: Dashboard automatically updates every 30 seconds
|
18
|
+
- **📱 Responsive Design**: Mobile-friendly interface matching Sidekiq's UI patterns
|
19
|
+
|
20
|
+
### Features
|
21
|
+
- **Orphaned Jobs Tab**: New tab in Sidekiq web interface at `/orphaned-jobs`
|
22
|
+
- **Job Arguments Display**: Arguments column in main table for better job identification
|
23
|
+
- **Bulk Selection**: Checkbox interface for selecting multiple jobs
|
24
|
+
- **Confirmation Dialogs**: Prevent accidental job deletions
|
25
|
+
- **Instance Health Cards**: Visual display of live vs dead worker instances
|
26
|
+
- **Job Duration Tracking**: Shows how long jobs have been orphaned
|
27
|
+
- **Error Information**: Display job errors and failure details
|
28
|
+
- **Demo Script**: Interactive demo at `examples/web_demo.rb`
|
29
|
+
|
30
|
+
### Technical
|
31
|
+
- **Web Extension**: Seamless integration with `Sidekiq::Web`
|
32
|
+
- **Helper Methods**: Time formatting, text truncation, CSRF protection
|
33
|
+
- **Data Access Layer**: Efficient Redis queries for orphaned job data
|
34
|
+
- **Test Coverage**: Comprehensive test suite for web functionality
|
35
|
+
- **Unicode Icons**: Replaced FontAwesome with Unicode symbols for better compatibility
|
36
|
+
|
37
|
+
### Documentation
|
38
|
+
- **Web Interface Guide**: Complete documentation of dashboard features
|
39
|
+
- **Setup Instructions**: Clear integration steps for Rails and standalone apps
|
40
|
+
- **Demo Instructions**: How to run the interactive demo
|
41
|
+
- **Feature Overview**: Detailed explanation of all web interface capabilities
|
42
|
+
|
8
43
|
## [1.0.0] - 2025-06-20
|
9
44
|
|
10
45
|
### Added
|
data/README.md
CHANGED
@@ -18,6 +18,7 @@ Sidekiq Assured Jobs ensures that your critical Sidekiq jobs are never lost due
|
|
18
18
|
- **🛡️ Job Assurance**: Guarantees that tracked jobs will complete or be automatically retried
|
19
19
|
- **🔄 Automatic Recovery**: Detects and re-enqueues orphaned jobs from crashed workers
|
20
20
|
- **⏰ Delayed Recovery**: Configurable additional recovery passes for enhanced reliability
|
21
|
+
- **🖥️ Web Dashboard**: Monitor and manage orphaned jobs through Sidekiq's web interface
|
21
22
|
- **⚡ Zero Configuration**: Works out of the box with sensible defaults
|
22
23
|
- **🏗️ Production Ready**: Designed for high-throughput production environments
|
23
24
|
- **🔗 Sidekiq Integration**: Uses Sidekiq's existing Redis connection pool
|
@@ -146,6 +147,72 @@ end
|
|
146
147
|
|
147
148
|
Your critical jobs are now protected. If a worker crashes while processing a tracked job, another worker will automatically detect and re-enqueue it.
|
148
149
|
|
150
|
+
## Web Interface
|
151
|
+
|
152
|
+
Sidekiq Assured Jobs includes a web dashboard that integrates seamlessly with Sidekiq's existing web interface. The dashboard allows you to monitor and manage orphaned jobs in real-time.
|
153
|
+
|
154
|
+
### Setup
|
155
|
+
|
156
|
+
The web interface is automatically available when you mount Sidekiq::Web in your application:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
# config/routes.rb (Rails)
|
160
|
+
require 'sidekiq/web'
|
161
|
+
mount Sidekiq::Web => '/sidekiq'
|
162
|
+
```
|
163
|
+
|
164
|
+
Or for standalone applications:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
# config.ru
|
168
|
+
require 'sidekiq/web'
|
169
|
+
run Sidekiq::Web
|
170
|
+
```
|
171
|
+
|
172
|
+
### Features
|
173
|
+
|
174
|
+
The **Orphaned Jobs** tab provides:
|
175
|
+
|
176
|
+
- **📊 Real-time Dashboard**: View all orphaned jobs with key information
|
177
|
+
- **🔍 Job Details**: Detailed view of individual orphaned jobs including arguments and error information
|
178
|
+
- **🔄 Manual Recovery**: Retry orphaned jobs individually or in bulk
|
179
|
+
- **🗑️ Job Management**: Delete orphaned jobs that are no longer needed
|
180
|
+
- **📈 Instance Monitoring**: Track the status of worker instances (alive/dead)
|
181
|
+
- **⏱️ Auto-refresh**: Dashboard automatically updates every 30 seconds
|
182
|
+
- **🎯 Bulk Operations**: Select multiple jobs for batch retry or delete operations
|
183
|
+
|
184
|
+
### Dashboard Information
|
185
|
+
|
186
|
+
For each orphaned job, the dashboard displays:
|
187
|
+
|
188
|
+
- **Job ID**: Unique identifier for the job
|
189
|
+
- **Class**: The worker class name
|
190
|
+
- **Queue**: The queue the job was running in
|
191
|
+
- **Instance**: The worker instance that was processing the job
|
192
|
+
- **Orphaned Time**: When the job became orphaned
|
193
|
+
- **Duration**: How long the job has been orphaned
|
194
|
+
- **Arguments**: The job's input parameters
|
195
|
+
- **Error Information**: Any error details if the job failed
|
196
|
+
|
197
|
+
### Actions Available
|
198
|
+
|
199
|
+
- **Retry**: Re-enqueue the job for processing
|
200
|
+
- **Delete**: Remove the job from tracking (cannot be undone)
|
201
|
+
- **Bulk Retry**: Retry multiple selected jobs at once
|
202
|
+
- **Bulk Delete**: Delete multiple selected jobs at once
|
203
|
+
|
204
|
+
The web interface provides a user-friendly way to monitor your job reliability and take action when needed, complementing the automatic recovery system.
|
205
|
+
|
206
|
+
### Demo
|
207
|
+
|
208
|
+
To see the web interface in action, run the included demo:
|
209
|
+
|
210
|
+
```bash
|
211
|
+
ruby examples/web_demo.rb
|
212
|
+
```
|
213
|
+
|
214
|
+
Then visit `http://localhost:4567/orphaned-jobs` to explore the dashboard with sample orphaned jobs.
|
215
|
+
|
149
216
|
## Configuration
|
150
217
|
|
151
218
|
The gem works with zero configuration but provides extensive customization options. See the [Complete Configuration Reference](#complete-configuration-reference) below for all available options.
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Demo script showing the Sidekiq Assured Jobs web interface
|
5
|
+
# Run this script and visit http://localhost:4567/orphaned-jobs
|
6
|
+
|
7
|
+
require 'bundler/setup'
|
8
|
+
require 'sidekiq'
|
9
|
+
require 'sidekiq/web'
|
10
|
+
require_relative '../lib/sidekiq-assured-jobs'
|
11
|
+
|
12
|
+
# Configure Redis for demo
|
13
|
+
Sidekiq.configure_server do |config|
|
14
|
+
config.redis = { url: 'redis://localhost:6379/15' } # Use test database
|
15
|
+
end
|
16
|
+
|
17
|
+
Sidekiq.configure_client do |config|
|
18
|
+
config.redis = { url: 'redis://localhost:6379/15' } # Use test database
|
19
|
+
end
|
20
|
+
|
21
|
+
# Configure AssuredJobs for demo
|
22
|
+
Sidekiq::AssuredJobs.configure do |config|
|
23
|
+
config.namespace = "demo_assured_jobs"
|
24
|
+
config.heartbeat_interval = 5
|
25
|
+
config.heartbeat_ttl = 15
|
26
|
+
config.auto_recovery_enabled = false # Disable auto-recovery for demo
|
27
|
+
end
|
28
|
+
|
29
|
+
# Demo worker class
|
30
|
+
class DemoWorker
|
31
|
+
include Sidekiq::Worker
|
32
|
+
include Sidekiq::AssuredJobs::Worker
|
33
|
+
|
34
|
+
def perform(message, delay = 0)
|
35
|
+
puts "Processing: #{message}"
|
36
|
+
sleep(delay) if delay > 0
|
37
|
+
puts "Completed: #{message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create some demo orphaned jobs
|
42
|
+
def create_demo_orphaned_jobs
|
43
|
+
puts "Creating demo orphaned jobs..."
|
44
|
+
|
45
|
+
# Simulate orphaned jobs by creating tracking data without live instances
|
46
|
+
dead_instances = ["demo-worker-1", "demo-worker-2", "demo-worker-3"]
|
47
|
+
|
48
|
+
Sidekiq::AssuredJobs.redis_sync do |conn|
|
49
|
+
dead_instances.each_with_index do |instance_id, i|
|
50
|
+
# Create some orphaned jobs for each dead instance
|
51
|
+
(1..3).each do |job_num|
|
52
|
+
jid = "demo_job_#{instance_id}_#{job_num}"
|
53
|
+
job_data = {
|
54
|
+
"class" => "DemoWorker",
|
55
|
+
"args" => ["Demo job #{job_num} from #{instance_id}", 2],
|
56
|
+
"jid" => jid,
|
57
|
+
"queue" => "default",
|
58
|
+
"created_at" => (Time.now - (i * 300) - (job_num * 60)).to_f,
|
59
|
+
"enqueued_at" => (Time.now - (i * 300) - (job_num * 60)).to_f,
|
60
|
+
"retry_count" => job_num - 1
|
61
|
+
}
|
62
|
+
|
63
|
+
# Add to tracking
|
64
|
+
job_tracking_key = Sidekiq::AssuredJobs.send(:namespaced_key, "jobs:#{instance_id}")
|
65
|
+
job_data_key = Sidekiq::AssuredJobs.send(:namespaced_key, "job:#{jid}")
|
66
|
+
|
67
|
+
conn.sadd(job_tracking_key, jid)
|
68
|
+
conn.set(job_data_key, job_data.to_json)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
puts "Created #{dead_instances.size * 3} demo orphaned jobs"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Create some live instances for comparison
|
77
|
+
def create_demo_live_instances
|
78
|
+
puts "Creating demo live instances..."
|
79
|
+
|
80
|
+
live_instances = ["live-worker-1", "live-worker-2"]
|
81
|
+
|
82
|
+
Sidekiq::AssuredJobs.redis_sync do |conn|
|
83
|
+
live_instances.each do |instance_id|
|
84
|
+
key = Sidekiq::AssuredJobs.send(:namespaced_key, "instance:#{instance_id}")
|
85
|
+
conn.setex(key, 60, Time.now.to_f)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
puts "Created #{live_instances.size} demo live instances"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Clean up demo data
|
93
|
+
def cleanup_demo_data
|
94
|
+
puts "Cleaning up demo data..."
|
95
|
+
|
96
|
+
Sidekiq::AssuredJobs.redis_sync do |conn|
|
97
|
+
keys = conn.keys("#{Sidekiq::AssuredJobs.namespace}:*")
|
98
|
+
conn.del(*keys) if keys.any?
|
99
|
+
end
|
100
|
+
|
101
|
+
puts "Demo data cleaned up"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Setup demo data
|
105
|
+
puts "Setting up demo environment..."
|
106
|
+
cleanup_demo_data
|
107
|
+
create_demo_orphaned_jobs
|
108
|
+
create_demo_live_instances
|
109
|
+
|
110
|
+
puts "\n" + "="*60
|
111
|
+
puts "SIDEKIQ ASSURED JOBS WEB DEMO"
|
112
|
+
puts "="*60
|
113
|
+
puts ""
|
114
|
+
puts "Demo server starting at: http://localhost:4567"
|
115
|
+
puts ""
|
116
|
+
puts "Available endpoints:"
|
117
|
+
puts " • Main dashboard: http://localhost:4567/"
|
118
|
+
puts " • Orphaned Jobs: http://localhost:4567/orphaned-jobs"
|
119
|
+
puts " • Job stats (JSON): http://localhost:4567/orphaned-jobs/stats"
|
120
|
+
puts ""
|
121
|
+
puts "Demo features:"
|
122
|
+
puts " • View orphaned jobs from 3 'dead' worker instances"
|
123
|
+
puts " • See live vs dead instance status"
|
124
|
+
puts " • Try retrying or deleting orphaned jobs"
|
125
|
+
puts " • Test bulk operations"
|
126
|
+
puts " • View detailed job information"
|
127
|
+
puts ""
|
128
|
+
puts "Press Ctrl+C to stop the demo and clean up"
|
129
|
+
puts "="*60
|
130
|
+
puts ""
|
131
|
+
|
132
|
+
# Trap interrupt to cleanup
|
133
|
+
trap('INT') do
|
134
|
+
puts "\n\nShutting down demo..."
|
135
|
+
cleanup_demo_data
|
136
|
+
puts "Demo data cleaned up. Goodbye!"
|
137
|
+
exit
|
138
|
+
end
|
139
|
+
|
140
|
+
# Start the web server
|
141
|
+
require 'rack'
|
142
|
+
|
143
|
+
app = Rack::Builder.new do
|
144
|
+
use Rack::ShowExceptions
|
145
|
+
run Sidekiq::Web
|
146
|
+
end
|
147
|
+
|
148
|
+
Rack::Handler::WEBrick.run(app, Port: 4567, Host: '0.0.0.0') do |server|
|
149
|
+
puts "Demo server started successfully!"
|
150
|
+
puts "Visit http://localhost:4567/orphaned-jobs to see the dashboard"
|
151
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq/web'
|
4
|
+
|
5
|
+
module Sidekiq
|
6
|
+
module AssuredJobs
|
7
|
+
module Web
|
8
|
+
def self.registered(app)
|
9
|
+
# Add helper methods to the app
|
10
|
+
app.helpers do
|
11
|
+
def relative_time(time)
|
12
|
+
return 'Unknown' unless time
|
13
|
+
|
14
|
+
diff = Time.now - time
|
15
|
+
case diff
|
16
|
+
when 0..59
|
17
|
+
"#{diff.to_i} seconds ago"
|
18
|
+
when 60..3599
|
19
|
+
"#{(diff / 60).to_i} minutes ago"
|
20
|
+
when 3600..86399
|
21
|
+
"#{(diff / 3600).to_i} hours ago"
|
22
|
+
else
|
23
|
+
"#{(diff / 86400).to_i} days ago"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def distance_of_time_in_words(seconds)
|
28
|
+
return 'Unknown' unless seconds
|
29
|
+
|
30
|
+
case seconds
|
31
|
+
when 0..59
|
32
|
+
"#{seconds.to_i}s"
|
33
|
+
when 60..3599
|
34
|
+
"#{(seconds / 60).to_i}m"
|
35
|
+
when 3600..86399
|
36
|
+
"#{(seconds / 3600).to_i}h"
|
37
|
+
else
|
38
|
+
"#{(seconds / 86400).to_i}d"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def truncate(text, length)
|
43
|
+
return text unless text
|
44
|
+
text.length > length ? "#{text[0, length]}..." : text
|
45
|
+
end
|
46
|
+
|
47
|
+
def csrf_tag
|
48
|
+
# Sidekiq web uses Rack::Protection, get the token
|
49
|
+
"<input type='hidden' name='authenticity_token' value='#{env['rack.session'][:csrf]}' />"
|
50
|
+
end
|
51
|
+
|
52
|
+
def root_path
|
53
|
+
"#{env['SCRIPT_NAME']}/"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
app.get '/orphaned-jobs' do
|
58
|
+
@orphaned_jobs = OrphanedJobsManager.get_orphaned_jobs
|
59
|
+
@instances = OrphanedJobsManager.get_instances_info
|
60
|
+
@total_count = @orphaned_jobs.size
|
61
|
+
erb File.read(File.join(File.dirname(__FILE__), '../../../web/views/orphaned_jobs.erb'))
|
62
|
+
end
|
63
|
+
|
64
|
+
app.get '/orphaned-jobs/:jid' do
|
65
|
+
jid = params[:jid]
|
66
|
+
@job = OrphanedJobsManager.get_orphaned_job(jid)
|
67
|
+
halt 404 unless @job
|
68
|
+
erb File.read(File.join(File.dirname(__FILE__), '../../../web/views/orphaned_job.erb'))
|
69
|
+
end
|
70
|
+
|
71
|
+
app.post '/orphaned-jobs/:jid/retry' do
|
72
|
+
jid = params[:jid]
|
73
|
+
result = OrphanedJobsManager.retry_orphaned_job(jid)
|
74
|
+
if result[:success]
|
75
|
+
redirect to('/orphaned-jobs')
|
76
|
+
else
|
77
|
+
halt 400, result[:error]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
app.post '/orphaned-jobs/:jid/delete' do
|
82
|
+
jid = params[:jid]
|
83
|
+
result = OrphanedJobsManager.delete_orphaned_job(jid)
|
84
|
+
if result[:success]
|
85
|
+
redirect to('/orphaned-jobs')
|
86
|
+
else
|
87
|
+
halt 400, result[:error]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
app.post '/orphaned-jobs/bulk-action' do
|
92
|
+
action = params[:action]
|
93
|
+
jids = params[:jids] || []
|
94
|
+
|
95
|
+
case action
|
96
|
+
when 'retry'
|
97
|
+
result = OrphanedJobsManager.bulk_retry_orphaned_jobs(jids)
|
98
|
+
when 'delete'
|
99
|
+
result = OrphanedJobsManager.bulk_delete_orphaned_jobs(jids)
|
100
|
+
else
|
101
|
+
halt 400, "Invalid action: #{action}"
|
102
|
+
end
|
103
|
+
|
104
|
+
if result[:success]
|
105
|
+
redirect to('/orphaned-jobs')
|
106
|
+
else
|
107
|
+
halt 400, result[:error]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
app.get '/orphaned-jobs/stats' do
|
112
|
+
content_type :json
|
113
|
+
OrphanedJobsManager.get_stats.to_json
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Manager class for handling orphaned jobs operations
|
118
|
+
class OrphanedJobsManager
|
119
|
+
class << self
|
120
|
+
def get_orphaned_jobs
|
121
|
+
AssuredJobs.get_orphaned_jobs_info
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_orphaned_job(jid)
|
125
|
+
AssuredJobs.get_orphaned_job_by_jid(jid)
|
126
|
+
end
|
127
|
+
|
128
|
+
def retry_orphaned_job(jid)
|
129
|
+
begin
|
130
|
+
job_data = get_orphaned_job(jid)
|
131
|
+
return { success: false, error: "Job not found" } unless job_data
|
132
|
+
|
133
|
+
# Clear unique-jobs lock if present
|
134
|
+
AssuredJobs.clear_unique_jobs_lock(job_data)
|
135
|
+
|
136
|
+
# Re-enqueue the job
|
137
|
+
Sidekiq::Client.push(job_data)
|
138
|
+
|
139
|
+
# Clean up tracking data
|
140
|
+
cleanup_job_tracking(jid, job_data['instance_id'])
|
141
|
+
|
142
|
+
{ success: true }
|
143
|
+
rescue => e
|
144
|
+
{ success: false, error: e.message }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def delete_orphaned_job(jid)
|
149
|
+
begin
|
150
|
+
job_data = get_orphaned_job(jid)
|
151
|
+
return { success: false, error: "Job not found" } unless job_data
|
152
|
+
|
153
|
+
# Clean up tracking data
|
154
|
+
cleanup_job_tracking(jid, job_data['instance_id'])
|
155
|
+
|
156
|
+
{ success: true }
|
157
|
+
rescue => e
|
158
|
+
{ success: false, error: e.message }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def bulk_retry_orphaned_jobs(jids)
|
163
|
+
begin
|
164
|
+
success_count = 0
|
165
|
+
errors = []
|
166
|
+
|
167
|
+
jids.each do |jid|
|
168
|
+
result = retry_orphaned_job(jid)
|
169
|
+
if result[:success]
|
170
|
+
success_count += 1
|
171
|
+
else
|
172
|
+
errors << "#{jid}: #{result[:error]}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
if errors.empty?
|
177
|
+
{ success: true, message: "Successfully retried #{success_count} jobs" }
|
178
|
+
else
|
179
|
+
{ success: false, error: "Retried #{success_count} jobs, failed: #{errors.join(', ')}" }
|
180
|
+
end
|
181
|
+
rescue => e
|
182
|
+
{ success: false, error: e.message }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def bulk_delete_orphaned_jobs(jids)
|
187
|
+
begin
|
188
|
+
success_count = 0
|
189
|
+
errors = []
|
190
|
+
|
191
|
+
jids.each do |jid|
|
192
|
+
result = delete_orphaned_job(jid)
|
193
|
+
if result[:success]
|
194
|
+
success_count += 1
|
195
|
+
else
|
196
|
+
errors << "#{jid}: #{result[:error]}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if errors.empty?
|
201
|
+
{ success: true, message: "Successfully deleted #{success_count} jobs" }
|
202
|
+
else
|
203
|
+
{ success: false, error: "Deleted #{success_count} jobs, failed: #{errors.join(', ')}" }
|
204
|
+
end
|
205
|
+
rescue => e
|
206
|
+
{ success: false, error: e.message }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def get_instances_info
|
211
|
+
AssuredJobs.get_instances_status
|
212
|
+
end
|
213
|
+
|
214
|
+
def get_stats
|
215
|
+
stats = {
|
216
|
+
total_orphaned_jobs: 0,
|
217
|
+
dead_instances: 0,
|
218
|
+
live_instances: 0,
|
219
|
+
oldest_orphaned_job: nil
|
220
|
+
}
|
221
|
+
|
222
|
+
orphaned_jobs = get_orphaned_jobs
|
223
|
+
instances = get_instances_info
|
224
|
+
|
225
|
+
stats[:total_orphaned_jobs] = orphaned_jobs.size
|
226
|
+
stats[:dead_instances] = instances.count { |_, info| info[:status] == 'dead' }
|
227
|
+
stats[:live_instances] = instances.count { |_, info| info[:status] == 'alive' }
|
228
|
+
|
229
|
+
if orphaned_jobs.any?
|
230
|
+
oldest_job = orphaned_jobs.min_by { |job| job['orphaned_at'] || Float::INFINITY }
|
231
|
+
stats[:oldest_orphaned_job] = oldest_job['orphaned_duration'] if oldest_job
|
232
|
+
end
|
233
|
+
|
234
|
+
stats
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def cleanup_job_tracking(jid, instance_id)
|
240
|
+
AssuredJobs.redis_sync do |conn|
|
241
|
+
job_tracking_key = AssuredJobs.send(:namespaced_key, "jobs:#{instance_id}")
|
242
|
+
job_data_key = AssuredJobs.send(:namespaced_key, "job:#{jid}")
|
243
|
+
|
244
|
+
conn.multi do |multi|
|
245
|
+
multi.srem(job_tracking_key, jid)
|
246
|
+
multi.del(job_data_key)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Register the web extension with Sidekiq::Web
|
257
|
+
Sidekiq::Web.register(Sidekiq::AssuredJobs::Web)
|
258
|
+
Sidekiq::Web.tabs["Orphaned Jobs"] = "orphaned-jobs"
|