resque-stages 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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>