resque-stages 0.0.1

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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +103 -0
  5. data/.rubocop_todo.yml +34 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +6 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +9 -0
  10. data/Gemfile.lock +172 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +250 -0
  13. data/Rakefile +8 -0
  14. data/bin/console +16 -0
  15. data/bin/setup +8 -0
  16. data/lib/resque-stages.rb +11 -0
  17. data/lib/resque/plugins/stages.rb +110 -0
  18. data/lib/resque/plugins/stages/cleaner.rb +36 -0
  19. data/lib/resque/plugins/stages/redis_access.rb +16 -0
  20. data/lib/resque/plugins/stages/staged_group.rb +181 -0
  21. data/lib/resque/plugins/stages/staged_group_list.rb +79 -0
  22. data/lib/resque/plugins/stages/staged_group_stage.rb +275 -0
  23. data/lib/resque/plugins/stages/staged_job.rb +271 -0
  24. data/lib/resque/plugins/stages/version.rb +9 -0
  25. data/lib/resque/server/public/stages.css +56 -0
  26. data/lib/resque/server/views/_group_stages_list_pagination.erb +67 -0
  27. data/lib/resque/server/views/_group_stages_list_table.erb +25 -0
  28. data/lib/resque/server/views/_stage_job_list_pagination.erb +72 -0
  29. data/lib/resque/server/views/_stage_job_list_table.erb +46 -0
  30. data/lib/resque/server/views/_staged_group_list_pagination.erb +67 -0
  31. data/lib/resque/server/views/_staged_group_list_table.erb +44 -0
  32. data/lib/resque/server/views/group_stages_list.erb +58 -0
  33. data/lib/resque/server/views/groups.erb +40 -0
  34. data/lib/resque/server/views/job_details.erb +91 -0
  35. data/lib/resque/server/views/stage.erb +64 -0
  36. data/lib/resque/stages_server.rb +240 -0
  37. data/read_me/groups_list.png +0 -0
  38. data/read_me/job.png +0 -0
  39. data/read_me/stage.png +0 -0
  40. data/read_me/stages.png +0 -0
  41. data/resque-stages.gemspec +49 -0
  42. data/spec/rails_helper.rb +40 -0
  43. data/spec/resque/plugins/stages/cleaner_spec.rb +82 -0
  44. data/spec/resque/plugins/stages/staged_group_list_spec.rb +96 -0
  45. data/spec/resque/plugins/stages/staged_group_spec.rb +226 -0
  46. data/spec/resque/plugins/stages/staged_group_stage_spec.rb +293 -0
  47. data/spec/resque/plugins/stages/staged_job_spec.rb +324 -0
  48. data/spec/resque/plugins/stages_spec.rb +369 -0
  49. data/spec/resque/server/public/stages.css_spec.rb +18 -0
  50. data/spec/resque/server/views/group_stages_list.erb_spec.rb +67 -0
  51. data/spec/resque/server/views/groups.erb_spec.rb +81 -0
  52. data/spec/resque/server/views/job_details.erb_spec.rb +100 -0
  53. data/spec/resque/server/views/stage.erb_spec.rb +68 -0
  54. data/spec/spec_helper.rb +104 -0
  55. data/spec/support/01_utils/fake_logger.rb +7 -0
  56. data/spec/support/config/redis-auth.yml +12 -0
  57. data/spec/support/fake_logger.rb +7 -0
  58. data/spec/support/jobs/basic_job.rb +17 -0
  59. data/spec/support/jobs/compressed_job.rb +18 -0
  60. data/spec/support/jobs/retry_job.rb +21 -0
  61. data/spec/support/purge_all.rb +15 -0
  62. metadata +297 -0
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resque
4
+ module Plugins
5
+ module Stages
6
+ # A class representing a staged job.
7
+ #
8
+ # Staged jobs can have the following statuses:
9
+ # :pending - not yet run
10
+ # :queued - in the Resque queue
11
+ # :running - currently running
12
+ # :pending_re_run - currently in the retry queue
13
+ # :failed - completed with a failure
14
+ # :successful - completed successfully
15
+
16
+ # rubocop:disable Metrics/ClassLength
17
+ class StagedJob
18
+ include Resque::Plugins::Stages::RedisAccess
19
+ include Comparable
20
+
21
+ attr_reader :job_id
22
+ attr_writer :class_name
23
+
24
+ class << self
25
+ # Creates a job to be queued to Resque that has an ID that we can track its status with.
26
+ def create_job(staged_group_stage, klass, *args)
27
+ job = Resque::Plugins::Stages::StagedJob.new(SecureRandom.uuid)
28
+
29
+ job.staged_group_stage = staged_group_stage
30
+ job.class_name = klass.name
31
+ job.args = args
32
+
33
+ job.save!
34
+
35
+ job
36
+ end
37
+ end
38
+
39
+ def initialize(job_id)
40
+ @job_id = job_id
41
+ end
42
+
43
+ def <=>(other)
44
+ return nil unless other.is_a?(Resque::Plugins::Stages::StagedJob)
45
+
46
+ job_id <=> other.job_id
47
+ end
48
+
49
+ def class_name
50
+ @class_name ||= stored_values[:class_name]
51
+ end
52
+
53
+ def queue_time
54
+ @queue_time ||= stored_values[:queue_time].to_time
55
+ end
56
+
57
+ def status
58
+ @status ||= stored_values[:status]&.to_sym || :pending
59
+ end
60
+
61
+ def status=(value)
62
+ @status = value
63
+ redis.hset(job_key, "status", status)
64
+
65
+ notify_stage
66
+ end
67
+
68
+ def status_message
69
+ @status_message ||= stored_values[:status_message]
70
+ end
71
+
72
+ def status_message=(value)
73
+ @status_message = value
74
+ redis.hset(job_key, "status_message", status_message)
75
+ end
76
+
77
+ def staged_group_stage
78
+ return nil if staged_group_stage_id.blank?
79
+
80
+ @staged_group_stage ||= Resque::Plugins::Stages::StagedGroupStage.new(staged_group_stage_id)
81
+ end
82
+
83
+ def staged_group_stage=(value)
84
+ @staged_group_stage = value
85
+ @staged_group_stage_id = value.group_stage_id
86
+
87
+ redis.hset(job_key, "staged_group_stage_id", staged_group_stage_id)
88
+
89
+ value.add_job(self)
90
+ end
91
+
92
+ # rubocop:disable Metrics/AbcSize
93
+ def save!
94
+ redis.hsetnx(job_key, "queue_time", Time.now)
95
+ redis.hset(job_key, "class_name", class_name)
96
+ redis.hset(job_key, "args", encode_args(*compressed_args(args)))
97
+ redis.hset(job_key, "staged_group_stage_id", staged_group_stage_id)
98
+ redis.hset(job_key, "status", status)
99
+ redis.hset(job_key, "status_message", status_message)
100
+ end
101
+
102
+ # rubocop:enable Metrics/AbcSize
103
+
104
+ def delete
105
+ # Make sure the job is loaded into memory so we can use it even though we are going to delete it.
106
+ stored_values
107
+
108
+ redis.del(job_key)
109
+
110
+ staged_group_stage.remove_job(self)
111
+ end
112
+
113
+ def enqueue_job
114
+ case status
115
+ when :pending
116
+ self.status = :queued
117
+ Resque.enqueue(*enqueue_args)
118
+
119
+ when :pending_re_run
120
+ Resque.enqueue_delayed_selection do |args|
121
+ # :nocov:
122
+ klass.perform_job(*Array.wrap(args)).job_id == job_id
123
+ # :nocov:
124
+ end
125
+ end
126
+ end
127
+
128
+ def enqueue_args
129
+ [klass, *enqueue_compressed_args]
130
+ end
131
+
132
+ def enqueue_compressed_args
133
+ new_args = compressed_args([{ staged_job_id: job_id }, *args])
134
+
135
+ new_args[0][:staged_job_id] = job_id
136
+
137
+ new_args
138
+ end
139
+
140
+ def uncompressed_args
141
+ decompress_args(args)
142
+ end
143
+
144
+ def args
145
+ @args = if defined?(@args)
146
+ @args
147
+ else
148
+ decompress_args(Array.wrap(decode_args(stored_values[:args])))
149
+ end
150
+ end
151
+
152
+ def args=(value)
153
+ @args = value.nil? ? [] : Array.wrap(value).dup
154
+ end
155
+
156
+ def completed?
157
+ %i[failed successful].include? status
158
+ end
159
+
160
+ def queued?
161
+ %i[queued running pending_re_run].include? status
162
+ end
163
+
164
+ def pending?
165
+ %i[pending pending_re_run].include? status
166
+ end
167
+
168
+ def blank?
169
+ !redis.exists(job_key)
170
+ end
171
+
172
+ def verify
173
+ return build_new_structure if staged_group_stage.blank?
174
+
175
+ staged_group_stage.verify
176
+ staged_group_stage.verify_job(self)
177
+ end
178
+
179
+ private
180
+
181
+ def build_new_structure
182
+ group = Resque::Plugins::Stages::StagedGroup.new(SecureRandom.uuid)
183
+ stage = group.current_stage
184
+
185
+ self.staged_group_stage = stage
186
+ end
187
+
188
+ def stored_values
189
+ @stored_values ||= (redis.hgetall(job_key) || {}).with_indifferent_access
190
+ end
191
+
192
+ def klass
193
+ @klass ||= class_name.constantize
194
+ end
195
+
196
+ def encode_args(*args)
197
+ Resque.encode(args)
198
+ end
199
+
200
+ def decode_args(args_string)
201
+ return if args_string.blank?
202
+
203
+ Resque.decode(args_string)
204
+ end
205
+
206
+ def job_key
207
+ "StagedJob::#{job_id}"
208
+ end
209
+
210
+ def staged_group_stage_id
211
+ @staged_group_stage_id ||= stored_values[:staged_group_stage_id]
212
+ end
213
+
214
+ def notify_stage
215
+ return if staged_group_stage.blank?
216
+
217
+ if status == :pending
218
+ mark_stage_pending
219
+ elsif queued?
220
+ mark_stage_running
221
+ else
222
+ staged_group_stage.job_completed
223
+ end
224
+ end
225
+
226
+ def mark_stage_pending
227
+ return if %i[running pending].include? staged_group_stage.status
228
+
229
+ staged_group_stage.status = :pending
230
+ end
231
+
232
+ def mark_stage_running
233
+ return if staged_group_stage.status == :running
234
+
235
+ staged_group_stage.status = :running
236
+ end
237
+
238
+ def described_class
239
+ return if class_name.blank?
240
+
241
+ class_name.constantize
242
+ rescue StandardError
243
+ # :nocov:
244
+ nil
245
+ # :nocov:
246
+ end
247
+
248
+ def compressable?
249
+ !described_class.blank? &&
250
+ described_class.singleton_class.included_modules.map(&:name).include?("Resque::Plugins::Compressible")
251
+ end
252
+
253
+ def compressed_args(compress_args)
254
+ return compress_args unless compressable?
255
+ return compress_args if described_class.compressed?(compress_args)
256
+
257
+ [{ resque_compressed: true, payload: described_class.compressed_args(compress_args) }]
258
+ end
259
+
260
+ def decompress_args(basic_args)
261
+ return basic_args unless compressable?
262
+ return basic_args unless described_class.compressed?(basic_args)
263
+
264
+ described_class.uncompressed_args(basic_args.first[:payload] || basic_args.first["payload"])
265
+ end
266
+ end
267
+
268
+ # rubocop:enable Metrics/ClassLength
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resque
4
+ module Plugins
5
+ module Stages
6
+ VERSION = "0.0.1"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ .stages_pagination_block {
2
+ width: 100%;
3
+ display: flex;
4
+ justify-content: space-between;
5
+ margin-bottom: 5px;
6
+ }
7
+
8
+ .stages_first_page {
9
+ margin-right: 5px;
10
+ }
11
+
12
+ .stages_prev_page {
13
+ margin-right: 5px;
14
+ margin-left: 5px;
15
+ }
16
+
17
+ .stages_page {
18
+ margin-right: 5px;
19
+ margin-left: 5px;
20
+ }
21
+
22
+ .stages_next_page {
23
+ margin-right: 5px;
24
+ margin-left: 5px;
25
+ }
26
+
27
+ .stages_last_page {
28
+ margin-left: 5px;
29
+ }
30
+
31
+ .table_container {
32
+ width: 100%;
33
+ overflow-x: scroll;
34
+ }
35
+
36
+ .stages_error {
37
+ background-color: lightcoral;
38
+ }
39
+
40
+ .stages_linear_history_div {
41
+ margin-top: 5px;
42
+ float: left;
43
+ }
44
+
45
+ #stages_search_div {
46
+ float: right;
47
+ margin-bottom: 10px;
48
+ }
49
+
50
+ #stages_search_div form {
51
+ margin-top: 0px;
52
+ }
53
+
54
+ .stages_reset {
55
+ clear: both;
56
+ }
@@ -0,0 +1,67 @@
1
+ <div class="stages_pagination_block">
2
+ <% total_pages = group.num_stages / page_size %>
3
+ <% total_pages += 1 if group.num_stages % page_size > 0 %>
4
+ <% page_num = 1 if page_num > total_pages || page_num < 1 %>
5
+ <% first_page = [1, page_num - 3].max %>
6
+ <% last_page = [total_pages, page_num + 3].min %>
7
+ <% last_page = page_num < 4 ? [total_pages, last_page + (4 - page_num)].min : last_page %>
8
+ <% first_page = page_num > total_pages - 3 ? [1, first_page + ((total_pages - page_num) - 3)].max : first_page %>
9
+
10
+ <% if total_pages > 1 %>
11
+ <div class="stages_prev_links">
12
+ <a href="<%= u("stages/group_stages_list") %>?<%=
13
+ { group_id: group_id,
14
+ page_size: page_size,
15
+ page_num: 1 }.to_param %>"
16
+ class="stages_first_page"
17
+ disabled="<%= first_page > 1 %>">
18
+ &lt;&lt; First
19
+ </a>
20
+
21
+ <a href="<%= u("stages/group_stages_list") %>?<%=
22
+ { group_id: group_id,
23
+ page_size: page_size,
24
+ page_num: [1, page_num - 1].max }.to_param %>"
25
+ class="stages_prev_page"
26
+ disabled="<%= page_num > 1 %>">
27
+ &lt; Prev
28
+ </a>
29
+ </div>
30
+
31
+ <div class="stages_pages">
32
+ <% (first_page..last_page).each do |page_number| %>
33
+ <% if page_number != page_num %>
34
+ <a href="<%= u("stages/group_stages_list") %>?<%=
35
+ { group_id: group_id,
36
+ page_size: page_size,
37
+ page_num: page_number }.to_param %>"
38
+ class="stages_page">
39
+ <%= page_number %>
40
+ </a>
41
+ <% else %>
42
+ <%= page_number %>
43
+ <% end %>
44
+ <% end %>
45
+ </div>
46
+
47
+ <div class="stages_next_links">
48
+ <a href="<%= u("stages/group_stages_list") %>?<%=
49
+ { group_id: group_id,
50
+ page_size: page_size,
51
+ page_num: [total_pages, page_num + 1].min }.to_param %>"
52
+ class="stages_next_page"
53
+ disabled="<%= page_num < last_page %>">
54
+ Next &gt;
55
+ </a>
56
+
57
+ <a href="<%= u("stages/group_stages_list") %>?<%=
58
+ { group_id: group_id,
59
+ page_size: page_size,
60
+ page_num: total_pages }.to_param %>"
61
+ class="stages_last_page"
62
+ disabled="<%= last_page < total_pages %>">
63
+ Last &gt;&gt;
64
+ </a>
65
+ </div>
66
+ <% end %>
67
+ </div>
@@ -0,0 +1,25 @@
1
+ <div class="table_container">
2
+ <table>
3
+ <tr>
4
+ <th>Number</th>
5
+ <th>Status</th>
6
+ <th>Num Jobs</th>
7
+ </tr>
8
+ <% stages.each do |stage| %>
9
+ <tr>
10
+ <td>
11
+ <a href="<%= u("stages/stage") %>?<%=
12
+ { group_stage_id: stage.group_stage_id }.to_param %>">
13
+ <%= stage.number %>
14
+ </a>
15
+ </td>
16
+ <td>
17
+ <%= stage.status %>
18
+ </td>
19
+ <td>
20
+ <%= stage.num_jobs %>
21
+ </td>
22
+ </tr>
23
+ <% end %>
24
+ </table>
25
+ </div>
@@ -0,0 +1,72 @@
1
+ <div class="stages_pagination_block">
2
+ <% total_pages = staged_group_stage.num_jobs / page_size %>
3
+ <% total_pages += 1 if staged_group_stage.num_jobs % page_size > 0 %>
4
+ <% page_num = 1 if page_num > total_pages || page_num < 1 %>
5
+ <% first_page = [1, page_num - 3].max %>
6
+ <% last_page = [total_pages, page_num + 3].min %>
7
+ <% last_page = page_num < 4 ? [total_pages, last_page + (4 - page_num)].min : last_page %>
8
+ <% first_page = page_num > total_pages - 3 ? [1, first_page + ((total_pages - page_num) - 3)].max : first_page %>
9
+
10
+ <% if total_pages > 1 %>
11
+ <div class="stages_prev_links">
12
+ <a href="<%= u("stages/stage") %>?<%= { group_stage_id: staged_group_stage.group_stage_id,
13
+ sort: @sort_by,
14
+ page_size: page_size,
15
+ page_num: 1,
16
+ order: @sort_order }.to_param %>"
17
+ class="stages_first_page"
18
+ disabled="<%= first_page > 1 %>">
19
+ &lt;&lt; First
20
+ </a>
21
+
22
+ <a href="<%= u("stages/stage") %>?<%= { group_stage_id: staged_group_stage.group_stage_id,
23
+ sort: @sort_by,
24
+ page_size: page_size,
25
+ page_num: [1, page_num - 1].max,
26
+ order: @sort_order }.to_param %>"
27
+ class="stages_prev_page"
28
+ disabled="<%= page_num > 1 %>">
29
+ &lt; Prev
30
+ </a>
31
+ </div>
32
+
33
+ <div class="stages_pages">
34
+ <% (first_page..last_page).each do |page_number| %>
35
+ <% if page_number != page_num %>
36
+ <a href="<%= u("stages/stage") %>?<%= { group_stage_id: staged_group_stage.group_stage_id,
37
+ sort: @sort_by,
38
+ page_size: page_size,
39
+ page_num: page_number,
40
+ order: @sort_order }.to_param %>"
41
+ class="stages_page">
42
+ <%= page_number %>
43
+ </a>
44
+ <% else %>
45
+ <%= page_number %>
46
+ <% end %>
47
+ <% end %>
48
+ </div>
49
+
50
+ <div class="stages_next_links">
51
+ <a href="<%= u("stages/stage") %>?<%= { group_stage_id: staged_group_stage.group_stage_id,
52
+ sort: @sort_by,
53
+ page_size: page_size,
54
+ page_num: [total_pages, page_num + 1].min,
55
+ order: @sort_order }.to_param %>"
56
+ class="stages_next_page"
57
+ disabled="<%= page_num < last_page %>">
58
+ Next &gt;
59
+ </a>
60
+
61
+ <a href="<%= u("stages/stage") %>?<%= { group_stage_id: staged_group_stage.group_stage_id,
62
+ sort: @sort_by,
63
+ page_size: page_size,
64
+ page_num: total_pages,
65
+ order: @sort_order }.to_param %>"
66
+ class="stages_last_page"
67
+ disabled="<%= last_page < total_pages %>">
68
+ Last &gt;&gt;
69
+ </a>
70
+ </div>
71
+ <% end %>
72
+ </div>