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.
@@ -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
@@ -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
@@ -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
- class_inheritable_accessor :default_name, :instance_writer => false
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
- # Subclass must implement this as part of the contract
58
- def run; end
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
@@ -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"
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-08-20}
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.4}
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<collectiveidea-delayed_job>, [">= 1.8.0"])
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<collectiveidea-delayed_job>, [">= 1.8.0"])
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<collectiveidea-delayed_job>, [">= 1.8.0"])
96
+ s.add_dependency(%q<delayed_job>, [">= 1.8.0"])
91
97
  end
92
98
  end
99
+
@@ -22,6 +22,7 @@ ActiveRecord::Schema.define do
22
22
  t.string :type
23
23
  t.string :status
24
24
  t.integer :attempts, :default => 0
25
+ t.references :external
25
26
  t.timestamps
26
27
  end
27
28
 
@@ -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