sidekiq-undertaker 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby-build.yml +1 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +6 -5
  5. data/assets/Undertaker_demo_1_job_1_error.png +0 -0
  6. data/assets/Undertaker_demo_1_job_all_errors.png +0 -0
  7. data/assets/Undertaker_demo_all_errors.png +0 -0
  8. data/assets/Undertaker_demo_all_jobs.png +0 -0
  9. data/assets/Undertaker_demo_all_jobs_1_error.png +0 -0
  10. data/lib/sidekiq/undertaker/version.rb +1 -1
  11. data/lib/sidekiq/undertaker/web_extension/api_helpers.rb +69 -12
  12. data/lib/sidekiq/undertaker/web_extension.rb +8 -1
  13. data/sidekiq-undertaker.gemspec +1 -0
  14. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_filter_page_is_called/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +16 -0
  15. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_all_failures_and_errors/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +6 -0
  16. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_specific_job_class_and_a_specific_error/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +6 -0
  17. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_specific_job_class_and_a_specific_error/with_pagination/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +6 -0
  18. data/spec/sidekiq/undertaker/job_distributor_spec.rb +3 -3
  19. data/spec/sidekiq/undertaker/job_filter_spec.rb +3 -3
  20. data/spec/sidekiq/undertaker/web_extension_spec.rb +115 -0
  21. data/web/locales/en.yml +2 -0
  22. data/web/views/filter.erb +16 -0
  23. data/web/views/morgue.erb +6 -0
  24. metadata +21 -6
  25. data/Demo_Filter.png +0 -0
  26. data/Demo_Job_Filter.png +0 -0
  27. data/Demo_Morgue_1_Job.png +0 -0
  28. data/Demo_Morgue_all.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbe7e0e84ffed4be546988e142fadfef4b8ec6643f16f8c48507a11227a6a2f5
4
- data.tar.gz: 798f2e9631d028423fcf5b10d403f4c7074d20014310293413af767866404610
3
+ metadata.gz: fb255bb741814575247793b7ddfdd7dfa6fe5c0d86a21df3b33f315209beef53
4
+ data.tar.gz: fc6b4970d6e7d2c8410525c76fd97fc1c98943a0f9bb381807946829e38180ab
5
5
  SHA512:
6
- metadata.gz: ccb8eaf232d8f751ea3f75f80c4eaf91eff50bda583a392c7206ba0c69bec4d32d0fe95e3feac947430d713caf65583a18f31ce37e80d0b327058bb7b4fca401
7
- data.tar.gz: 21380b3857b132a5e7fa7016c05a698d40808d86f02f260604b61ffe718f5810c921508088a696970e77149f8877848175f008dfd3b4034c7c50081dd641b7d9
6
+ metadata.gz: '0162683d05be1edf9d3dc362a7b7c543fc4d0d6891945540637cc03dcb919a1780003ed55503f3cbc3af8ab0ef3a03ee28bfe6d6575dac4610ee623dd373b3b1'
7
+ data.tar.gz: 610f3d98e380193606e37c74953dd53b00c79ae42fd6f091f940f27e9b61d55305df546a207676b8139a8d41719ba0fd3336cf87b5f35ec0b16b43283796c64a
@@ -39,7 +39,7 @@ jobs:
39
39
  - 'truffleruby+graalvm'
40
40
 
41
41
  steps:
42
- - uses: actions/checkout@v2
42
+ - uses: actions/checkout@v3
43
43
  - name: Set up Ruby
44
44
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
45
45
  # change this to (see https://github.com/ruby/setup-ruby#versioning):
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [1.3.0] - 2022-04-27
6
+ ### Added
7
+ - Added option to export and re-import dead jobs
8
+
5
9
  ## [1.2.0] - 2022-02-03
6
10
  ### Changed
7
11
  - Updated sidekiq dependency to 6.4.0
data/README.md CHANGED
@@ -45,24 +45,24 @@ Or install it yourself as:
45
45
 
46
46
  The filter page shows a table with time-buckets as columns and rows for each job class.
47
47
 
