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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 213608148534e316bf24588d51b181572130d503b664db631ce2a0bb5be53f1a
4
- data.tar.gz: 5e158bcc84da84d90ed15918ba1216ad6eddcf555aa3f9ca923bf840551e57ad
3
+ metadata.gz: a5ea24f7575f4e5183d9d21a0d3a59f623496e19f13e79a611db1eb286ef87b5
4
+ data.tar.gz: 5f4a107e730f7f9ae5ae4a7efb51e4789441689395cd08d8f22237b8f73acd83
5
5
  SHA512:
6
- metadata.gz: e63bf1b133743c43def063eede4e64a94a19d10ab90775f5cf0b36e0cdf9a50dc0597c78e349a4df2c650119b02db9cc8fb165b911ffff4af5cdd7a570df249a
7
- data.tar.gz: f41c16f24cf7273f41a76f39c301f4398ed989496355cbe37097ebe7fedf03b798bf623e48c632026d46996caf9e41cf134a19fb6479e914365e026f47df70e3
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module AssuredJobs
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  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"