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,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
+ ![Pending Job Details](https://raw.githubusercontent.com/RealNobody/resque-stages/master/read_me/groups_list.png)
223
+
224
+ ### Stages
225
+ ![Pending Job Details](https://raw.githubusercontent.com/RealNobody/resque-stages/master/read_me/stages.png)
226
+
227
+ ### Jobs
228
+ ![Pending Job Details](https://raw.githubusercontent.com/RealNobody/resque-stages/master/read_me/stage.png)
229
+
230
+ ### Job
231
+ ![Pending Job Details](https://raw.githubusercontent.com/RealNobody/resque-stages/master/read_me/job.png)
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).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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