pipeline 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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