48
- ![Sidekiq Undertaker](Demo_Filter.png)
48
+ ![Sidekiq Undertaker](assets/Undertaker_demo_all_jobs.png)
49
49
 
50
50
  #### Job Filter View
51
51
 
52
52
  For each job class, you can drill down to view error distribution based on
53
53
  error class.
54
54
 
55
- ![Sidekiq Undertaker](Demo_Job_Filter.png)
55
+ ![Sidekiq Undertaker](assets/Undertaker_demo_1_job_all_errors.png)
56
56
 
57
57
  #### Morgue View
58
58
  Finally, click on the individual error counts to display details of the
59
59
  errors in a list form.
60
60
 
61
- ![Sidekiq Undertaker](Demo_Morgue_1_Job.png)
61
+ ![Sidekiq Undertaker](assets/Undertaker_demo_1_job_1_error.png)
62
62
 
63
63
  The morgue view can, for example, also show an error distribution over all job classes.
64
64
 
65
- ![Sidekiq Undertaker](Demo_Morgue_all.png)
65
+ ![Sidekiq Undertaker](assets/Undertaker_demo_all_jobs_1_error.png)
66
66
 
67
67
  ## Contributing
68
68
 
@@ -81,7 +81,8 @@ this fork was renamed to `sidekiq-undertaker`.
81
81
 
