resque-multi-step 1.0.0
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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +72 -0
- data/VERSION +1 -0
- data/lib/resque/plugins/multi_step_task/assure_finalization.rb +15 -0
- data/lib/resque/plugins/multi_step_task/atomic_counters.rb +21 -0
- data/lib/resque/plugins/multi_step_task/constantization.rb +20 -0
- data/lib/resque/plugins/multi_step_task/finalization_job.rb +38 -0
- data/lib/resque/plugins/multi_step_task.rb +268 -0
- data/lib/resque-multi-step.rb +2 -0
- data/lib/resque_mutli_step.rb +1 -0
- data/resque-multi-step-task.gemspec +67 -0
- data/resque-multi-step.gemspec +81 -0
- data/spec/acceptance/acceptance_jobs.rb +31 -0
- data/spec/acceptance/job_handling_spec.rb +121 -0
- data/spec/acceptance/spec_helper.rb +13 -0
- data/spec/resque/plugins/multi_step_task/finalization_job_spec.rb +34 -0
- data/spec/resque/plugins/multi_step_task_spec.rb +235 -0
- data/spec/resque-multi-step_spec.rb +7 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- metadata +145 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Peter Williams
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
resque-multi-step
|
2
|
+
======
|
3
|
+
|
4
|
+
Resque multi-step provides an abstraction for managing multiple step
|
5
|
+
async tasks.
|
6
|
+
|
7
|
+
Status
|
8
|
+
----
|
9
|
+
|
10
|
+
This software is not considered stable at this time. Use at your own risk.
|
11
|
+
|
12
|
+
Using multi-step tasks
|
13
|
+
----
|
14
|
+
|
15
|
+
Consider a situation where you need to perform several actions and
|
16
|
+
would like those actions to run in parallel and you would like to keep
|
17
|
+
track of the progress. Mutli-step can be use to implement that as
|
18
|
+
follows:
|
19
|
+
|
20
|
+
task = Resque::Plugins::MultiStepTask.create("pirate-take-over") do |task|
|
21
|
+
blog.posts.each do |post|
|
22
|
+
task.add_job ConvertPostToPirateTalk, post.id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
A resque job will be queued for each post. The `task` object will
|
27
|
+
keep track of how many of the tasks have been completed successfully
|
28
|
+
(`#completed_count`). That combined with the overall job count
|
29
|
+
(`#total_job_count`) make it easy to compute the percentage completion
|
30
|
+
of a mutli-step task.
|
31
|
+
|
32
|
+
The failed job count (`#failed_count`) makes it easy to determine if
|
33
|
+
problem has occurred during the execution.
|
34
|
+
|
35
|
+
Looking up existing tasks
|
36
|
+
----
|
37
|
+
|
38
|
+
Once you have kicked off a job you can look it up again later using
|
39
|
+
it's task id. First you persist the task id when you create the task.
|
40
|
+
|
41
|
+
task = Resque::Plugins::MultiStepTask.create('pirate-take-over") do |task|
|
42
|
+
...
|
43
|
+
end
|
44
|
+
blog.async_task_id = task.task_id
|
45
|
+
blog.save!
|
46
|
+
|
47
|
+
Then you can look it up using the `.find` method on `MultiStepTask`.
|
48
|
+
|
49
|
+
# Progress reporting action; executed in a different process.
|
50
|
+
begin
|
51
|
+
task = Resque::Plugins::MultiStepTask.find(blog.async_task_id)
|
52
|
+
render :text => "percent complete #{(task.completed_count.quo(task.total_job_count) * 100).round}%
|
53
|
+
|
54
|
+
rescue Resque::Plugins::MultiStepTask::NoSuchMultiStepTask
|
55
|
+
# task completed...
|
56
|
+
|
57
|
+
redirect_to blog_url(blog)
|
58
|
+
end
|
59
|
+
|
60
|
+
Finalization
|
61
|
+
----
|
62
|
+
|
63
|
+
Often when doing mutli-step tasks there are a bunch of tasks that can
|
64
|
+
all happen in parallel and then a few that can only be executed after
|
65
|
+
all the rest have completed. Mutli-step task finalization supports
|
66
|
+
just that use case.
|
67
|
+
|
68
|
+
Using our example, say we want to commit the solr index and then
|
69
|
+
unlock the blog we are converting to pirate talk once the conversion
|
70
|
+
is complete.
|
71
|
+
|
72
|
+
task = Resque::Plugins::MultiStepTask.create("pirate-take-over") do |task|
|
73
|
+
blog.posts.each do |post|
|
74
|
+
task.add_job ConvertPostToPirateTalk, post.id
|
75
|
+
end
|
76
|
+
|
77
|
+
task.add_finalization_job CommitSolr
|
78
|
+
task.add_finalization_job UnlockBlog, blog.id
|
79
|
+
end
|
80
|
+
|
81
|
+
This would convert all the posts to pirate talk in parallel, using as
|
82
|
+
many workers as are available. Once all the normal jobs are completed
|
83
|
+
the finalization jobs are run serially in a single worker.
|
84
|
+
Finalization are executed in the order in which they are registered.
|
85
|
+
In our example, solr will be committed and then, after the commit is
|
86
|
+
complete, the blog will be unlocked.
|
87
|
+
|
88
|
+
Details
|
89
|
+
----
|
90
|
+
|
91
|
+
MultiStepTask creates a queue in resque for each task. To process
|
92
|
+
multi-step jobs you will need at least one Resque worker with
|
93
|
+
`QUEUES=*`. This combined with [resque-fairly][] provides fair
|
94
|
+
scheduling of the constituent jobs.
|
95
|
+
|
96
|
+
Having a queue per multi-step task means that is easy to determine to
|
97
|
+
what task a particular job belongs. It also provides a nice way to see
|
98
|
+
what is going on in the system at any given time. Just got to
|
99
|
+
resque-web and look the queue list. Use meaningful slugs for your
|
100
|
+
tasks and you get a quick birds-eye view of what is going on.
|
101
|
+
|
102
|
+
Note on Patches/Pull Requests
|
103
|
+
----
|
104
|
+
|
105
|
+
* Fork the project.
|
106
|
+
* Make your feature addition or bug fix.
|
107
|
+
* Add tests for it. This is important so I don't break it in a
|
108
|
+
future version unintentionally.
|
109
|
+
* Update history to reflect the change.
|
110
|
+
* Commit, do not mess with rakefile, version.
|
111
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
112
|
+
* Send me a pull request. Bonus points for topic branches.
|
113
|
+
|
114
|
+
Copyright
|
115
|
+
-----
|
116
|
+
|
117
|
+
Copyright (c) OpenLogic, Peter Williams. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "resque-multi-step"
|
8
|
+
gem.summary = "Provides multi-step tasks with finalization and progress tracking"
|
9
|
+
gem.description = "Provides multi-step tasks with finalization and progress tracking"
|
10
|
+
gem.email = "pezra@barelyenough.org"
|
11
|
+
gem.homepage = "http://github.com/pezra/resque-multi-step"
|
12
|
+
gem.authors = ["Peter Williams", "Morgan Whitney"]
|
13
|
+
|
14
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
|
+
|
16
|
+
gem.add_dependency 'redis-namespace', '~> 0.8.0'
|
17
|
+
gem.add_dependency 'resque', '~> 1.10'
|
18
|
+
gem.add_dependency 'resque-fairly', '~> 1.0'
|
19
|
+
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'spec/rake/spectask'
|
28
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
34
|
+
spec.libs << 'lib' << 'spec'
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
namespace(:spec) do
|
40
|
+
Spec::Rake::SpecTask.new(:acceptance) do |spec|
|
41
|
+
spec.libs << 'lib' << 'spec'
|
42
|
+
spec.spec_files = FileList['spec/acceptance/*_spec.rb']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
task :spec => :check_dependencies
|
47
|
+
|
48
|
+
task :default => :spec
|
49
|
+
|
50
|
+
require 'rake/rdoctask'
|
51
|
+
Rake::RDocTask.new do |rdoc|
|
52
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
53
|
+
|
54
|
+
rdoc.rdoc_dir = 'rdoc'
|
55
|
+
rdoc.title = "resque-multi-step #{version}"
|
56
|
+
rdoc.rdoc_files.include('README*')
|
57
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
end
|
59
|
+
|
60
|
+
# Setup for acceptance testing
|
61
|
+
require 'rubygems'
|
62
|
+
require 'resque/tasks'
|
63
|
+
require 'resque-fairly'
|
64
|
+
|
65
|
+
Resque.redis.namespace = ENV['NAMESPACE'] if ENV['NAMESPACE']
|
66
|
+
|
67
|
+
$LOAD_PATH << File.expand_path("lib", File.dirname(__FILE__))
|
68
|
+
require 'resque-multi-step'
|
69
|
+
|
70
|
+
$LOAD_PATH << File.expand_path("spec/acceptance", File.dirname(__FILE__))
|
71
|
+
require 'acceptance_jobs'
|
72
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
class MultiStepTask
|
4
|
+
# in the case that all normal jobs have completed before the job group
|
5
|
+
# is finalized, the job group will never receive the hook to enter
|
6
|
+
# finalizataion. To avoid this, an AssureFinalization job will be added
|
7
|
+
# to the queue for the sole purposed of initiating finalization for certain.
|
8
|
+
class AssureFinalization
|
9
|
+
def self.perform(task_id)
|
10
|
+
MultiStepTask.find(task_id).maybe_finalize
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
class MultiStepTask
|
4
|
+
module AtomicCounters
|
5
|
+
def counter(name)
|
6
|
+
class_eval <<-INCR
|
7
|
+
def increment_#{name}
|
8
|
+
redis.incrby('#{name}', 1)
|
9
|
+
end
|
10
|
+
INCR
|
11
|
+
|
12
|
+
class_eval <<-GETTER
|
13
|
+
def #{name}
|
14
|
+
redis.get('#{name}').to_i
|
15
|
+
end
|
16
|
+
GETTER
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
class MultiStepTask
|
4
|
+
module Constantization
|
5
|
+
# Courtesy ActiveSupport (Ruby on Rails)
|
6
|
+
def constantize(camel_cased_word)
|
7
|
+
names = camel_cased_word.split('::')
|
8
|
+
names.shift if names.empty? || names.first.empty?
|
9
|
+
|
10
|
+
constant = Object
|
11
|
+
names.each do |name|
|
12
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
13
|
+
end
|
14
|
+
constant
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'resque/plugins/multi_step_task/constantization'
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
class MultiStepTask
|
6
|
+
# Executes a single finalization job
|
7
|
+
class FinalizationJob
|
8
|
+
extend Constantization
|
9
|
+
|
10
|
+
# Handle job invocation
|
11
|
+
def self.perform(task_id, job_module_name, *args)
|
12
|
+
# puts "finalizationjob#perform(#{task_id.inspect}, #{job_module_name.inspect}, #{args.inspect})"
|
13
|
+
task = MultiStepTask.find(task_id)
|
14
|
+
|
15
|
+
begin
|
16
|
+
constantize(job_module_name).perform(*args)
|
17
|
+
rescue Exception
|
18
|
+
task.increment_failed_count
|
19
|
+
raise
|
20
|
+
end
|
21
|
+
|
22
|
+
task.increment_completed_count
|
23
|
+
|
24
|
+
if fin_job_info = task.redis.lpop('finalize_jobs')
|
25
|
+
# Queue the next finalization job
|
26
|
+
Resque::Job.create(task.queue_name, FinalizationJob, task.task_id,
|
27
|
+
*Yajl::Parser.parse(fin_job_info))
|
28
|
+
else
|
29
|
+
# There is nothing left to do so cleanup.
|
30
|
+
task.nuke
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'resque'
|
2
|
+
require 'redis-namespace'
|
3
|
+
require 'resque/plugins/multi_step_task/assure_finalization'
|
4
|
+
require 'resque/plugins/multi_step_task/finalization_job'
|
5
|
+
require 'resque/plugins/multi_step_task/constantization'
|
6
|
+
require 'resque/plugins/multi_step_task/atomic_counters'
|
7
|
+
|
8
|
+
module Resque
|
9
|
+
module Plugins
|
10
|
+
class MultiStepTask
|
11
|
+
class NoSuchMultiStepTask < StandardError; end
|
12
|
+
class NotReadyForFinalization < StandardError; end
|
13
|
+
class FinalizationAlreadyBegun < StandardError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
include Constantization
|
17
|
+
|
18
|
+
NONCE_CHARS = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
|
19
|
+
|
20
|
+
# A bit of randomness to ensure tasks are uniquely identified.
|
21
|
+
def nonce
|
22
|
+
nonce = ""
|
23
|
+
5.times{nonce << NONCE_CHARS[rand(NONCE_CHARS.length)]}
|
24
|
+
nonce
|
25
|
+
end
|
26
|
+
|
27
|
+
# A redis client suitable for storing global mutli-step task info.
|
28
|
+
def redis
|
29
|
+
@redis ||= Redis::Namespace.new("resque:multisteptask", :redis => Resque.redis)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Does a task with the specified id exist?
|
33
|
+
def active?(task_id)
|
34
|
+
redis.sismember("active-tasks", task_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a brand new parallel job group.
|
38
|
+
#
|
39
|
+
# @param [#to_s] slug The descriptive slug of the new job. Default: a
|
40
|
+
# random UUID
|
41
|
+
#
|
42
|
+
# @yield [multi_step_task] A block to define the work to take place in parallel
|
43
|
+
#
|
44
|
+
# @yieldparam [MultiStepTask] The newly create job group.
|
45
|
+
#
|
46
|
+
# @return [MultiStepTask] The new job group
|
47
|
+
def create(slug=nil)
|
48
|
+
task_id = if slug.nil? || slug.empty?
|
49
|
+
"multi-step-task"
|
50
|
+
else
|
51
|
+
slug.to_s
|
52
|
+
end
|
53
|
+
task_id << "~" << nonce
|
54
|
+
|
55
|
+
pjg = new(task_id)
|
56
|
+
pjg.nuke
|
57
|
+
redis.sadd("active-tasks", task_id)
|
58
|
+
redis.sismember("active-tasks", task_id)
|
59
|
+
if block_given?
|
60
|
+
yield pjg
|
61
|
+
pjg.finalizable!
|
62
|
+
end
|
63
|
+
|
64
|
+
pjg
|
65
|
+
end
|
66
|
+
|
67
|
+
# Prevent calling MultiStepTask.new
|
68
|
+
private :new
|
69
|
+
|
70
|
+
# Find an existing MultiStepTask.
|
71
|
+
#
|
72
|
+
# @param [#to_s] task_id The unique key for the job group of interest.
|
73
|
+
#
|
74
|
+
# @return [ParallelJobGroup] The group of interest
|
75
|
+
#
|
76
|
+
# @raise [NoSuchMultiStepTask] If there is not a group with the specified key.
|
77
|
+
def find(task_id)
|
78
|
+
raise NoSuchMultiStepTask unless active?(task_id)
|
79
|
+
|
80
|
+
pjg = new(task_id)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Handle job invocation
|
84
|
+
def perform(task_id, job_module_name, *args)
|
85
|
+
task = MultiStepTask.find(task_id)
|
86
|
+
begin
|
87
|
+
constantize(job_module_name).perform(*args)
|
88
|
+
rescue Exception => e
|
89
|
+
task.increment_failed_count
|
90
|
+
raise
|
91
|
+
end
|
92
|
+
|
93
|
+
task.increment_completed_count
|
94
|
+
task.maybe_finalize
|
95
|
+
end
|
96
|
+
|
97
|
+
# Normally jobs that are part of a multi-step task are run
|
98
|
+
# asynchronously by putting them on a queue. However, it is
|
99
|
+
# often more convenient to just run the jobs synchronously as
|
100
|
+
# they are registered in a development environment. Setting
|
101
|
+
# mode to `:sync` provides a way to do just that.
|
102
|
+
#
|
103
|
+
# @param [:sync,:async] sync_or_async
|
104
|
+
def mode=(sync_or_async)
|
105
|
+
@@synchronous = (sync_or_async == :sync)
|
106
|
+
end
|
107
|
+
|
108
|
+
def synchronous?
|
109
|
+
@@synchronous
|
110
|
+
end
|
111
|
+
@@synchronous = false
|
112
|
+
end
|
113
|
+
|
114
|
+
def synchronous?
|
115
|
+
@@synchronous
|
116
|
+
end
|
117
|
+
|
118
|
+
# Instance methods
|
119
|
+
|
120
|
+
include Constantization
|
121
|
+
|
122
|
+
attr_reader :task_id
|
123
|
+
|
124
|
+
extend AtomicCounters
|
125
|
+
|
126
|
+
counter :normal_job_count
|
127
|
+
counter :finalize_job_count
|
128
|
+
|
129
|
+
counter :completed_count
|
130
|
+
counter :failed_count
|
131
|
+
|
132
|
+
|
133
|
+
# Initialize a newly instantiated parallel job group.
|
134
|
+
#
|
135
|
+
# @param [String] task_id The UUID of the group of interest.
|
136
|
+
def initialize(task_id)
|
137
|
+
@task_id = task_id
|
138
|
+
end
|
139
|
+
|
140
|
+
def redis
|
141
|
+
@redis ||= Redis::Namespace.new("resque:multisteptask:#{task_id}", :redis => Resque.redis)
|
142
|
+
end
|
143
|
+
|
144
|
+
# The total number of jobs that are part of this task.
|
145
|
+
def total_job_count
|
146
|
+
normal_job_count + finalize_job_count
|
147
|
+
end
|
148
|
+
|
149
|
+
# Removes all data from redis related to this task.
|
150
|
+
def nuke
|
151
|
+
redis.keys('*').each{|k| redis.del k}
|
152
|
+
Resque.remove_queue queue_name
|
153
|
+
self.class.redis.srem('active-tasks', task_id)
|
154
|
+
end
|
155
|
+
|
156
|
+
# The name of the queue for jobs what are part of this task.
|
157
|
+
def queue_name
|
158
|
+
task_id
|
159
|
+
end
|
160
|
+
|
161
|
+
# Add a job to this task
|
162
|
+
#
|
163
|
+
# @param [Class,Module] job_type The type of the job to be performed.
|
164
|
+
def add_job(job_type, *args)
|
165
|
+
increment_normal_job_count
|
166
|
+
|
167
|
+
if synchronous?
|
168
|
+
self.class.perform(task_id, job_type.to_s, *args)
|
169
|
+
else
|
170
|
+
Resque::Job.create(queue_name, self.class, task_id, job_type.to_s, *args)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Finalization jobs are performed after all the normal jobs
|
175
|
+
# (i.e. the ones registered with #add_job) have been completed.
|
176
|
+
# Finalization jobs are performed in the order they are defined.
|
177
|
+
#
|
178
|
+
# @param [Class,Module] job_type The type of job to be performed.
|
179
|
+
def add_finalization_job(job_type, *args)
|
180
|
+
increment_finalize_job_count
|
181
|
+
|
182
|
+
redis.rpush 'finalize_jobs', Yajl::Encoder.encode([job_type.to_s, *args])
|
183
|
+
end
|
184
|
+
|
185
|
+
# A multi-step task is finalizable when all the normal jobs (see
|
186
|
+
# #add_job) have been registered. Finalization jobs will not be
|
187
|
+
# executed until the task becomes finalizable regardless of the
|
188
|
+
# number of jobs that have been completed.
|
189
|
+
def finalizable?
|
190
|
+
redis.exists 'is_finalizable'
|
191
|
+
end
|
192
|
+
|
193
|
+
# Make this multi-step task finalizable (see #finalizable?).
|
194
|
+
def finalizable!
|
195
|
+
redis.set 'is_finalizable', true
|
196
|
+
if synchronous?
|
197
|
+
maybe_finalize
|
198
|
+
else
|
199
|
+
Resque::Job.create(queue_name, AssureFinalization, self.task_id)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Finalize this job group. Finalization entails running all
|
204
|
+
# finalization jobs serially in the order they were defined.
|
205
|
+
#
|
206
|
+
# @raise [NotReadyForFinalization] When called before all normal
|
207
|
+
# jobs have been attempted.
|
208
|
+
#
|
209
|
+
# @raise [FinalizationAlreadyBegun] If some other process has
|
210
|
+
# already started (and/or finished) the finalization process.
|
211
|
+
def finalize!
|
212
|
+
raise FinalizationAlreadyBegun unless MultiStepTask.active?(task_id)
|
213
|
+
raise NotReadyForFinalization if !ready_for_finalization? || incomplete_because_of_errors?
|
214
|
+
|
215
|
+
# Only one process is allowed to start the finalization
|
216
|
+
# process. This setnx acts a global mutex for other processes
|
217
|
+
# that finish about the same time.
|
218
|
+
raise FinalizationAlreadyBegun unless redis.setnx("i_am_the_finalizer", 1)
|
219
|
+
|
220
|
+
if synchronous?
|
221
|
+
sync_finalize!
|
222
|
+
|
223
|
+
else
|
224
|
+
if fin_job_info = redis.lpop('finalize_jobs')
|
225
|
+
fin_job_info = Yajl::Parser.parse(fin_job_info)
|
226
|
+
Resque::Job.create(queue_name, FinalizationJob, self.task_id, *fin_job_info)
|
227
|
+
else
|
228
|
+
# There is nothing left to do so cleanup.
|
229
|
+
nuke
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def sync_finalize!
|
235
|
+
while fin_job_info = redis.lpop('finalize_jobs')
|
236
|
+
job_class_name, *args = Yajl::Parser.parse(fin_job_info)
|
237
|
+
self.class.perform(task_id, job_class_name, *args)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Execute finalization sequence if it is time.
|
242
|
+
def maybe_finalize
|
243
|
+
return unless ready_for_finalization?
|
244
|
+
finalize!
|
245
|
+
rescue Exception
|
246
|
+
# just eat the exception
|
247
|
+
end
|
248
|
+
|
249
|
+
# Is this task at the point where finalization can occur.
|
250
|
+
def ready_for_finalization?
|
251
|
+
finalizable? && completed_count >= normal_job_count
|
252
|
+
end
|
253
|
+
|
254
|
+
# If a normal or finalization job fails (i.e. raises an
|
255
|
+
# exception) the task as a whole is considered to be incomplete.
|
256
|
+
# The finalization sequence will not be performed. If the
|
257
|
+
# failure occurred during finalization any remaining
|
258
|
+
# finalization job will not be run.
|
259
|
+
#
|
260
|
+
# If the failed job is retried and succeeds finalization will
|
261
|
+
# proceed at usual.
|
262
|
+
def incomplete_because_of_errors?
|
263
|
+
failed_count > 0 && completed_count < normal_job_count
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'resque-multi-step'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{resque-multi-step-task}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Peter Williams"]
|
12
|
+
s.date = %q{2010-09-20}
|
13
|
+
s.description = %q{Provides multi-step tasks with finalization and progress tracking}
|
14
|
+
s.email = %q{pezra@barelyenough.org}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/resque-multi-step-task.rb",
|
27
|
+
"lib/resque/plugins/multi_step_task.rb",
|
28
|
+
"lib/resque/plugins/multi_step_task/assure_finalization.rb",
|
29
|
+
"lib/resque/plugins/multi_step_task/atomic_counters.rb",
|
30
|
+
"lib/resque/plugins/multi_step_task/constantization.rb",
|
31
|
+
"lib/resque_mutli_step_task.rb",
|
32
|
+
"spec/resque-multi-step-task_spec.rb",
|
33
|
+
"spec/resque/plugins/multi_step_task_spec.rb",
|
34
|
+
"spec/spec.opts",
|
35
|
+
"spec/spec_helper.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/pezra/resque-multi-step-task}
|
38
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.6}
|
41
|
+
s.summary = %q{Provides multi-step tasks with finalization and progress tracking}
|
42
|
+
s.test_files = [
|
43
|
+
"spec/resque/plugins/multi_step_task_spec.rb",
|
44
|
+
"spec/resque-multi-step-task_spec.rb",
|
45
|
+
"spec/spec_helper.rb"
|
46
|
+
]
|
47
|
+
|
48
|
+
if s.respond_to? :specification_version then
|
49
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
50
|
+
s.specification_version = 3
|
51
|
+
|
52
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
53
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
54
|
+
s.add_runtime_dependency(%q<resque>, ["~> 1.10"])
|
55
|
+
s.add_runtime_dependency(%q<redis-namespace>, ["~> 0.8.0"])
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
58
|
+
s.add_dependency(%q<resque>, ["~> 1.10"])
|
59
|
+
s.add_dependency(%q<redis-namespace>, ["~> 0.8.0"])
|
60
|
+
end
|
61
|
+
else
|
62
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
63
|
+
s.add_dependency(%q<resque>, ["~> 1.10"])
|
64
|
+
s.add_dependency(%q<redis-namespace>, ["~> 0.8.0"])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|