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
data/README.md
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
# Resque::Stages
|
2
|
+
|
3
|
+
A Resque plugin for executing jobs in stages. Groups are created each
|
4
|
+
with multiple stages. Each stage contains a number of Jobs. All jobs
|
5
|
+
within a stage must complete before the next stage is executed. What
|
6
|
+
makes this gem special is that it will wait for jobs to Retry before
|
7
|
+
they are considered complete and the next stage will be executed.
|
8
|
+
|
9
|
+
Once all jobs in all stages are complete, the stages and jobs are all
|
10
|
+
deleted - cleaning up after itself.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'resque-stages'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle install
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install resque-stages
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
###Basic Usage
|
31
|
+
|
32
|
+
Include the stages plugin in all jobs that will be enqueued as a part of
|
33
|
+
a Stage. Do not worry, you will not have to enqueue the job only as
|
34
|
+
part of a stage. You will be able to use the job inside of or outside
|
35
|
+
of a stage.
|
36
|
+
|
37
|
+
This gem will enqueue a job with extra paramters when it is enqueued
|
38
|
+
as a part of a stage. To support this, the
|
39
|
+
`perform_job` method is added to your class and it will return an
|
40
|
+
object which will have the un-altered arguments for the job - whether or
|
41
|
+
not the job is called with the extra parameters.
|
42
|
+
|
43
|
+
```Ruby
|
44
|
+
class MyJob
|
45
|
+
extend Resque::Plugins::Retry
|
46
|
+
include Resque::Plugins::Stages
|
47
|
+
|
48
|
+
def perform(*args)
|
49
|
+
job = perform_job(*args)
|
50
|
+
|
51
|
+
real_perform(*job.args)
|
52
|
+
end
|
53
|
+
|
54
|
+
# The "old" perform function can be called here as-is because the
|
55
|
+
# perform_job will always ensure that the original parameters are called
|
56
|
+
def real_perform(my_param_1, my_param_2)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
To create a grouping of stages and execute the jobs, you then create a
|
62
|
+
group and the stages you need in it:
|
63
|
+
|
64
|
+
```Ruby
|
65
|
+
def enqueue_stages
|
66
|
+
Resque::Plugins::Stages::StagedGroup.within_a_grouping do |grouping|
|
67
|
+
stage = grouping.stage(1)
|
68
|
+
|
69
|
+
12.times do |index|
|
70
|
+
stage.enqueue MyJob, index, "parameter"
|
71
|
+
end
|
72
|
+
|
73
|
+
stage = grouping.stage(2)
|
74
|
+
stage.enqueue MyJob, -1, "summary"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
`within_a_grouping` will `initiate` the first stage when it completes.
|
80
|
+
This will cause all jobs in stage 1 to complete before stage 2 executes.
|
81
|
+
|
82
|
+
NOTE: All jobs and stages in a grouping are kept until the last job in
|
83
|
+
the last stage completes so that you can query the jobs to find out which
|
84
|
+
ones completed successfully and which ones failed.
|
85
|
+
|
86
|
+
To aid in querying information about the jobs in a stage, each job
|
87
|
+
includes a single string value `status_message` which can be set at any
|
88
|
+
time and will be available until all jobs are completed.
|
89
|
+
|
90
|
+
###Compatibility with other Resque Plugins
|
91
|
+
|
92
|
+
In general, this plugin should be compatible with other gems. Some special
|
93
|
+
notations on specific compatibility should be made.
|
94
|
+
|
95
|
+
[resque-compressible](https://github.com/MishaConway/resque-compressible) -
|
96
|
+
Special code has been added to ensure compatibility with this gem and
|
97
|
+
compressed jobs. The only condition for this to work properly is that the
|
98
|
+
`Resque::Plugins::Stages` gem must be included AFTER the
|
99
|
+
`Resque::Plugins::Compressible` gem is extended so that the code knows
|
100
|
+
that it needs to accomidate resque-compressible.
|
101
|
+
|
102
|
+
[resque-retry](https://github.com/lantins/resque-retry) -
|
103
|
+
Special code has been added to ensure compatibility with this gem and
|
104
|
+
Retryable jobs. When a job is retried, we know it and mark the status
|
105
|
+
of the job with a special status indicating that the job is pending being
|
106
|
+
retried. When enqueued, if the job is pending a retry, the pending job
|
107
|
+
is enqueued, not a new copy of the job. The only condition for this to
|
108
|
+
work properly is that the `Resque::Plugins::Stages` gem must be included
|
109
|
+
AFTER the `Resque::Plugins::Retry` gem is extended so that the code knows
|
110
|
+
that it needs to accomidate resque-retry.
|
111
|
+
|
112
|
+
[resque-job_history](https://github.com/RealNobody/resque-job_history) -
|
113
|
+
No special code has been added for this gem, but this gem and gems like it
|
114
|
+
(resque-cleaner, resque-history, etc.) which record the parameters for a
|
115
|
+
job and allow it to be re-enqueued all have the same/similar potential
|
116
|
+
problem of enqueueing a job with outdated job IDs because we include
|
117
|
+
the job ID in the enqueued job. If this happens and you use the
|
118
|
+
`perform_job` method as described, there will be no problem. A temporary
|
119
|
+
job with a `nil` `staged_group_stage` will be created and the params
|
120
|
+
will be available as always.
|
121
|
+
|
122
|
+
[resque](https://github.com/resque/resque) - Enqueuing jobs directly.
|
123
|
+
If you enque a job that is staged through `Resque` itself, the job will
|
124
|
+
still work as designed if you properly use the `perform_job` method as
|
125
|
+
described. It will recognize that there is no actual job and create a
|
126
|
+
temporary one with the right parameters for you to use.
|
127
|
+
|
128
|
+
###API
|
129
|
+
|
130
|
+
**StagedJob** - A job that is a part of a Stage
|
131
|
+
|
132
|
+
The Staged Job is the job which has been enqueued and using
|
133
|
+
the `perform_job` will be available to you within your `perform` method.
|
134
|
+
|
135
|
+
```Ruby
|
136
|
+
job = perform_job(*args)
|
137
|
+
|
138
|
+
job.args # The original args to the job
|
139
|
+
job.blank? # true if there is no job or stage/grouping etc.
|
140
|
+
job.staged_group_stage # The stage that the job is a part of.
|
141
|
+
job.status # The status of the job
|
142
|
+
# Valid statuses:
|
143
|
+
# :pending
|
144
|
+
# :queued
|
145
|
+
# :running
|
146
|
+
# :pending_re_run
|
147
|
+
# :failed
|
148
|
+
# :successful
|
149
|
+
job.status_message # A message that you can set on the job
|
150
|
+
job.class_name # The name of the jobs class
|
151
|
+
job.queue_time # The time that the job was initially enqueued
|
152
|
+
job.delete # Delete the job. It may remain in the Resque queue
|
153
|
+
job.enqueue_job # Enque the job (Will enqueue from the delay queue if retrying)
|
154
|
+
job.completed? # True if the job is :failed or :successful
|
155
|
+
job.queued? # True if the job has been queued to Resque
|
156
|
+
job.pending? # True if the job is :pending or :pending_re_run
|
157
|
+
|
158
|
+
# To get the group so you can see other stages:
|
159
|
+
job.staged_group_stage&.staged_group
|
160
|
+
```
|
161
|
+
|
162
|
+
**StagedGroupStage** - A Stage that is a part of a Group (contains Jobs)
|
163
|
+
|
164
|
+
```Ruby
|
165
|
+
job = perform_job(*args)
|
166
|
+
stage = job.staged_group_stage
|
167
|
+
|
168
|
+
## OR
|
169
|
+
stage = group.stage(1)
|
170
|
+
|
171
|
+
stage.enqueue # Add a job to a stage
|
172
|
+
stage.enqueue_to # If a stage is running, then
|
173
|
+
stage.enqueue_at # the job is enqueued immediately
|
174
|
+
stage.enqueue_at_with_queue # using the enqueue method specified
|
175
|
+
stage.enqueue_in # otherwise it is simply enqueued
|
176
|
+
stage.enqueue_in_with_queue # when the stage is initiated
|
177
|
+
stage.status # The status of the stage
|
178
|
+
# Valid statuses:
|
179
|
+
# :pending
|
180
|
+
# :running
|
181
|
+
# :complete
|
182
|
+
stage.number # The stages number
|
183
|
+
stage.staged_group # The group the stage belongs to
|
184
|
+
stage.jobs # The jobs for the stage in the order they
|
185
|
+
# were enqueued
|
186
|
+
stage.num_jobs # The number of jobs in the stage
|
187
|
+
stage.delete # Delete the stage
|
188
|
+
stage.initiate # Enqueue all jobs in the stage.
|
189
|
+
# Once all jobs compelte, the next stage
|
190
|
+
# will be initiated.
|
191
|
+
stage.blank? # Returns true if the stage does not really exist.
|
192
|
+
```
|
193
|
+
|
194
|
+
|
195
|
+
**StagedGroup** - A group of stages
|
196
|
+
|
197
|
+
```Ruby
|
198
|
+
Resque::Plugins::Stages::StagedGroup.within_a_grouping("description") do |group|
|
199
|
+
group.initiate # Find the next stage that is not complete and initiate it
|
200
|
+
group.description # The description that was provided for the group
|
201
|
+
group.created_at # The date/time that the group was created.
|
202
|
+
group.current_stage # The first non-complete stage.
|
203
|
+
group.stage(number) # The indicated stage. Will create a stage if none found.
|
204
|
+
group.stages # A hash of all of the stages. The stage numbers
|
205
|
+
# will be the key values.
|
206
|
+
group.delete # Delete the group and all stages and jobs.
|
207
|
+
group.blank? # Returns true if the group is not saved
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
**Cleaner** - A cleaner utility class for fixing up mixed up jobs
|
212
|
+
|
213
|
+
```Ruby
|
214
|
+
Resque::Plugins::Stages::Cleaner.purge_all # delete all values from Redis
|
215
|
+
Resque::Plugins::Stages::Cleaner.cleanup_jobs # Create any stages or groups
|
216
|
+
# needed for orphaned jobs.
|
217
|
+
```
|
218
|
+
|
219
|
+
## Screenshots
|
220
|
+
|
221
|
+
### Groups
|
222
|
+

|
223
|
+
|
224
|
+
### Stages
|
225
|
+

|
226
|
+
|
227
|
+
### Jobs
|
228
|
+

|
229
|
+
|
230
|
+
### Job
|
231
|
+

|
232
|
+
|
233
|
+
## Development
|
234
|
+
|
235
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
236
|
+
|
237
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
238
|
+
|
239
|
+
## Contributing
|
240
|
+
|
241
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/resque-stages. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/resque-stages/blob/master/CODE_OF_CONDUCT.md).
|
242
|
+
|
243
|
+
|
244
|
+
## License
|
245
|
+
|
246
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
247
|
+
|
248
|
+
## Code of Conduct
|
249
|
+
|
250
|
+
Everyone interacting in the Resque::Stages project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/resque-stages/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#!/usr/bin/env ruby
|
4
|
+
|
5
|
+
require "bundler/setup"
|
6
|
+
require "resque/resque-stages"
|
7
|
+
|
8
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
9
|
+
# with your gem easier. You can also use a different console, if you like.
|
10
|
+
|
11
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
12
|
+
# require "pry"
|
13
|
+
# Pry.start
|
14
|
+
|
15
|
+
require "irb"
|
16
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "resque"
|
4
|
+
require File.expand_path(File.join("resque", "plugins", "stages", "redis_access"), File.dirname(__FILE__))
|
5
|
+
require File.expand_path(File.join("resque", "plugins", "stages", "staged_group_list"), File.dirname(__FILE__))
|
6
|
+
require File.expand_path(File.join("resque", "plugins", "stages", "staged_group"), File.dirname(__FILE__))
|
7
|
+
require File.expand_path(File.join("resque", "plugins", "stages", "staged_group_stage"), File.dirname(__FILE__))
|
8
|
+
require File.expand_path(File.join("resque", "plugins", "stages", "staged_job"), File.dirname(__FILE__))
|
9
|
+
require File.expand_path(File.join("resque", "plugins", "stages", "cleaner"), File.dirname(__FILE__))
|
10
|
+
|
11
|
+
require File.expand_path(File.join("resque", "plugins", "stages"), File.dirname(__FILE__))
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
# This module is added to any job class which needs to work within a stage.
|
6
|
+
#
|
7
|
+
# If the job is going to be retryable, this module needs to be included after
|
8
|
+
# the retry module is extended so that we know that the class is retryable.
|
9
|
+
module Stages
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
included do
|
15
|
+
if singleton_class.included_modules.map(&:name).include?("Resque::Plugins::Retry")
|
16
|
+
try_again_callback :stages_report_try_again
|
17
|
+
give_up_callback :stages_report_giving_up
|
18
|
+
else
|
19
|
+
add_record_failure
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# rubocop:disable Metrics/BlockLength
|
24
|
+
class_methods do
|
25
|
+
def perform_job(*args)
|
26
|
+
job = perform_job_from_param(args)
|
27
|
+
|
28
|
+
if job.nil?
|
29
|
+
job = Resque::Plugins::Stages::StagedJob.new(SecureRandom.uuid)
|
30
|
+
job.class_name = name
|
31
|
+
job.args = args
|
32
|
+
end
|
33
|
+
|
34
|
+
job
|
35
|
+
end
|
36
|
+
|
37
|
+
# rubocop:disable Metrics/AbcSize
|
38
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
39
|
+
def perform_job_from_param(args)
|
40
|
+
return if args.blank? || !args.first.is_a?(Hash)
|
41
|
+
|
42
|
+
hash = args.first.with_indifferent_access
|
43
|
+
job = Resque::Plugins::Stages::StagedJob.new(hash[:staged_job_id]) if hash.key?(:staged_job_id)
|
44
|
+
job&.class_name = name
|
45
|
+
job.args = (hash.key?(:resque_compressed) ? args : args[1..]) if !job.nil? && job.blank?
|
46
|
+
|
47
|
+
job
|
48
|
+
end
|
49
|
+
|
50
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
51
|
+
# rubocop:enable Metrics/AbcSize
|
52
|
+
|
53
|
+
def before_perform_stages_successful(*args)
|
54
|
+
job = perform_job(*args)
|
55
|
+
|
56
|
+
return if job.blank?
|
57
|
+
|
58
|
+
job.status = :running
|
59
|
+
end
|
60
|
+
|
61
|
+
def after_perform_stages_successful(*args)
|
62
|
+
job = perform_job(*args)
|
63
|
+
|
64
|
+
return if job.blank?
|
65
|
+
return if job.status == :failed && Resque.inline?
|
66
|
+
|
67
|
+
job.status = :successful
|
68
|
+
end
|
69
|
+
|
70
|
+
def around_perform_stages_inline_around(*args)
|
71
|
+
yield
|
72
|
+
rescue StandardError => e
|
73
|
+
raise e unless Resque.inline?
|
74
|
+
|
75
|
+
job = perform_job(*args)
|
76
|
+
return if job.blank?
|
77
|
+
|
78
|
+
job.status = :failed
|
79
|
+
end
|
80
|
+
|
81
|
+
def stages_report_try_again(_exception, *args)
|
82
|
+
job = perform_job(*args)
|
83
|
+
|
84
|
+
return if job.blank?
|
85
|
+
|
86
|
+
job.status = :pending_re_run
|
87
|
+
end
|
88
|
+
|
89
|
+
def stages_report_giving_up(_exception, *args)
|
90
|
+
job = perform_job(*args)
|
91
|
+
|
92
|
+
return if job.blank?
|
93
|
+
|
94
|
+
job.status = :failed
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_record_failure
|
98
|
+
define_singleton_method(:on_failure_stages_failed) do |_error, *args|
|
99
|
+
job = perform_job(*args)
|
100
|
+
|
101
|
+
return if job.blank?
|
102
|
+
|
103
|
+
job.status = :failed
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
# rubocop:enable Metrics/BlockLength
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Stages
|
6
|
+
# A class for cleaning up stranded objects for the Stages plugin
|
7
|
+
class Cleaner
|
8
|
+
include Resque::Plugins::Stages::RedisAccess
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def redis
|
12
|
+
@redis ||= Resque::Plugins::Stages::Cleaner.new.redis
|
13
|
+
end
|
14
|
+
|
15
|
+
def purge_all
|
16
|
+
keys = redis.keys("*")
|
17
|
+
|
18
|
+
return if keys.blank?
|
19
|
+
|
20
|
+
redis.del(*keys)
|
21
|
+
end
|
22
|
+
|
23
|
+
def cleanup_jobs
|
24
|
+
jobs = redis.keys("StagedJob::*")
|
25
|
+
|
26
|
+
jobs.each do |job_key|
|
27
|
+
job = Resque::Plugins::Stages::StagedJob.new(job_key[11..])
|
28
|
+
|
29
|
+
job.verify
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Stages
|
6
|
+
# A module to add a `redis` method for a class in this gem that needs redis to get a reids object that is namespaced.
|
7
|
+
module RedisAccess
|
8
|
+
NAME_SPACE = "Resque::Plugins::Stages::"
|
9
|
+
|
10
|
+
def redis
|
11
|
+
@redis ||= Redis::Namespace.new(Resque::Plugins::Stages::RedisAccess::NAME_SPACE, redis: Resque.redis)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|