pipeline 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/CHANGELOG +12 -0
- data/LICENSE +20 -0
- data/README.rdoc +67 -0
- data/Rakefile +104 -0
- data/VERSION +1 -0
- data/examples/auto_recoverable_pipeline.rb +35 -0
- data/examples/cancelling_pipeline.rb +32 -0
- data/examples/two_step_pipeline.rb +27 -0
- data/examples/user_recoverable_pipeline.rb +33 -0
- data/generators/pipeline/pipeline_generator.rb +12 -0
- data/generators/pipeline/templates/pipeline_instances_migration.rb +16 -0
- data/generators/pipeline/templates/pipeline_stages_migration.rb +19 -0
- data/init.rb +1 -0
- data/lib/pipeline.rb +16 -0
- data/lib/pipeline/api_methods.rb +23 -0
- data/lib/pipeline/base.rb +67 -0
- data/lib/pipeline/core_ext/symbol_attribute.rb +29 -0
- data/lib/pipeline/core_ext/transactional_attribute.rb +26 -0
- data/lib/pipeline/errors.rb +24 -0
- data/lib/pipeline/stage/base.rb +60 -0
- data/pipeline.gemspec +86 -0
- data/spec/database_integration_helper.rb +42 -0
- data/spec/pipeline/api_methods_spec.rb +98 -0
- data/spec/pipeline/base_spec.rb +383 -0
- data/spec/pipeline/core_ext/symbol_attribute_spec.rb +38 -0
- data/spec/pipeline/core_ext/transactional_attribute_spec.rb +22 -0
- data/spec/pipeline/errors_spec.rb +46 -0
- data/spec/pipeline/stage/base_spec.rb +203 -0
- data/spec/rcov.opts +1 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +10 -0
- metadata +117 -0
@@ -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
|
data/pipeline.gemspec
ADDED
@@ -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
|