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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +103 -0
- data/.rubocop_todo.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +172 -0
- data/LICENSE.txt +21 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/lib/resque-stages.rb +11 -0
- data/lib/resque/plugins/stages.rb +110 -0
- data/lib/resque/plugins/stages/cleaner.rb +36 -0
- data/lib/resque/plugins/stages/redis_access.rb +16 -0
- data/lib/resque/plugins/stages/staged_group.rb +181 -0
- data/lib/resque/plugins/stages/staged_group_list.rb +79 -0
- data/lib/resque/plugins/stages/staged_group_stage.rb +275 -0
- data/lib/resque/plugins/stages/staged_job.rb +271 -0
- data/lib/resque/plugins/stages/version.rb +9 -0
- data/lib/resque/server/public/stages.css +56 -0
- data/lib/resque/server/views/_group_stages_list_pagination.erb +67 -0
- data/lib/resque/server/views/_group_stages_list_table.erb +25 -0
- data/lib/resque/server/views/_stage_job_list_pagination.erb +72 -0
- data/lib/resque/server/views/_stage_job_list_table.erb +46 -0
- data/lib/resque/server/views/_staged_group_list_pagination.erb +67 -0
- data/lib/resque/server/views/_staged_group_list_table.erb +44 -0
- data/lib/resque/server/views/group_stages_list.erb +58 -0
- data/lib/resque/server/views/groups.erb +40 -0
- data/lib/resque/server/views/job_details.erb +91 -0
- data/lib/resque/server/views/stage.erb +64 -0
- data/lib/resque/stages_server.rb +240 -0
- data/read_me/groups_list.png +0 -0
- data/read_me/job.png +0 -0
- data/read_me/stage.png +0 -0
- data/read_me/stages.png +0 -0
- data/resque-stages.gemspec +49 -0
- data/spec/rails_helper.rb +40 -0
- data/spec/resque/plugins/stages/cleaner_spec.rb +82 -0
- data/spec/resque/plugins/stages/staged_group_list_spec.rb +96 -0
- data/spec/resque/plugins/stages/staged_group_spec.rb +226 -0
- data/spec/resque/plugins/stages/staged_group_stage_spec.rb +293 -0
- data/spec/resque/plugins/stages/staged_job_spec.rb +324 -0
- data/spec/resque/plugins/stages_spec.rb +369 -0
- data/spec/resque/server/public/stages.css_spec.rb +18 -0
- data/spec/resque/server/views/group_stages_list.erb_spec.rb +67 -0
- data/spec/resque/server/views/groups.erb_spec.rb +81 -0
- data/spec/resque/server/views/job_details.erb_spec.rb +100 -0
- data/spec/resque/server/views/stage.erb_spec.rb +68 -0
- data/spec/spec_helper.rb +104 -0
- data/spec/support/01_utils/fake_logger.rb +7 -0
- data/spec/support/config/redis-auth.yml +12 -0
- data/spec/support/fake_logger.rb +7 -0
- data/spec/support/jobs/basic_job.rb +17 -0
- data/spec/support/jobs/compressed_job.rb +18 -0
- data/spec/support/jobs/retry_job.rb +21 -0
- data/spec/support/purge_all.rb +15 -0
- 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,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
|
+
<< 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
|
+
< 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 >
|
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 >>
|
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
|
+
<< 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
|
+
< 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 >
|
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 >>
|
69
|
+
</a>
|
70
|
+
</div>
|
71
|
+
<% end %>
|
72
|
+
</div>
|