resque-multi-step 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|