82
82
  The [Sidekiq-Cleaner](https://github.com/HackingHabits/sidekiq-cleaner) gem was originally created by [Madan Thangavelu](https://github.com/HackingHabits).
83
83
  [Tout](https://github.com/Tout/sidekiq-cleaner) and [TheWudu](https://github.com/TheWudu/sidekiq-cleaner) also contributed to it.
84
- Thank you [Zlatko Rednjak](https://github.com/Rednjak) for adding the initial version of the `CHANGELOG.md`.
84
+ [Zlatko Rednjak](https://github.com/Rednjak) added the initial version of the `CHANGELOG.md`.
85
+ [Lucas Dell'Isola](https://github.com/ldellisola) added the export/import feature for dead jobs.
85
86
 
86
87
  For the complete list of network members have a look at the [fork overview](https://github.com/ThomasKoppensteiner/sidekiq-undertaker/network/members).
87
88
 
Binary file
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Undertaker
5
- VERSION = "1.2.0"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -2,11 +2,16 @@
2
2
 
3
3
  require "sidekiq/undertaker/job_distributor"
4
4
  require "sidekiq/undertaker/job_filter"
5
+ require "json"
6
+ require "zip"
5
7
 
6
8
  module Sidekiq
7
9
  module Undertaker
8
10
  module WebExtension
11
+ # rubocop:disable Metrics/ModuleLength
9
12
  module APIHelpers
13
+ EXPORT_CHUNK_SIZE = 2000
14
+
10
15
  def show_filter
11
16
  store_request_params
12
17
 
@@ -59,22 +64,25 @@ module Sidekiq
59
64
  def post_undertaker
60
65
  raise ::ArgumentError.new("Key missing") unless params["key"]
61
66
 
62
- params["key"].each do |key|
63
- job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
64
- if job
65
- if params["retry"]
66
- job.retry
67
- elsif params["delete"]
68
- job.delete
69
- end
70
- end
71
- end
67
+ jobs = params["key"].map { |k| Sidekiq::DeadSet.new.fetch(*parse_params(k)).first }.compact
72
68
 
73
- redirect redirect_path(request)
69
+ handle_selected_jobs jobs
74
70
  rescue ::ArgumentError
75
71
  bad_request
76
72
  end
77
73
 
74
+ def handle_selected_jobs(jobs)
75
+ return send_data(*prepare_data(jobs.map(&:item), EXPORT_CHUNK_SIZE)) if params["export"]
76
+
77
+ if params["retry"]
78
+ jobs.each(&:retry)
79
+ elsif params["delete"]
80
+ jobs.each(&:delete)
81
+ end
82
+
83
+ redirect redirect_path(request)
84
+ end
85
+
78
86
  def post_undertaker_job_class_error_class_buckent_name_delete
79
87
  store_request_params
80
88
  @dead_jobs = Sidekiq::Undertaker::JobFilter.filter_dead_jobs(params)
@@ -96,6 +104,28 @@ module Sidekiq
96
104
  redirect redirect_path(request)
97
105
  end
98
106
 
107
+ def post_undertaker_job_class_error_class_buckent_name_export
108
+ store_request_params
109
+
110
+ @dead_jobs = Sidekiq::Undertaker::JobFilter.filter_dead_jobs(params)
111
+ send_data(*prepare_data(@dead_jobs.map { |j| j.job.item }, EXPORT_CHUNK_SIZE))
112
+ end
113
+
114
+ def post_import_jobs
115
+ file = params["upload_file"]
116
+ raise ::ArgumentError.new("The file is not a json") if file.nil? || file[:type] != "application/json"
117
+
118
+ data = params["upload_file"][:tempfile].read
119
+ dead_set = Sidekiq::DeadSet.new
120
+
121
+ JSON.parse(data).each do |job|
122
+ dead_set.kill(Sidekiq.dump_json(job))
123
+ end
124
+ redirect redirect_path(request)
125
+ rescue StandardError
126
+ bad_request
127
+ end
128
+
99
129
  def render_result(template)
100
130
  render(:erb, File.read(File.join(view_path, template)))
101
131
  end
@@ -112,13 +142,40 @@ module Sidekiq
112
142
 
113
143
  def redirect_path(request)
114
144
  path = request.referer ? URI.parse(request.referer).path : request.path
115
- path.gsub("/delete", "").gsub("/retry", "")
145
+ path.gsub("/delete", "").gsub("/retry", "").gsub("/export", "")
146
+ end
147
+
148
+ def prepare_data(data, chunk_size)
149
+ filename = Time.now.strftime("%Y-%m-%d_%H-%M").to_s
150
+ return [data.to_json, "application/json", "#{filename}.json"] if data.length <= chunk_size
151
+
152
+ filename = "#{@req_job_class}_#{filename}"
153
+ zip = Zip::OutputStream.write_buffer do |file|
154
+ data.each_slice(chunk_size).each_with_index do |chunk, index|
155
+ file.put_next_entry("#{filename}_part-#{index + 1}.json")
156
+ file.write chunk.to_json
157
+ end
158
+ end
159
+
160
+ [zip.string, "application/zip", "#{filename}.zip"]
161
+ end
162
+
163
+ def send_data(data, content_type = "text/plain", file_name = "attachment.txt")
164
+ [
165
+ 200,
166
+ {
167
+ "Content-Type" => content_type,
168
+ "Content-Disposition" => "attachment; filename=\"#{file_name}\""
169
+ },
170
+ [data]
171
+ ]
116
172
  end
117
173
 
118
174
  def bad_request
119
175
  [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
120
176
  end
121
177
  end
178
+ # rubocop:enable Metrics/ModuleLength
122
179
  end
123
180
  end
124
181
  end
@@ -12,7 +12,6 @@ module Sidekiq
12
12
  app.get "/undertaker/filter" do
13
13
  show_filter
14
14
  end
15
-
16
15
  app.get "/undertaker/filter/:job_class/:bucket_name" do
17
16
  show_filter_by_job_class_bucket_name
18
17
  end
@@ -32,6 +31,14 @@ module Sidekiq
32
31
  app.post "/undertaker/morgue/:job_class/:error_class/:bucket_name/retry" do
33
32
  post_undertaker_job_class_error_class_buckent_name_retry
34
33
  end
34
+
35
+ app.post "/undertaker/morgue/:job_class/:error_class/:bucket_name/export" do
36
+ post_undertaker_job_class_error_class_buckent_name_export
37
+ end
38
+
39
+ app.post "/undertaker/import_jobs" do
40
+ post_import_jobs
41
+ end
35
42
  end
36
43
  # rubocop:enable Metrics/MethodLength
37
44
  end
@@ -55,5 +55,6 @@ Gem::Specification.new do |spec|
55
55
  spec.add_development_dependency "sinatra", "~> 2.0"
56
56
  spec.add_development_dependency "timecop", "~> 0.9"
57
57
 
58
+ spec.add_runtime_dependency "rubyzip"
58
59
  spec.add_runtime_dependency "sidekiq", ">= 6.4", "< 7"
59
60
  end
@@ -211,6 +211,22 @@
211
211
 
212
212
  </table>
213
213
 
214
+ <header class="row header">
215
+ <div class="col-sm-12">
216
+ <h3>
217
+ Import Jobs
218
+ </h3>
219
+ </div>
220
+ </header>
221
+
222
+ <form enctype="multipart/form-data" method="post" action='/sidekiq/undertaker/import_jobs'>
223
+ <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
224
+ <input type="file" id=upload_file" name="upload_file" >
225
+ <button class="btn btn-danger" style="margin-top: 10px" type="submit">
226
+ Import
227
+ </button>
228
+ </form>
229
+
214
230
  </div>
215
231
  </div>
216
232
  </div>
@@ -276,6 +276,7 @@
276
276
 
277
277
  </table>
278
278
  <input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="Revive" />
279
+ <input class="btn btn-secondary btn-xs pull-left" type="submit" name="export" value="Export" />
279
280
  <input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="Bury" />
280
281
  </form>
281
282
 
@@ -284,6 +285,11 @@
284
285
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="Bury All" data-confirm="Are you sure?" />
285
286
  </form>
286
287
 
288
+ <form action="/sidekiq/undertaker/morgue/all/all/total_dead/export" method="post">
289
+ <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
290
+ <input class="btn btn-secondary btn-xs pull-right" type="submit" name="export" value="Export All" />
291
+ </form>
292
+
287
293
  <form action="/sidekiq/undertaker/morgue/all/all/total_dead/retry" method="post">
288
294
  <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
289
295
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="Revive All" data-confirm="Are you sure?" />
@@ -234,6 +234,7 @@
234
234
 
235
235
  </table>
236
236
  <input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="Revive" />
237
+ <input class="btn btn-secondary btn-xs pull-left" type="submit" name="export" value="Export" />
237
238
  <input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="Bury" />
238
239
  </form>
239
240
 
@@ -242,6 +243,11 @@
242
243
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="Bury All" data-confirm="Are you sure?" />
243
244
  </form>
244
245
 
246
+ <form action="/sidekiq/undertaker/morgue/HardWorker/RuntimeError/1_hour/export" method="post">
247
+ <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
248
+ <input class="btn btn-secondary btn-xs pull-right" type="submit" name="export" value="Export All" />
249
+ </form>
250
+
245
251
  <form action="/sidekiq/undertaker/morgue/HardWorker/RuntimeError/1_hour/retry" method="post">
246
252
  <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
247
253
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="Revive All" data-confirm="Are you sure?" />
@@ -1260,6 +1260,7 @@
1260
1260
 
1261
1261
  </table>
1262
1262
  <input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="Revive" />
1263
+ <input class="btn btn-secondary btn-xs pull-left" type="submit" name="export" value="Export" />
1263
1264
  <input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="Bury" />
1264
1265
  </form>
1265
1266
 
@@ -1268,6 +1269,11 @@
1268
1269
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="Bury All" data-confirm="Are you sure?" />
1269
1270
  </form>
1270
1271
 
1272
+ <form action="/sidekiq/undertaker/morgue/HardWorker/RuntimeError/1_hour/export" method="post">
1273
+ <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
1274
+ <input class="btn btn-secondary btn-xs pull-right" type="submit" name="export" value="Export All" />
1275
+ </form>
1276
+
1271
1277
  <form action="/sidekiq/undertaker/morgue/HardWorker/RuntimeError/1_hour/retry" method="post">
1272
1278
  <input type='hidden' name='authenticity_token' value='stubbed-csrf-token'/>
1273
1279
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="Revive All" data-confirm="Are you sure?" />
@@ -45,21 +45,21 @@ module Sidekiq
45
45
  let(:dead_job2) do
46
46
  DeadJob.new(
47
47
  job: job2, # 'A', 'E1'
48
- time_elapsed_since_failure: 10 + 60 * 60,
48
+ time_elapsed_since_failure: 10 + (60 * 60),
49
49
  bucket_name: "3_hours"
50
50
  )
51
51
  end
52
52
  let(:dead_job3) do
53
53
  DeadJob.new(
54
54
  job: job3, # 'B', 'E1'
55
- time_elapsed_since_failure: 10 + 60 * 60,
55
+ time_elapsed_since_failure: 10 + (60 * 60),
56
56
  bucket_name: "3_hours"
57
57
  )
58
58
  end
59
59
  let(:dead_job4) do
60
60
  DeadJob.new(
61
61
  job: job4, # 'B', 'E2'
62
- time_elapsed_since_failure: 10 + 60 * 60 * 24,
62
+ time_elapsed_since_failure: 10 + (60 * 60 * 24),
63
63
  bucket_name: "1_day"
64
64
  )
65
65
  end
@@ -9,7 +9,7 @@ module Sidekiq
9
9
  let(:job1) do
10
10
  instance_double(Sidekiq::JobRecord, item: {
11
11
  "class" => "HardWorkTask",
12
- "failed_at" => Time.now.to_i - 5 * 60,
12
+ "failed_at" => Time.now.to_i - (5 * 60),
13
13
  "error_class" => "NoMethodError"
14
14
  })
15
15
  end
@@ -17,7 +17,7 @@ module Sidekiq
17
17
  let(:job2) do
18
18
  instance_double(Sidekiq::JobRecord, item: {
19
19
  "class" => "HardWorkTask",
20
- "failed_at" => Time.now.to_i - 2 * 60 * 60,
20
+ "failed_at" => Time.now.to_i - (2 * 60 * 60),
21
21
  "error_class" => "RandomError"
22
22
  })
23
23
  end
@@ -25,7 +25,7 @@ module Sidekiq
25
25
  let(:job3) do
26
26
  instance_double(Sidekiq::JobRecord, item: {
27
27
  "class" => "LazyWorkTask",
28
- "failed_at" => Time.now.to_i - 2 * 60 * 60,
28
+ "failed_at" => Time.now.to_i - (2 * 60 * 60),
29
29
  "error_class" => "NoMethodError"
30
30
  })
31
31
  end
@@ -6,6 +6,7 @@ require "sidekiq/api"
6
6
  require "sidekiq/web"
7
7
  require "sinatra"
8
8
  require "rack/test"
9
+ require "stringio"
9
10
 
10
11
  module Sidekiq
11
12
  # rubocop:disable Metrics/ModuleLength
@@ -205,6 +206,67 @@ module Sidekiq
205
206
  end
206
207
  end
207
208
 
209
+ describe "import" do
210
+ subject { post "/undertaker/import_jobs", "upload_file" => file }
211
+
212
+ let(:file) do
213
+ Rack::Test::UploadedFile.new(StringIO.new(file_content), file_content_type, original_filename: file_name)
214
+ end
215
+ let(:job) do
216
+ opts = default_job_opts.merge({ "class" => "SuperHardWorking" })
217
+
218
+ build_job(opts)
219
+ end
220
+
221
+ context "when the file is valid" do
222
+ let(:file_content) { [job.item].to_json }
223
+ let(:file_name) { "jobs.json" }
224
+ let(:file_content_type) { "application/json" }
225
+
226
+ it "redirects the response" do
227
+ subject
228
+ expect(last_response.status).to eq 302
229
+ end
230
+
231
+ it "adds the jobs to the deadset" do
232
+ expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(5)
233
+ end
234
+ end
235
+
236
+ context "when the file type is not valid" do
237
+ let(:file_content) { "" }
238
+ let(:file_name) { "jobs.zip" }
239
+ let(:file_content_type) { "application/zip" }
240
+
241
+ it "returns status 400" do
242
+ subject
243
+ expect(last_response.status).to eq 400
244
+ end
245
+ end
246
+
247
+ context "when the file type is a json but not a Sidekiq Job" do
248
+ let(:file_content) { "{am_i_a_job: \"no\"}" }
249
+ let(:file_name) { "jobs.json" }
250
+ let(:file_content_type) { "application/json" }
251
+
252
+ it "returns status 400" do
253
+ subject
254
+ expect(last_response.status).to eq 400
255
+ end
256
+ end
257
+
258
+ context "when the content of the file is not a json" do
259
+ let(:file_content) { "DEFINETLY NOT A JSON" }
260
+ let(:file_name) { "jobs.json" }
261
+ let(:file_content_type) { "application/json" }
262
+
263
+ it "returns status 400" do
264
+ subject
265
+ expect(last_response.status).to eq 400
266
+ end
267
+ end
268
+ end
269
+
208
270
  describe "retry" do
209
271
  context "when job class, error and bucket are given" do
210
272
  subject { post "/undertaker/morgue/HardWorker/RuntimeError/1_hour/retry" }
@@ -262,6 +324,50 @@ module Sidekiq
262
324
  end
263
325
  end
264
326
 
327
+ describe "export" do
328
+ context "when job class, error and bucket are given" do
329
+ subject { post "/undertaker/morgue/HardWorker/RuntimeError/1_hour/export" }
330
+
331
+ let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
332
+ let(:expected_content_disposition_header) { "attachment; filename=\"2018-12-16_20-57.json\"" }
333
+
334
+ let(:params) do
335
+ { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour" }
336
+ end
337
+ let(:dead_jobs_set) { [dead_job1, dead_job2] }
338
+ let(:dead_job1) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid1)) }
339
+ let(:dead_job2) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid2)) }
340
+
341
+ before do
342
+ allow(Sidekiq::Undertaker::JobFilter).to receive(:filter_dead_jobs).with(params)
343
+ .and_return(dead_jobs_set)
344
+ end
345
+
346
+ it "exports the dead jobs" do
347
+ subject
348
+ expect(last_response.status).to eq 200
349
+ expect(last_response.content_type).to eq "application/json"
350
+ expect(last_response.headers["Content-Disposition"]).to eq expected_content_disposition_header
351
+ expect(last_response.body).to eq dead_jobs_set.map { |t| t.job.item }.to_json
352
+ end
353
+
354
+ context "when there are more jobs than the current CHUNK_SIZE" do
355
+ before do
356
+ stub_const("Sidekiq::Undertaker::WebExtension::APIHelpers::EXPORT_CHUNK_SIZE", 1)
357
+ end
358
+
359
+ let(:expected_content_disposition_header) { "attachment; filename=\"HardWorker_2018-12-16_20-57.zip\"" }
360
+
361
+ it "exports the dead jobs" do
362
+ subject
363
+ expect(last_response.status).to eq 200
364
+ expect(last_response.content_type).to eq "application/zip"
365
+ expect(last_response.headers["Content-Disposition"]).to eq expected_content_disposition_header
366
+ end
367
+ end
368
+ end
369
+ end
370
+
265
371
  describe "specific jobs" do
266
372
  let(:dead_job) { Sidekiq::DeadSet.new.find_job(jid1) }
267
373
 
@@ -282,6 +388,15 @@ module Sidekiq
282
388
  post "/undertaker/morgue", "key[]=#{job_refs[0]}&retry=Retry+Now"
283
389
  end
284
390
 
391
+ it "exports specific dead job now" do
392
+ post "/undertaker/morgue", "key[]=#{job_refs[0]}&export=now"
393
+
394
+ expect(last_response.status).to eq 200
395
+ expect(last_response.content_type).to eq "application/json"
396
+ expect(last_response.headers["Content-Disposition"]).to eq "attachment; filename=\"2018-12-16_20-57.json\""
397
+ expect(last_response.body).to eq [dead_job.item].to_json
398
+ end
399
+
285
400
  it "redirects on specific retry post" do
286
401
  post("/undertaker/morgue",
287
402
  "key[]=#{job_refs[0]}&retry=Retry+Now",
data/web/locales/en.yml CHANGED
@@ -3,3 +3,5 @@ en:
3
3
  UndertakerBuryAll: Bury All
4
4
  UndertakerRevive: Revive
5
5
  UndertakerReviveAll: Revive All
6
+ UndertakerExport: Export
7
+ UndertakerExportAll: Export All
data/web/views/filter.erb CHANGED
@@ -32,3 +32,19 @@
32
32
  </tr>
33
33
  <% end %>
34
34
  </table>
35
+
36
+ <header class="row header">
37
+ <div class="col-sm-12">
38
+ <h3>
39
+ Import Jobs
40
+ </h3>
41
+ </div>
42
+ </header>
43
+
44
+ <form enctype="multipart/form-data" method="post" action='<%="#{root_path}undertaker/import_jobs"%>'>
45
+ <%= respond_to?(:csrf_tag) && csrf_tag %>
46
+ <input type="file" id=upload_file" name="upload_file" >
47
+ <button class="btn btn-danger" style="margin-top: 10px" type="submit">
48
+ Import
49
+ </button>
50
+ </form>
data/web/views/morgue.erb CHANGED
@@ -56,6 +56,7 @@
56
56
  <% end %>
57
57
  </table>
58
58
  <input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="<%= t('UndertakerRevive') %>" />
59
+ <input class="btn btn-secondary btn-xs pull-left" type="submit" name="export" value="<%= t('UndertakerExport') %>" />
59
60
  <input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="<%= t('UndertakerBury') %>" />
60
61
  </form>
61
62
 
@@ -64,6 +65,11 @@
64
65
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="<%= t('UndertakerBuryAll') %>" data-confirm="<%= t('AreYouSure') %>" />
65
66
  </form>
66
67
 
68
+ <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{@req_bucket_name}/export" %>" method="post">
69
+ <%= respond_to?(:csrf_tag) && csrf_tag %>
70
+ <input class="btn btn-secondary btn-xs pull-right" type="submit" name="export" value="<%= t('UndertakerExportAll') %>" />
71
+ </form>
72
+
67
73
  <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{@req_bucket_name}/retry" %>" method="post">
68
74
  <%= respond_to?(:csrf_tag) && csrf_tag %>
69
75
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="<%= t('UndertakerReviveAll') %>" data-confirm="<%= t('AreYouSure') %>" />
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-undertaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Koppensteiner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-03 00:00:00.000000000 Z
11
+ date: 2022-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -282,6 +282,20 @@ dependencies:
282
282
  - - "~>"
283
283
  - !ruby/object:Gem::Version
284
284
  version: '0.9'
285
+ - !ruby/object:Gem::Dependency
286
+ name: rubyzip
287
+ requirement: !ruby/object:Gem::Requirement
288
+ requirements:
289
+ - - ">="
290
+ - !ruby/object:Gem::Version
291
+ version: '0'
292
+ type: :runtime
293
+ prerelease: false
294
+ version_requirements: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - ">="
297
+ - !ruby/object:Gem::Version
298
+ version: '0'
285
299
  - !ruby/object:Gem::Dependency
286
300
  name: sidekiq
287
301
  requirement: !ruby/object:Gem::Requirement
@@ -322,14 +336,15 @@ files:
322
336
  - ".rubocop_todo.yml"
323
337
  - ".travis.yml"
324
338
  - CHANGELOG.md
325
- - Demo_Filter.png
326
- - Demo_Job_Filter.png
327
- - Demo_Morgue_1_Job.png
328
- - Demo_Morgue_all.png
329
339
  - Gemfile
330
340
  - LICENSE.txt
331
341
  - README.md
332
342
  - Rakefile
343
+ - assets/Undertaker_demo_1_job_1_error.png
344
+ - assets/Undertaker_demo_1_job_all_errors.png
345
+ - assets/Undertaker_demo_all_errors.png
346
+ - assets/Undertaker_demo_all_jobs.png
347
+ - assets/Undertaker_demo_all_jobs_1_error.png
333
348
  - lib/sidekiq/undertaker.rb
334
349
  - lib/sidekiq/undertaker/bucket.rb
335
350
  - lib/sidekiq/undertaker/dead_job.rb
data/Demo_Filter.png DELETED
Binary file
data/Demo_Job_Filter.png DELETED
Binary file
Binary file
data/Demo_Morgue_all.png DELETED
Binary file