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.
- 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
|