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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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,2 @@
1
+ require 'rubygems'
2
+ require 'resque/plugins/multi_step_task'
@@ -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
+