pipeline 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ module Pipeline
2
+ class Base < ActiveRecord::Base
3
+ set_table_name :pipeline_instances
4
+
5
+ # :not_started ---> :in_progress ---> :completed
6
+ # ^ | \-> :failed
7
+ # | v
8
+ # :paused
9
+ symbol_attr :status
10
+ transactional_attr :status
11
+ private :status=
12
+
13
+ has_many :stages, :class_name => 'Pipeline::Stage::Base', :foreign_key => 'pipeline_instance_id', :dependent => :destroy
14
+
15
+ class_inheritable_accessor :defined_stages, :instance_writer => false
16
+ self.defined_stages = []
17
+
18
+ def self.define_stages(stages)
19
+ self.defined_stages = stages.build_chain
20
+ end
21
+
22
+ def after_initialize
23
+ self[:status] = :not_started if new_record?
24
+ if new_record?
25
+ self.class.defined_stages.each do |stage_class|
26
+ stages << stage_class.new(:pipeline => self)
27
+ end
28
+ end
29
+ end
30
+
31
+ def perform
32
+ raise InvalidStatusError.new(status) unless ok_to_resume?
33
+ begin
34
+ _setup
35
+ stages.each do |stage|
36
+ stage.perform unless stage.completed?
37
+ end
38
+ self.status = :completed
39
+ rescue IrrecoverableError
40
+ self.status = :failed
41
+ rescue RecoverableError => e
42
+ if e.input_required?
43
+ self.status = :paused
44
+ else
45
+ raise e
46
+ end
47
+ rescue
48
+ self.status = :paused
49
+ end
50
+ end
51
+
52
+ def cancel
53
+ raise InvalidStatusError.new(status) unless ok_to_resume?
54
+ self.status = :failed
55
+ end
56
+
57
+ def ok_to_resume?
58
+ [:not_started, :paused].include?(status)
59
+ end
60
+
61
+ private
62
+ def _setup
63
+ self.attempts += 1
64
+ self.status = :in_progress
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ module Pipeline
2
+ module SymbolAttribute
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def symbol_attrs(*attributes)
9
+ attributes.each do |attribute|
10
+ class_eval <<-EOD
11
+ def #{attribute.to_s}
12
+ read_attribute('#{attribute.to_s}').to_sym rescue nil
13
+ end
14
+ EOD
15
+ end
16
+ end
17
+
18
+ alias_method :symbol_attr, :symbol_attrs
19
+ end
20
+ end
21
+ end
22
+
23
+ class Symbol
24
+ def quoted_id
25
+ "'#{ActiveRecord::Base.connection.quote_string(self.to_s)}'"
26
+ end
27
+ end
28
+
29
+ ActiveRecord::Base.send(:include, Pipeline::SymbolAttribute)
@@ -0,0 +1,26 @@
1
+ module Pipeline
2
+ module TransactionalAttribute
3
+ def self.included (base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def transactional_attrs(*attributes)
9
+ attributes.each do |attribute|
10
+ class_eval <<-EOD
11
+ def #{attribute.to_s}=(value)
12
+ ActiveRecord::Base.transaction(:requires_new => true) do
13
+ write_attribute('#{attribute.to_s}', value)
14
+ save!
15
+ end
16
+ end
17
+ EOD
18
+ end
19
+ end
20
+
21
+ alias_method :transactional_attr, :transactional_attrs
22
+ end
23
+ end
24
+ end
25
+
26
+ ActiveRecord::Base.send(:include, Pipeline::TransactionalAttribute)
@@ -0,0 +1,24 @@
1
+ module Pipeline
2
+ class InvalidPipelineError < StandardError; end
3
+
4
+ class InvalidStatusError < StandardError
5
+ def initialize(status)
6
+ super("Status is already #{status.to_s.gsub(/_/, ' ')}")
7
+ end
8
+ end
9
+
10
+ class IrrecoverableError < StandardError; end
11
+
12
+ class RecoverableError < StandardError
13
+ def initialize(msg = nil, input_required = false)
14
+ super(msg)
15
+ @input_required = input_required
16
+ end
17
+
18
+ def input_required?
19
+ @input_required
20
+ end
21
+ end
22
+
23
+ extend(ApiMethods)
24
+ end
@@ -0,0 +1,60 @@
1
+ module Pipeline
2
+ module Stage
3
+ class Base < ActiveRecord::Base
4
+ set_table_name :pipeline_stages
5
+
6
+ # :not_started ---> :in_progress ---> :completed
7
+ # ^ |
8
+ # | v
9
+ # :failed
10
+ symbol_attr :status
11
+ transactional_attr :status
12
+ private :status=
13
+
14
+ belongs_to :pipeline, :class_name => "Pipeline::Base"
15
+
16
+ @@chain = []
17
+ def self.>>(next_stage)
18
+ @@chain << self
19
+ next_stage
20
+ end
21
+
22
+ def self.build_chain
23
+ chain = @@chain + [self]
24
+ @@chain = []
25
+ chain
26
+ end
27
+
28
+ def after_initialize
29
+ self.name ||= self.class.to_s
30
+ self[:status] = :not_started if new_record?
31
+ end
32
+
33
+ def completed?
34
+ status == :completed
35
+ end
36
+
37
+ def perform
38
+ raise InvalidStatusError.new(status) unless [:not_started, :failed].include?(status)
39
+ begin
40
+ _setup
41
+ run
42
+ self.status = :completed
43
+ rescue => e
44
+ self.message = e.message
45
+ self.status = :failed
46
+ raise e
47
+ end
48
+ end
49
+
50
+ # Subclass must implement this as part of the contract
51
+ def run; end
52
+
53
+ private
54
+ def _setup
55
+ self.attempts += 1
56
+ self.status = :in_progress
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,86 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{pipeline}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Danilo Sato"]
9
+ s.date = %q{2009-07-30}
10
+ s.description = %q{Pipeline is a Rails plugin/gem to run asynchronous processes in a configurable pipeline.}
11
+ s.email = %q{danilo@dtsato.com}
12
+ s.extra_rdoc_files = [
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "CHANGELOG",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "examples/auto_recoverable_pipeline.rb",
23
+ "examples/cancelling_pipeline.rb",
24
+ "examples/two_step_pipeline.rb",
25
+ "examples/user_recoverable_pipeline.rb",
26
+ "generators/pipeline/pipeline_generator.rb",
27
+ "generators/pipeline/templates/pipeline_instances_migration.rb",
28
+ "generators/pipeline/templates/pipeline_stages_migration.rb",
29
+ "init.rb",
30
+ "lib/pipeline.rb",
31
+ "lib/pipeline/api_methods.rb",
32
+ "lib/pipeline/base.rb",
33
+ "lib/pipeline/core_ext/symbol_attribute.rb",
34
+ "lib/pipeline/core_ext/transactional_attribute.rb",
35
+ "lib/pipeline/errors.rb",
36
+ "lib/pipeline/stage/base.rb",
37
+ "pipeline.gemspec",
38
+ "spec/database_integration_helper.rb",
39
+ "spec/pipeline/api_methods_spec.rb",
40
+ "spec/pipeline/base_spec.rb",
41
+ "spec/pipeline/core_ext/symbol_attribute_spec.rb",
42
+ "spec/pipeline/core_ext/transactional_attribute_spec.rb",
43
+ "spec/pipeline/errors_spec.rb",
44
+ "spec/pipeline/stage/base_spec.rb",
45
+ "spec/rcov.opts",
46
+ "spec/spec.opts",
47
+ "spec/spec_helper.rb"
48
+ ]
49
+ s.homepage = %q{http://github.com/dtsato/pipeline}
50
+ s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
51
+ s.require_paths = ["lib"]
52
+ s.rubyforge_project = %q{pipeline}
53
+ s.rubygems_version = %q{1.3.4}
54
+ s.summary = %q{A Rails plugin/gem to run asynchronous processes in a configurable pipeline}
55
+ s.test_files = [
56
+ "spec/database_integration_helper.rb",
57
+ "spec/pipeline",
58
+ "spec/pipeline/api_methods_spec.rb",
59
+ "spec/pipeline/base_spec.rb",
60
+ "spec/pipeline/core_ext",
61
+ "spec/pipeline/core_ext/symbol_attribute_spec.rb",
62
+ "spec/pipeline/core_ext/transactional_attribute_spec.rb",
63
+ "spec/pipeline/errors_spec.rb",
64
+ "spec/pipeline/stage",
65
+ "spec/pipeline/stage/base_spec.rb",
66
+ "spec/rcov.opts",
67
+ "spec/spec.opts",
68
+ "spec/spec_helper.rb"
69
+ ]
70
+
71
+ if s.respond_to? :specification_version then
72
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
73
+ s.specification_version = 3
74
+
75
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
76
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.0"])
77
+ s.add_runtime_dependency(%q<collectiveidea-delayed_job>, [">= 1.8.0"])
78
+ else
79
+ s.add_dependency(%q<activerecord>, [">= 2.0"])
80
+ s.add_dependency(%q<collectiveidea-delayed_job>, [">= 1.8.0"])
81
+ end
82
+ else
83
+ s.add_dependency(%q<activerecord>, [">= 2.0"])
84
+ s.add_dependency(%q<collectiveidea-delayed_job>, [">= 1.8.0"])
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ gem 'sqlite3-ruby'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => 'pipeline.sqlite')
5
+ ActiveRecord::Migration.verbose = false
6
+
7
+ ActiveRecord::Schema.define do
8
+
9
+ create_table :delayed_jobs, :force => true do |table|
10
+ table.integer :priority, :default => 0
11
+ table.integer :attempts, :default => 0
12
+ table.text :handler
13
+ table.string :last_error
14
+ table.datetime :run_at
15
+ table.datetime :locked_at
16
+ table.string :locked_by
17
+ table.datetime :failed_at
18
+ table.timestamps
19
+ end
20
+
21
+ create_table :pipeline_instances, :force => true do |t|
22
+ t.string :type
23
+ t.string :status
24
+ t.integer :attempts, :default => 0
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :pipeline_stages, :force => true do |t|
29
+ t.references :pipeline_instance
30
+ t.string :type
31
+ t.string :name
32
+ t.string :status
33
+ t.text :message
34
+ t.integer :attempts, :default => 0
35
+ t.timestamps
36
+ end
37
+
38
+ end
39
+
40
+ at_exit do
41
+ File.delete("pipeline.sqlite") if File.exists?("pipeline.sqlite")
42
+ end
@@ -0,0 +1,98 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Pipeline
4
+ describe ApiMethods do
5
+ class FakePipeline < Pipeline::Base
6
+ end
7
+
8
+ describe "#start" do
9
+ before(:each) do
10
+ @pipeline = FakePipeline.new
11
+ @pipeline.stub!(:save!)
12
+ Delayed::Job.stub!(:enqueue)
13
+ end
14
+
15
+ it "should only accept instance of Pipeline::Base" do
16
+ lambda {Pipeline.start(@pipeline)}.should_not raise_error
17
+ lambda {Pipeline.start(Object.new)}.should raise_error(InvalidPipelineError, "Invalid pipeline")
18
+ end
19
+
20
+ it "should save pipeline instance" do
21
+ @pipeline.should_receive(:save!)
22
+
23
+ Pipeline.start(@pipeline)
24
+ end
25
+
26
+ it "should start a job for a pipeline instance" do
27
+ Delayed::Job.should_receive(:enqueue).with(@pipeline)
28
+
29
+ Pipeline.start(@pipeline)
30
+ end
31
+
32
+ it "should provide a token for the pipeline instance" do
33
+ @pipeline.stub!(:id).and_return('123')
34
+
35
+ token = Pipeline.start(@pipeline)
36
+ token.should == '123'
37
+ end
38
+ end
39
+
40
+ describe "#resume" do
41
+ before(:each) do
42
+ @pipeline = Pipeline::Base.new
43
+ Pipeline::Base.stub!(:find).with('1').and_return(@pipeline)
44
+ Delayed::Job.stub!(:enqueue)
45
+ end
46
+
47
+ it "should accept a token for a pipeline instance" do
48
+ Pipeline::Base.should_receive(:find).with('1')
49
+
50
+ Pipeline.resume('1')
51
+ end
52
+
53
+ it "should raise error is trying to resume invalid pipeline" do
54
+ Pipeline::Base.should_receive(:find).and_return(nil)
55
+
56
+ lambda {Pipeline.resume('1')}.should raise_error(InvalidPipelineError, "Invalid pipeline")
57
+ end
58
+
59
+ it "should start a new job for that pipeline instance" do
60
+ Delayed::Job.should_receive(:enqueue).with(@pipeline)
61
+
62
+ Pipeline.resume('1')
63
+ end
64
+
65
+ it "should raise error is trying to resume in invalid state" do
66
+ @pipeline.send(:_setup)
67
+
68
+ lambda {Pipeline.resume('1')}.should raise_error(InvalidStatusError, "Status is already in progress")
69
+ end
70
+
71
+ end
72
+
73
+ describe "#cancel" do
74
+ before(:each) do
75
+ @pipeline = Pipeline::Base.new
76
+ @pipeline.stub!(:cancel)
77
+ Pipeline::Base.stub!(:find).with('1').and_return(@pipeline)
78
+ end
79
+
80
+ it "should accept a token for a pipeline instance" do
81
+ Pipeline::Base.should_receive(:find).with('1')
82
+ Pipeline.cancel('1')
83
+ end
84
+
85
+ it "should raise error is trying to cancel invalid pipeline" do
86
+ Pipeline::Base.should_receive(:find).and_return(nil)
87
+
88
+ lambda {Pipeline.cancel('1')}.should raise_error(InvalidPipelineError, "Invalid pipeline")
89
+ end
90
+
91
+ it "should cancel pipeline instance" do
92
+ @pipeline.should_receive(:cancel)
93
+ Pipeline.cancel('1')
94
+ end
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,383 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ class FirstStage < Pipeline::Stage::Base
4
+ def run
5
+ @executed = true
6
+ end
7
+
8
+ def executed?
9
+ !!@executed
10
+ end
11
+ end
12
+
13
+ class SecondStage < FirstStage; end # Ugly.. just so I don't have to write stub again
14
+
15
+ class SamplePipeline < Pipeline::Base
16
+ define_stages FirstStage >> SecondStage
17
+ end
18
+
19
+ module Pipeline
20
+ describe Base do
21
+
22
+ describe "- configuring" do
23
+ it "should allow accessing stages" do
24
+ SamplePipeline.defined_stages.should == [FirstStage, SecondStage]
25
+ end
26
+ end
27
+
28
+ describe "- setup" do
29
+ before(:each) do
30
+ @pipeline = SamplePipeline.new
31
+ end
32
+
33
+ it "should start with status not_started" do
34
+ @pipeline.status.should == :not_started
35
+ end
36
+
37
+ it "should instantiate stages with status not_started" do
38
+ @pipeline.stages.each { |stage| stage.status.should == :not_started }
39
+ end
40
+
41
+ it "should validate status" do
42
+ lambda {Base.new(:status => :something_else)}.should raise_error
43
+ end
44
+ end
45
+
46
+ describe "- persistence" do
47
+ before(:each) do
48
+ @pipeline = Base.new
49
+ end
50
+
51
+ it "should persist pipeline instance" do
52
+ @pipeline.id.should be_nil
53
+ lambda {@pipeline.save!}.should_not raise_error
54
+ @pipeline.id.should_not be_nil
55
+ end
56
+
57
+ it "should allow retrieval by id" do
58
+ @pipeline.save!
59
+
60
+ retrieved_pipeline = Base.find(@pipeline.id.to_s)
61
+ retrieved_pipeline.should === @pipeline
62
+ end
63
+
64
+ it "should persist type as single table inheritance" do
65
+ pipeline = SamplePipeline.new
66
+ pipeline.save!
67
+
68
+ retrieved_pipeline = Base.find(pipeline.id)
69
+ retrieved_pipeline.should be_an_instance_of(SamplePipeline)
70
+ end
71
+
72
+ it "should persist pipeline stages" do
73
+ pipeline = SamplePipeline.new
74
+ pipeline.stages.each {|stage| stage.id.should be_nil}
75
+ lambda {pipeline.save!}.should_not raise_error
76
+ pipeline.stages.each {|stage| stage.id.should_not be_nil}
77
+ end
78
+
79
+ it "should allow retrieval of stages with pipeline instance" do
80
+ pipeline = SamplePipeline.new
81
+ pipeline.save!
82
+
83
+ retrieved_pipeline = SamplePipeline.find(pipeline.id)
84
+ retrieved_pipeline.stages.should === pipeline.stages
85
+ end
86
+
87
+ it "should associate stages with pipeline instance" do
88
+ pipeline = SamplePipeline.new
89
+ pipeline.save!
90
+
91
+ pipeline.stages.each {|stage| stage.pipeline.should === pipeline}
92
+ end
93
+
94
+ it "should destroy stages when pipeline instance is destroyed" do
95
+ pipeline = SamplePipeline.new
96
+ pipeline.save!
97
+
98
+ Pipeline::Stage::Base.count(:conditions => ['pipeline_instance_id = ?', pipeline.id]).should > 0
99
+
100
+ pipeline.destroy
101
+ Pipeline::Stage::Base.count(:conditions => ['pipeline_instance_id = ?', pipeline.id]).should == 0
102
+ end
103
+ end
104
+
105
+ describe "- execution (success)" do
106
+ before(:each) do
107
+ @pipeline = SamplePipeline.new
108
+ end
109
+
110
+ it "should increment attempts" do
111
+ failed_stage = SecondStage.new
112
+ failed_stage.stub!(:run).and_raise(RecoverableError.new("message", true))
113
+ SecondStage.stub!(:new).and_return(failed_stage)
114
+
115
+ pipeline = SamplePipeline.new
116
+
117
+ pipeline.perform
118
+ pipeline.attempts.should == 1
119
+
120
+ pipeline.perform
121
+ pipeline.attempts.should == 2
122
+ end
123
+
124
+ it "should perform each stage" do
125
+ @pipeline.stages.each { |stage| stage.should_not be_executed }
126
+ @pipeline.perform
127
+ @pipeline.stages.each { |stage| stage.should be_executed }
128
+ end
129
+
130
+ it "should update pipeline status after all stages finished" do
131
+ @pipeline.perform
132
+ @pipeline.status.should == :completed
133
+ end
134
+
135
+ it "should save status" do
136
+ @pipeline.save!
137
+ @pipeline.perform
138
+ @pipeline.reload.status.should == :completed
139
+ end
140
+ end
141
+
142
+ describe "- execution (in progress)" do
143
+ it "should set status to in_progress" do
144
+ pipeline = SamplePipeline.new
145
+ pipeline.send(:_setup)
146
+
147
+ pipeline.status.should == :in_progress
148
+ pipeline.reload.status.should == :in_progress
149
+ end
150
+ end
151
+
152
+ describe "- execution (irrecoverable error)" do
153
+ before(:each) do
154
+ failed_stage = SecondStage.new
155
+ failed_stage.stub!(:run).and_raise(IrrecoverableError.new)
156
+ SecondStage.stub!(:new).and_return(failed_stage)
157
+ @pipeline = SamplePipeline.new
158
+ end
159
+
160
+ it "should not re-raise error" do
161
+ lambda {@pipeline.perform}.should_not raise_error(IrrecoverableError)
162
+ end
163
+
164
+ it "should update status" do
165
+ @pipeline.perform
166
+ @pipeline.status.should == :failed
167
+ end
168
+
169
+ it "should save status" do
170
+ @pipeline.save!
171
+ @pipeline.perform
172
+ @pipeline.reload.status.should == :failed
173
+ end
174
+ end
175
+
176
+ describe "- execution (recoverable error that doesn't require user input)" do
177
+ before(:each) do
178
+ failed_stage = SecondStage.new
179
+ failed_stage.stub!(:run).and_raise(RecoverableError.new)
180
+ SecondStage.stub!(:new).and_return(failed_stage)
181
+ @pipeline = SamplePipeline.new
182
+ end
183
+
184
+ it "should re-raise error (so delayed_job retry works)" do
185
+ lambda {@pipeline.perform}.should raise_error(RecoverableError)
186
+ end
187
+
188
+ it "should keep status :in_progress" do
189
+ lambda {@pipeline.perform}.should raise_error(RecoverableError)
190
+ @pipeline.status.should == :in_progress
191
+ end
192
+
193
+ it "should save status" do
194
+ @pipeline.save!
195
+ lambda {@pipeline.perform}.should raise_error(RecoverableError)
196
+ @pipeline.reload.status.should == :in_progress
197
+ end
198
+ end
199
+
200
+ describe "- execution (recoverable error that requires user input)" do
201
+ before(:each) do
202
+ failed_stage = SecondStage.new
203
+ failed_stage.stub!(:run).and_raise(RecoverableError.new('message', true))
204
+ SecondStage.stub!(:new).and_return(failed_stage)
205
+ @pipeline = SamplePipeline.new
206
+ end
207
+
208
+ it "should not re-raise error" do
209
+ lambda {@pipeline.perform}.should_not raise_error(RecoverableError)
210
+ end
211
+
212
+ it "should update status" do
213
+ @pipeline.perform
214
+ @pipeline.status.should == :paused
215
+ end
216
+
217
+ it "should save status" do
218
+ @pipeline.save!
219
+ @pipeline.perform
220
+ @pipeline.reload.status.should == :paused
221
+ end
222
+ end
223
+
224
+ describe "- execution (other errors will pause the pipeline)" do
225
+ before(:each) do
226
+ failed_stage = SecondStage.new
227
+ failed_stage.stub!(:run).and_raise(StandardError.new)
228
+ SecondStage.stub!(:new).and_return(failed_stage)
229
+ @pipeline = SamplePipeline.new
230
+ end
231
+
232
+ it "should not re-raise error" do
233
+ lambda {@pipeline.perform}.should_not raise_error(StandardError)
234
+ end
235
+
236
+ it "should update status" do
237
+ @pipeline.perform
238
+ @pipeline.status.should == :paused
239
+ end
240
+
241
+ it "should save status" do
242
+ @pipeline.save!
243
+ @pipeline.perform
244
+ @pipeline.reload.status.should == :paused
245
+ end
246
+ end
247
+
248
+ describe "- execution (retrying)" do
249
+ before(:each) do
250
+ @passing_stage = FirstStage.new
251
+ FirstStage.stub!(:new).and_return(@passing_stage)
252
+
253
+ @failed_stage = SecondStage.new
254
+ @failed_stage.stub!(:run).and_raise(RecoverableError.new('message', true))
255
+ SecondStage.stub!(:new).and_return(@failed_stage)
256
+ @pipeline = SamplePipeline.new
257
+ end
258
+
259
+ it "should not re-raise error" do
260
+ lambda {@pipeline.perform}.should_not raise_error(RecoverableError)
261
+ end
262
+
263
+ it "should update status" do
264
+ @pipeline.perform
265
+ @pipeline.status.should == :paused
266
+ end
267
+
268
+ it "should save status" do
269
+ @pipeline.save!
270
+ @pipeline.perform
271
+ @pipeline.reload.status.should == :paused
272
+ end
273
+
274
+ it "should skip completed stages" do
275
+ @pipeline.perform
276
+ @passing_stage.attempts.should == 1
277
+ @failed_stage.attempts.should == 1
278
+
279
+ @pipeline.perform
280
+ @passing_stage.attempts.should == 1
281
+ @failed_stage.attempts.should == 2
282
+ end
283
+ end
284
+
285
+ describe "- execution (state transitions)" do
286
+ before(:each) do
287
+ @pipeline = Base.new
288
+ end
289
+
290
+ it "should execute if status is :not_started" do
291
+ @pipeline.should be_ok_to_resume
292
+ lambda {@pipeline.perform}.should_not raise_error(InvalidStatusError)
293
+ end
294
+
295
+ it "should execute if status is :paused (for retrying)" do
296
+ @pipeline.send(:status=, :paused)
297
+
298
+ @pipeline.should be_ok_to_resume
299
+ lambda {@pipeline.perform}.should_not raise_error(InvalidStatusError)
300
+ end
301
+
302
+ it "should not execute if status is :in_progress" do
303
+ @pipeline.send(:status=, :in_progress)
304
+
305
+ @pipeline.should_not be_ok_to_resume
306
+ lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already in progress")
307
+ end
308
+
309
+ it "should not execute if status is :completed" do
310
+ @pipeline.send(:status=, :completed)
311
+
312
+ @pipeline.should_not be_ok_to_resume
313
+ lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already completed")
314
+ end
315
+
316
+ it "should not execute if status is :failed" do
317
+ @pipeline.send(:status=, :failed)
318
+
319
+ @pipeline.should_not be_ok_to_resume
320
+ lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already failed")
321
+ end
322
+ end
323
+
324
+ describe "- cancelling" do
325
+ before(:each) do
326
+ @passing_stage = FirstStage.new
327
+ FirstStage.stub!(:new).and_return(@passing_stage)
328
+
329
+ @failed_stage = SecondStage.new
330
+ @failed_stage.stub!(:run).and_raise(RecoverableError.new('message', true))
331
+ SecondStage.stub!(:new).and_return(@failed_stage)
332
+ @pipeline = SamplePipeline.new
333
+ @pipeline.perform
334
+ end
335
+
336
+ it "should update status" do
337
+ @pipeline.cancel
338
+ @pipeline.status.should == :failed
339
+ end
340
+
341
+ it "should save status" do
342
+ @pipeline.save!
343
+ @pipeline.cancel
344
+ @pipeline.reload.status.should == :failed
345
+ end
346
+ end
347
+
348
+ describe "- cancelling (state transitions)" do
349
+ before(:each) do
350
+ @pipeline = Base.new
351
+ end
352
+
353
+ it "should cancel if status is :not_started" do
354
+ lambda {@pipeline.cancel}.should_not raise_error(InvalidStatusError)
355
+ end
356
+
357
+ it "should cancel if status is :paused (for retrying)" do
358
+ @pipeline.send(:status=, :paused)
359
+
360
+ lambda {@pipeline.cancel}.should_not raise_error(InvalidStatusError)
361
+ end
362
+
363
+ it "should not cancel if status is :in_progress" do
364
+ @pipeline.send(:status=, :in_progress)
365
+
366
+ lambda {@pipeline.cancel}.should raise_error(InvalidStatusError, "Status is already in progress")
367
+ end
368
+
369
+ it "should not cancel if status is :completed" do
370
+ @pipeline.send(:status=, :completed)
371
+
372
+ lambda {@pipeline.cancel}.should raise_error(InvalidStatusError, "Status is already completed")
373
+ end
374
+
375
+ it "should not cancel if status is :failed" do
376
+ @pipeline.send(:status=, :failed)
377
+
378
+ lambda {@pipeline.cancel}.should raise_error(InvalidStatusError, "Status is already failed")
379
+ end
380
+ end
381
+
382
+ end
383
+ end