sidekiq-undertaker 1.2.0 → 1.3.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.
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