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,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