pipeline 0.0.8 → 0.0.9
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 +2 -1
- data/CHANGELOG +14 -0
- data/README.rdoc +31 -9
- data/Rakefile +3 -1
- data/TODO +1 -2
- data/VERSION +1 -1
- data/generators/pipeline/templates/migration.rb +3 -0
- data/lib/pipeline.rb +2 -1
- data/lib/pipeline/api_methods.rb +15 -1
- data/lib/pipeline/base.rb +204 -13
- data/lib/pipeline/core_ext/symbol_attribute.rb +16 -2
- data/lib/pipeline/core_ext/transactional_attribute.rb +18 -1
- data/lib/pipeline/errors.rb +18 -2
- data/lib/pipeline/stage/base.rb +141 -8
- data/pipeline.gemspec +13 -6
- data/spec/database_integration_helper.rb +1 -0
- data/spec/models.rb +72 -0
- data/spec/pipeline/api_methods_spec.rb +5 -4
- data/spec/pipeline/base_spec.rb +119 -80
- data/spec/pipeline/core_ext/symbol_attribute_spec.rb +1 -1
- data/spec/pipeline/core_ext/transactional_attribute_spec.rb +1 -1
- data/spec/pipeline/errors_spec.rb +1 -1
- data/spec/pipeline/stage/base_spec.rb +103 -88
- data/spec/spec_helper.rb +2 -0
- metadata +7 -4
@@ -1,10 +1,24 @@
|
|
1
1
|
module Pipeline
|
2
|
+
# Extends ActiveRecord::Base to save and retrieve symbol attributes as strings.
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
# class Card < ActiveRecord::Base
|
6
|
+
# symbol_attrs :rank, :suit
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# card = Card.new(:rank => 'jack', :suit => 'hearts')
|
10
|
+
# card.rank # => :jack
|
11
|
+
# card.suit # => :hearts
|
12
|
+
#
|
13
|
+
# It also allow symbol attributes to be used on ActiveRecord #find conditions:
|
14
|
+
#
|
15
|
+
# Card.find(:all, :conditions => ['suit = ?', :clubs])
|
2
16
|
module SymbolAttribute
|
3
17
|
def self.included(base)
|
4
18
|
base.extend(ClassMethods)
|
5
19
|
end
|
6
20
|
|
7
|
-
module ClassMethods
|
21
|
+
module ClassMethods #:nodoc:
|
8
22
|
def symbol_attrs(*attributes)
|
9
23
|
attributes.each do |attribute|
|
10
24
|
class_eval <<-EOD
|
@@ -20,7 +34,7 @@ module Pipeline
|
|
20
34
|
end
|
21
35
|
end
|
22
36
|
|
23
|
-
class Symbol
|
37
|
+
class Symbol #:nodoc:
|
24
38
|
def quoted_id
|
25
39
|
"'#{ActiveRecord::Base.connection.quote_string(self.to_s)}'"
|
26
40
|
end
|
@@ -1,10 +1,27 @@
|
|
1
1
|
module Pipeline
|
2
|
+
# Extends ActiveRecord::Base to provide attributes that are saved in a nested
|
3
|
+
# transaction when updated through a setter.
|
4
|
+
#
|
5
|
+
# NOTE: When the extended attributes are updated, the entire record is saved
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# class Car < ActiveRecord::Base
|
9
|
+
# transactional_attrs :state, :engine_state
|
10
|
+
#
|
11
|
+
# def run
|
12
|
+
# self.engine_state = :on if self.state == :on
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# car = Car.new
|
17
|
+
# car.state = :on # this will save the record in a transaction
|
18
|
+
# car.run # Record will be saved again, since #run updates :engine_state
|
2
19
|
module TransactionalAttribute
|
3
20
|
def self.included (base)
|
4
21
|
base.extend(ClassMethods)
|
5
22
|
end
|
6
23
|
|
7
|
-
module ClassMethods
|
24
|
+
module ClassMethods #:nodoc:
|
8
25
|
def transactional_attrs(*attributes)
|
9
26
|
attributes.each do |attribute|
|
10
27
|
class_eval <<-EOD
|
data/lib/pipeline/errors.rb
CHANGED
@@ -1,24 +1,40 @@
|
|
1
1
|
module Pipeline
|
2
|
+
# Exception to represents an invalid pipeline. Raised by methods on Pipeline::ApiMethods
|
2
3
|
class InvalidPipelineError < StandardError; end
|
3
4
|
|
5
|
+
# Exception to represent an invalid state transition. Raised by execution methods on
|
6
|
+
# Pipeline::Base and Pipeline::Stage::Base. E.g. trying to transition a :failed
|
7
|
+
# pipeline to :in_progress.
|
4
8
|
class InvalidStatusError < StandardError
|
9
|
+
# The current status of pipeline or stage
|
5
10
|
def initialize(status)
|
6
11
|
super("Status is already #{status.to_s.gsub(/_/, ' ')}")
|
7
12
|
end
|
8
13
|
end
|
9
14
|
|
15
|
+
# Exception to represent an irrecoverable error that can be raised by subclasses of
|
16
|
+
# Pipeline::Stage::Base that override the method <tt>run</tt>. Please refer to
|
17
|
+
# Pipeline::Base for more details about error handling.
|
10
18
|
class IrrecoverableError < StandardError; end
|
11
19
|
|
20
|
+
# Exception to represent a recoverable error that can be raised by subclasses of
|
21
|
+
# Pipeline::Stage::Base that override the method <tt>run</tt>. Please refer to
|
22
|
+
# Pipeline::Base for more details about error handling.
|
12
23
|
class RecoverableError < StandardError
|
24
|
+
# Instantiates a new instance of RecoverableError.
|
25
|
+
# [msg] Is a description of the error message
|
26
|
+
# [input_required] Is a boolean that determines if the error requires user action
|
27
|
+
# (<tt>true</tt>, meaning it can not be automatically retried) or
|
28
|
+
# not (<tt>false</tt>, meaning it can be automatically retried).
|
29
|
+
# Default is <tt>false</tt>
|
13
30
|
def initialize(msg = nil, input_required = false)
|
14
31
|
super(msg)
|
15
32
|
@input_required = input_required
|
16
33
|
end
|
17
34
|
|
35
|
+
# Returns <tt>true</tt> if this error requires user action or <tt>false</tt> otherwise.
|
18
36
|
def input_required?
|
19
37
|
@input_required
|
20
38
|
end
|
21
39
|
end
|
22
|
-
|
23
|
-
extend(ApiMethods)
|
24
40
|
end
|
data/lib/pipeline/stage/base.rb
CHANGED
@@ -1,5 +1,106 @@
|
|
1
1
|
module Pipeline
|
2
|
-
module Stage
|
2
|
+
module Stage # :nodoc:
|
3
|
+
# A stage represents one of the steps in a pipeline. Stages can be reused by
|
4
|
+
# different pipelines. The behaviour of a stage is determined by subclasses of
|
5
|
+
# Pipeline::Stage::Base implementing the method #run:
|
6
|
+
#
|
7
|
+
# class PrepareIngredients < Pipeline::Stage::Base
|
8
|
+
# def run
|
9
|
+
# Ingredients.each do |ingredient|
|
10
|
+
# ingredient.wash
|
11
|
+
# ingredient.slice!
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# If a stage need access to its associated pipeline, it can call the association
|
17
|
+
# <tt>pipeline</tt>.
|
18
|
+
#
|
19
|
+
# == Stage Name
|
20
|
+
#
|
21
|
+
# By default, a stage will have a standard name that corresponds to its fully
|
22
|
+
# qualified class name (e.g. Pipeline::Stage::Base or SamplePipeline::SampleStage).
|
23
|
+
# You can provide a more descriptive name by calling <tt>default_name</tt>:
|
24
|
+
#
|
25
|
+
# class PrepareIngredients < Pipeline::Stage::Base
|
26
|
+
# self.default_name = "Prepare Ingredients for Cooking"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# You can retrieve the name of a stage using the <tt>name</tt> attribute:
|
30
|
+
#
|
31
|
+
# stage = PrepareIngredients.new
|
32
|
+
# stage.name # => "Prepare Ingredients for Cooking"
|
33
|
+
#
|
34
|
+
# == Error Handling
|
35
|
+
#
|
36
|
+
# In case of failure, a stage can raise special exceptions (RecoverableError or
|
37
|
+
# IrrecoverableError) to determine what happens to the pipeline. Please refer to
|
38
|
+
# Pipeline::Base for a description of the possible outcomes. Any failure will
|
39
|
+
# persist the Exception's message on the <tt>message</tt> attribute and will move
|
40
|
+
# the stage to a :failed state.
|
41
|
+
#
|
42
|
+
# == State Transitions
|
43
|
+
#
|
44
|
+
# The following diagram represents the state transitions a stage instance can
|
45
|
+
# go through during its life-cycle:
|
46
|
+
#
|
47
|
+
# :not_started ---> :in_progress ---> :completed
|
48
|
+
# ^ |
|
49
|
+
# | v
|
50
|
+
# :failed
|
51
|
+
#
|
52
|
+
# [:not_started] The stage was instantiated but not started yet.
|
53
|
+
# [:in_progress] After started or retried, the stage remains on this state while
|
54
|
+
# executing.
|
55
|
+
# [:completed] After successfully executing, the stage is completed.
|
56
|
+
# [:failed] If an error occurs, the stage goes into this stage.
|
57
|
+
#
|
58
|
+
# == Callbacks
|
59
|
+
#
|
60
|
+
# You can define custom callbacks to be called before (+before_stage+) and after
|
61
|
+
# (+after_stage+) executing a stage. Example:
|
62
|
+
#
|
63
|
+
# class PrepareIngredients < Pipeline::Stage::Base
|
64
|
+
# before_stage :wash_ingredients
|
65
|
+
#
|
66
|
+
# def run
|
67
|
+
# puts "Slicing..."
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# protected
|
71
|
+
# def wash_ingredients
|
72
|
+
# puts "Washing..."
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# class Cook < Pipeline::Stage::Base
|
77
|
+
# after_stage :serve
|
78
|
+
#
|
79
|
+
# def run
|
80
|
+
# puts "Cooking..."
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# protected
|
84
|
+
# def serve
|
85
|
+
# puts "bon appetit!"
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# class MakeDinnerPipeline < Pipeline::Base
|
90
|
+
# define_stages PrepareIngredients >> Cook
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# Pipeline.start(MakeDinnerPipeline.new)
|
94
|
+
#
|
95
|
+
# Outputs:
|
96
|
+
# Washing...
|
97
|
+
# Slicing...
|
98
|
+
# Cooking...
|
99
|
+
# bon appetit!
|
100
|
+
#
|
101
|
+
# Callbacks can be defined as a symbol that calls a private/protected method (like the
|
102
|
+
# example above), as an inline block, or as a +Callback+ object, as a regular
|
103
|
+
# +ActiveRecord+ callback.
|
3
104
|
class Base < ActiveRecord::Base
|
4
105
|
set_table_name :pipeline_stages
|
5
106
|
|
@@ -10,23 +111,40 @@ module Pipeline
|
|
10
111
|
symbol_attr :status
|
11
112
|
transactional_attr :status
|
12
113
|
private :status=
|
13
|
-
|
14
|
-
belongs_to :pipeline, :class_name => "Pipeline::Base", :foreign_key => 'pipeline_instance_id'
|
15
114
|
|
115
|
+
# Allows access to the associated pipeline
|
116
|
+
belongs_to :pipeline, :class_name => "Pipeline::Base", :foreign_key => 'pipeline_instance_id'
|
117
|
+
|
118
|
+
class_inheritable_accessor :default_name, :instance_writer => false
|
119
|
+
|
120
|
+
define_callbacks :before_stage, :after_stage
|
121
|
+
|
16
122
|
@@chain = []
|
123
|
+
# Method used for chaining stages on a pipeline sequence. Please refer to
|
124
|
+
# Pipeline::Base for example usages.
|
17
125
|
def self.>>(next_stage)
|
18
126
|
@@chain << self
|
19
127
|
next_stage
|
20
128
|
end
|
21
129
|
|
130
|
+
# Method used by Pipeline::Base to construct its chain of stages. Please
|
131
|
+
# refer to Pipeline::Base
|
22
132
|
def self.build_chain
|
23
133
|
chain = @@chain + [self]
|
24
134
|
@@chain = []
|
25
135
|
chain
|
26
136
|
end
|
27
137
|
|
28
|
-
|
29
|
-
|
138
|
+
# Standard ActiveRecord callback to setup initial name and status
|
139
|
+
# when a new stage is instantiated. If you override this callback, make
|
140
|
+
# sure to call +super+:
|
141
|
+
#
|
142
|
+
# class SampleStage < Pipeline::Stage::Base
|
143
|
+
# def after_initialize
|
144
|
+
# super
|
145
|
+
# self[:special_attribute] ||= "standard value"
|
146
|
+
# end
|
147
|
+
# end
|
30
148
|
def after_initialize
|
31
149
|
if new_record?
|
32
150
|
self[:status] = :not_started
|
@@ -34,10 +152,19 @@ module Pipeline
|
|
34
152
|
end
|
35
153
|
end
|
36
154
|
|
155
|
+
# Returns <tt>true</tt> if the stage is in a :completed state, <tt>false</tt>
|
156
|
+
# otherwise.
|
37
157
|
def completed?
|
38
158
|
status == :completed
|
39
159
|
end
|
40
160
|
|
161
|
+
# Standard method called when executing this stage. Raises
|
162
|
+
# InvalidStatusError if stage is in an invalid state for execution (e.g.
|
163
|
+
# already completed, or in progress).
|
164
|
+
#
|
165
|
+
# <b>NOTE:</b> Do not override this method to determine the behaviour of a
|
166
|
+
# stage. This method will be called by the executing pipeline. Please override
|
167
|
+
# #run instead.
|
41
168
|
def perform
|
42
169
|
reload unless new_record?
|
43
170
|
raise InvalidStatusError.new(status) unless [:not_started, :failed].include?(status)
|
@@ -51,17 +178,23 @@ module Pipeline
|
|
51
178
|
self.message = e.message
|
52
179
|
self.status = :failed
|
53
180
|
raise e
|
181
|
+
ensure
|
182
|
+
run_callbacks(:after_stage)
|
54
183
|
end
|
55
184
|
end
|
56
|
-
|
57
|
-
#
|
58
|
-
|
185
|
+
|
186
|
+
# Abstract method to be implemented by all subclasses that represents the
|
187
|
+
# action to be performed by this stage
|
188
|
+
def run
|
189
|
+
raise "This method must be implemented by any subclass of Pipeline::Stage::Base"
|
190
|
+
end
|
59
191
|
|
60
192
|
private
|
61
193
|
def _setup
|
62
194
|
self.attempts += 1
|
63
195
|
self.message = nil
|
64
196
|
self.status = :in_progress
|
197
|
+
run_callbacks(:before_stage)
|
65
198
|
end
|
66
199
|
end
|
67
200
|
end
|
data/pipeline.gemspec
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
1
4
|
# -*- encoding: utf-8 -*-
|
2
5
|
|
3
6
|
Gem::Specification.new do |s|
|
4
7
|
s.name = %q{pipeline}
|
5
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.9"
|
6
9
|
|
7
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
11
|
s.authors = ["Danilo Sato"]
|
9
|
-
s.date = %q{2009-
|
12
|
+
s.date = %q{2009-12-14}
|
10
13
|
s.description = %q{Pipeline is a Rails plugin/gem to run asynchronous processes in a configurable pipeline.}
|
11
14
|
s.email = %q{danilo@dtsato.com}
|
12
15
|
s.extra_rdoc_files = [
|
@@ -37,6 +40,7 @@ Gem::Specification.new do |s|
|
|
37
40
|
"lib/pipeline/stage/base.rb",
|
38
41
|
"pipeline.gemspec",
|
39
42
|
"spec/database_integration_helper.rb",
|
43
|
+
"spec/models.rb",
|
40
44
|
"spec/pipeline/api_methods_spec.rb",
|
41
45
|
"spec/pipeline/base_spec.rb",
|
42
46
|
"spec/pipeline/core_ext/symbol_attribute_spec.rb",
|
@@ -51,10 +55,11 @@ Gem::Specification.new do |s|
|
|
51
55
|
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
|
52
56
|
s.require_paths = ["lib"]
|
53
57
|
s.rubyforge_project = %q{pipeline}
|
54
|
-
s.rubygems_version = %q{1.3.
|
58
|
+
s.rubygems_version = %q{1.3.5}
|
55
59
|
s.summary = %q{A Rails plugin/gem to run asynchronous processes in a configurable pipeline}
|
56
60
|
s.test_files = [
|
57
61
|
"spec/database_integration_helper.rb",
|
62
|
+
"spec/models.rb",
|
58
63
|
"spec/pipeline",
|
59
64
|
"spec/pipeline/api_methods_spec.rb",
|
60
65
|
"spec/pipeline/base_spec.rb",
|
@@ -68,6 +73,7 @@ Gem::Specification.new do |s|
|
|
68
73
|
"spec/spec.opts",
|
69
74
|
"spec/spec_helper.rb",
|
70
75
|
"spec/database_integration_helper.rb",
|
76
|
+
"spec/models.rb",
|
71
77
|
"spec/pipeline",
|
72
78
|
"spec/rcov.opts",
|
73
79
|
"spec/spec.opts",
|
@@ -80,13 +86,14 @@ Gem::Specification.new do |s|
|
|
80
86
|
|
81
87
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
82
88
|
s.add_runtime_dependency(%q<activerecord>, [">= 2.0"])
|
83
|
-
s.add_runtime_dependency(%q<
|
89
|
+
s.add_runtime_dependency(%q<delayed_job>, [">= 1.8.0"])
|
84
90
|
else
|
85
91
|
s.add_dependency(%q<activerecord>, [">= 2.0"])
|
86
|
-
s.add_dependency(%q<
|
92
|
+
s.add_dependency(%q<delayed_job>, [">= 1.8.0"])
|
87
93
|
end
|
88
94
|
else
|
89
95
|
s.add_dependency(%q<activerecord>, [">= 2.0"])
|
90
|
-
s.add_dependency(%q<
|
96
|
+
s.add_dependency(%q<delayed_job>, [">= 1.8.0"])
|
91
97
|
end
|
92
98
|
end
|
99
|
+
|
data/spec/models.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
class StubStage < Pipeline::Stage::Base
|
2
|
+
def run
|
3
|
+
@executed = true
|
4
|
+
end
|
5
|
+
|
6
|
+
def executed?
|
7
|
+
!!@executed
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class FirstStage < StubStage; end
|
12
|
+
|
13
|
+
class SecondStage < StubStage; end
|
14
|
+
|
15
|
+
class FailedStage < StubStage
|
16
|
+
def run
|
17
|
+
super
|
18
|
+
raise StandardError.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class IrrecoverableStage < StubStage
|
23
|
+
def run
|
24
|
+
super
|
25
|
+
raise Pipeline::IrrecoverableError.new("message")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class RecoverableInputRequiredStage < StubStage
|
30
|
+
def run
|
31
|
+
super
|
32
|
+
raise Pipeline::RecoverableError.new("message", true)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class RecoverableStage < StubStage
|
37
|
+
def run
|
38
|
+
super
|
39
|
+
raise Pipeline::RecoverableError.new("message")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class GenericErrorStage < StubStage
|
44
|
+
def run
|
45
|
+
super
|
46
|
+
raise Exception.new
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class SamplePipeline < Pipeline::Base
|
51
|
+
define_stages FirstStage >> SecondStage
|
52
|
+
|
53
|
+
before_pipeline :before_pipeline_callback
|
54
|
+
after_pipeline :after_pipeline_callback
|
55
|
+
|
56
|
+
private
|
57
|
+
def before_pipeline_callback; end
|
58
|
+
def after_pipeline_callback; end
|
59
|
+
end
|
60
|
+
|
61
|
+
class SampleStage < Pipeline::Stage::Base
|
62
|
+
before_stage :before_stage_callback
|
63
|
+
after_stage :after_stage_callback
|
64
|
+
|
65
|
+
def run
|
66
|
+
# nothing...
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def before_stage_callback; end
|
71
|
+
def after_stage_callback; end
|
72
|
+
end
|