dtsato-pipeline 0.0.6 → 0.0.7

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/CHANGELOG CHANGED
@@ -1,3 +1,16 @@
1
+ 0.0.7
2
+ =====
3
+
4
+ Features:
5
+ * Allowing for user-defined default failure mode: when generic errors are raised, pipeline can be paused (default) or fail. Usage:
6
+ class MySamplePipeline < Pipeline::Base
7
+ self.default_failure_mode = :cancel
8
+ end
9
+
10
+ Bug Fix:
11
+ * Refreshes the pipeline and stage statuses before execution (at runtime) in case of offline status update
12
+ * Rescue from ActiveRecord::RecordNotFound instead of relying on ActiveRecord#find(id) returning nil
13
+
1
14
  0.0.6
2
15
  =====
3
16
 
data/Rakefile CHANGED
@@ -77,7 +77,7 @@ begin
77
77
  Synthesis::Task.new("spec:synthesis") do |t|
78
78
  t.adapter = :rspec
79
79
  t.pattern = 'spec/**/*_spec.rb'
80
- t.ignored = ['Pipeline::FakePipeline', 'Delayed::Job']
80
+ t.ignored = ['Pipeline::FakePipeline', 'Delayed::Job', 'SampleStage', 'Logger']
81
81
  end
82
82
  rescue LoadError
83
83
  desc 'Synthesis rake task not available'
data/TODO CHANGED
@@ -4,6 +4,7 @@
4
4
  * Improve documentation (RDoc)
5
5
  * Adapter for other persistence frameworks (ActiveModel?)
6
6
  * Adapter for other background processing mechanisms (currently relying on dj's auto-retry)
7
+ * Max attempts on pipeline (or stage)
7
8
 
8
9
  Think more about:
9
10
  - Repeated code on status handling on pipeline instance/pipeline stage
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.6
1
+ 0.0.7
@@ -9,15 +9,17 @@ module Pipeline
9
9
 
10
10
  def resume(id)
11
11
  pipeline = Base.find(id)
12
- raise InvalidPipelineError.new("Invalid pipeline") unless pipeline
13
12
  raise InvalidStatusError.new(pipeline.status) unless pipeline.ok_to_resume?
14
13
  Delayed::Job.enqueue(pipeline)
14
+ rescue ActiveRecord::RecordNotFound
15
+ raise InvalidPipelineError.new("Invalid pipeline")
15
16
  end
16
17
 
17
18
  def cancel(id)
18
19
  pipeline = Base.find(id)
19
- raise InvalidPipelineError.new("Invalid pipeline") unless pipeline
20
20
  pipeline.cancel
21
+ rescue ActiveRecord::RecordNotFound
22
+ raise InvalidPipelineError.new("Invalid pipeline")
21
23
  end
22
24
  end
23
25
  end
data/lib/pipeline/base.rb CHANGED
@@ -14,11 +14,19 @@ module Pipeline
14
14
 
15
15
  class_inheritable_accessor :defined_stages, :instance_writer => false
16
16
  self.defined_stages = []
17
+
18
+ class_inheritable_accessor :failure_mode, :instance_writer => false
19
+ self.failure_mode = :pause
17
20
 
18
21
  def self.define_stages(stages)
19
22
  self.defined_stages = stages.build_chain
20
23
  end
21
24
 
25
+ def self.default_failure_mode=(mode)
26
+ new_mode = [:pause, :cancel].include?(mode) ? mode : :pause
27
+ self.failure_mode = new_mode
28
+ end
29
+
22
30
  def after_initialize
23
31
  self[:status] = :not_started if new_record?
24
32
  if new_record?
@@ -29,6 +37,7 @@ module Pipeline
29
37
  end
30
38
 
31
39
  def perform
40
+ reload unless new_record?
32
41
  raise InvalidStatusError.new(status) unless ok_to_resume?
33
42
  begin
34
43
  _setup
@@ -45,7 +54,7 @@ module Pipeline
45
54
  raise e
46
55
  end
47
56
  rescue
48
- self.status = :paused
57
+ self.status = (failure_mode == :cancel ? :failed : :paused)
49
58
  end
50
59
  end
51
60
 
@@ -37,6 +37,7 @@ module Pipeline
37
37
  end
38
38
 
39
39
  def perform
40
+ reload unless new_record?
40
41
  raise InvalidStatusError.new(status) unless [:not_started, :failed].include?(status)
41
42
  begin
42
43
  _setup
data/pipeline.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{pipeline}
5
- s.version = "0.0.6"
5
+ s.version = "0.0.7"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Danilo Sato"]
9
- s.date = %q{2009-08-12}
9
+ s.date = %q{2009-08-18}
10
10
  s.description = %q{Pipeline is a Rails plugin/gem to run asynchronous processes in a configurable pipeline.}
11
11
  s.email = %q{danilo@dtsato.com}
12
12
  s.extra_rdoc_files = [
@@ -50,8 +50,10 @@ module Pipeline
50
50
  Pipeline.resume('1')
51
51
  end
52
52
 
53
- it "should raise error is trying to resume invalid pipeline" do
54
- Pipeline::Base.should_receive(:find).and_return(nil)
53
+ it "should raise error if trying to resume invalid pipeline" do
54
+ Pipeline::Base.should_receive(:find).
55
+ with('1').
56
+ and_raise(ActiveRecord::RecordNotFound.new)
55
57
 
56
58
  lambda {Pipeline.resume('1')}.should raise_error(InvalidPipelineError, "Invalid pipeline")
57
59
  end
@@ -83,7 +85,9 @@ module Pipeline
83
85
  end
84
86
 
85
87
  it "should raise error is trying to cancel invalid pipeline" do
86
- Pipeline::Base.should_receive(:find).and_return(nil)
88
+ Pipeline::Base.should_receive(:find).
89
+ with('1').
90
+ and_raise(ActiveRecord::RecordNotFound.new)
87
91
 
88
92
  lambda {Pipeline.cancel('1')}.should raise_error(InvalidPipelineError, "Invalid pipeline")
89
93
  end
@@ -12,6 +12,34 @@ end
12
12
 
13
13
  class SecondStage < FirstStage; end # Ugly.. just so I don't have to write stub again
14
14
 
15
+ class IrrecoverableStage < FirstStage
16
+ def run
17
+ super
18
+ raise Pipeline::IrrecoverableError.new
19
+ end
20
+ end
21
+
22
+ class RecoverableInputRequiredStage < FirstStage
23
+ def run
24
+ super
25
+ raise Pipeline::RecoverableError.new("message", true)
26
+ end
27
+ end
28
+
29
+ class RecoverableStage < FirstStage
30
+ def run
31
+ super
32
+ raise Pipeline::RecoverableError.new("message")
33
+ end
34
+ end
35
+
36
+ class GenericErrorStage < FirstStage
37
+ def run
38
+ super
39
+ raise StandardError.new
40
+ end
41
+ end
42
+
15
43
  class SamplePipeline < Pipeline::Base
16
44
  define_stages FirstStage >> SecondStage
17
45
  end
@@ -20,9 +48,26 @@ module Pipeline
20
48
  describe Base do
21
49
 
22
50
  describe "- configuring" do
51
+ before(:each) do
52
+ class ::SamplePipeline
53
+ define_stages FirstStage >> SecondStage
54
+ end
55
+ end
56
+
23
57
  it "should allow accessing stages" do
24
58
  SamplePipeline.defined_stages.should == [FirstStage, SecondStage]
25
59
  end
60
+
61
+ it "should allow configuring default failure mode (pause by default)" do
62
+ SamplePipeline.default_failure_mode = :pause
63
+ SamplePipeline.failure_mode.should == :pause
64
+
65
+ SamplePipeline.default_failure_mode = :cancel
66
+ SamplePipeline.failure_mode.should == :cancel
67
+
68
+ SamplePipeline.default_failure_mode = :something_else
69
+ SamplePipeline.failure_mode.should == :pause
70
+ end
26
71
  end
27
72
 
28
73
  describe "- setup" do
@@ -59,6 +104,8 @@ module Pipeline
59
104
 
60
105
  retrieved_pipeline = Base.find(@pipeline.id.to_s)
61
106
  retrieved_pipeline.should === @pipeline
107
+
108
+ lambda {Base.find('invalid_id')}.should raise_error(ActiveRecord::RecordNotFound)
62
109
  end
63
110
 
64
111
  it "should persist type as single table inheritance" do
@@ -104,21 +151,18 @@ module Pipeline
104
151
 
105
152
  describe "- execution (success)" do
106
153
  before(:each) do
107
- @pipeline = SamplePipeline.new
154
+ class ::SamplePipeline
155
+ define_stages FirstStage >> SecondStage
156
+ end
157
+ @pipeline = ::SamplePipeline.new
108
158
  end
109
159
 
110
160
  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
161
  pipeline = SamplePipeline.new
116
162
 
163
+ pipeline.attempts.should == 0
117
164
  pipeline.perform
118
165
  pipeline.attempts.should == 1
119
-
120
- pipeline.perform
121
- pipeline.attempts.should == 2
122
166
  end
123
167
 
124
168
  it "should perform each stage" do
@@ -151,10 +195,10 @@ module Pipeline
151
195
 
152
196
  describe "- execution (irrecoverable error)" do
153
197
  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
198
+ class ::SamplePipeline
199
+ define_stages FirstStage >> IrrecoverableStage
200
+ end
201
+ @pipeline = ::SamplePipeline.new
158
202
  end
159
203
 
160
204
  it "should not re-raise error" do
@@ -175,9 +219,9 @@ module Pipeline
175
219
 
176
220
  describe "- execution (recoverable error that doesn't require user input)" do
177
221
  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)
222
+ class ::SamplePipeline
223
+ define_stages FirstStage >> RecoverableStage
224
+ end
181
225
  @pipeline = SamplePipeline.new
182
226
  end
183
227
 
@@ -199,9 +243,9 @@ module Pipeline
199
243
 
200
244
  describe "- execution (recoverable error that requires user input)" do
201
245
  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)
246
+ class ::SamplePipeline
247
+ define_stages FirstStage >> RecoverableInputRequiredStage
248
+ end
205
249
  @pipeline = SamplePipeline.new
206
250
  end
207
251
 
@@ -221,11 +265,11 @@ module Pipeline
221
265
  end
222
266
  end
223
267
 
224
- describe "- execution (other errors will pause the pipeline)" do
268
+ describe "- execution (other errors will use failure mode to pause/cancel pipeline)" do
225
269
  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)
270
+ class ::SamplePipeline
271
+ define_stages FirstStage >> GenericErrorStage
272
+ end
229
273
  @pipeline = SamplePipeline.new
230
274
  end
231
275
 
@@ -233,26 +277,38 @@ module Pipeline
233
277
  lambda {@pipeline.perform}.should_not raise_error(StandardError)
234
278
  end
235
279
 
236
- it "should update status" do
280
+ it "should update status (pause mode)" do
281
+ SamplePipeline.default_failure_mode = :pause
237
282
  @pipeline.perform
238
283
  @pipeline.status.should == :paused
239
284
  end
240
285
 
241
- it "should save status" do
286
+ it "should save status (pause mode)" do
287
+ SamplePipeline.default_failure_mode = :pause
242
288
  @pipeline.save!
243
289
  @pipeline.perform
244
290
  @pipeline.reload.status.should == :paused
245
291
  end
292
+
293
+ it "should update status (cancel mode)" do
294
+ SamplePipeline.default_failure_mode = :cancel
295
+ @pipeline.perform
296
+ @pipeline.status.should == :failed
297
+ end
298
+
299
+ it "should save status (cancel mode)" do
300
+ SamplePipeline.default_failure_mode = :cancel
301
+ @pipeline.save!
302
+ @pipeline.perform
303
+ @pipeline.reload.status.should == :failed
304
+ end
246
305
  end
247
306
 
248
307
  describe "- execution (retrying)" do
249
308
  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)
309
+ class ::SamplePipeline
310
+ define_stages FirstStage >> RecoverableInputRequiredStage
311
+ end
256
312
  @pipeline = SamplePipeline.new
257
313
  end
258
314
 
@@ -273,12 +329,26 @@ module Pipeline
273
329
 
274
330
  it "should skip completed stages" do
275
331
  @pipeline.perform
276
- @passing_stage.attempts.should == 1
277
- @failed_stage.attempts.should == 1
332
+ @pipeline.stages[0].attempts.should == 1
333
+ @pipeline.stages[1].attempts.should == 1
278
334
 
279
335
  @pipeline.perform
280
- @passing_stage.attempts.should == 1
281
- @failed_stage.attempts.should == 2
336
+ @pipeline.stages[0].attempts.should == 1
337
+ @pipeline.stages[1].attempts.should == 2
338
+ end
339
+
340
+ it "should refresh object (in case it was cancelled after job was scheduled)" do
341
+ # Gets paused on the first time
342
+ @pipeline.save!
343
+ @pipeline.perform
344
+
345
+ # Status gets updated to failed on the database (not on the current instance)
346
+ same_pipeline = SamplePipeline.find(@pipeline.id)
347
+ same_pipeline.send(:status=, :failed)
348
+ same_pipeline.save!
349
+
350
+ # Retrying should fail because pipeline is now failed
351
+ lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already failed")
282
352
  end
283
353
  end
284
354
 
@@ -323,12 +393,9 @@ module Pipeline
323
393
 
324
394
  describe "- cancelling" do
325
395
  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)
396
+ class ::SamplePipeline
397
+ define_stages FirstStage >> RecoverableInputRequiredStage
398
+ end
332
399
  @pipeline = SamplePipeline.new
333
400
  @pipeline.perform
334
401
  end
@@ -76,6 +76,8 @@ module Pipeline
76
76
 
77
77
  s = SampleStage.find(@stage.id)
78
78
  s.should === @stage
79
+
80
+ lambda {Base.find('invalid_id')}.should raise_error(ActiveRecord::RecordNotFound)
79
81
  end
80
82
 
81
83
  it "should persist type as single table inheritance" do
@@ -113,12 +115,9 @@ module Pipeline
113
115
  end
114
116
 
115
117
  it "should increment attempts" do
116
- @stage.stub!(:run).and_raise(StandardError.new)
117
- lambda {@stage.perform}.should raise_error(StandardError)
118
+ @stage.attempts.should == 0
119
+ @stage.perform
118
120
  @stage.attempts.should == 1
119
-
120
- lambda {@stage.perform}.should raise_error(StandardError)
121
- @stage.attempts.should == 2
122
121
  end
123
122
 
124
123
  it "should call template method #run" do
@@ -126,6 +125,7 @@ module Pipeline
126
125
  @stage.perform
127
126
  @stage.should be_executed
128
127
  end
128
+
129
129
  end
130
130
 
131
131
  describe "- execution (failure)" do
@@ -184,6 +184,21 @@ module Pipeline
184
184
  lambda {@stage.perform}.should raise_error
185
185
  end
186
186
 
187
+ it "should refresh object (in case it was cancelled after job was scheduled)" do
188
+ # Gets failed on the first time
189
+ @stage.save!
190
+ @stage.should_receive(:run).and_raise(RecoverableError.new("message"))
191
+ lambda {@stage.perform}.should raise_error(RecoverableError)
192
+
193
+ # Status gets updated to completed on the database (not on the current instance)
194
+ same_stage = SampleStage.find(@stage.id)
195
+ same_stage.send(:status=, :completed)
196
+ same_stage.save!
197
+
198
+ # Retrying should fail because stage is now completed
199
+ lambda {@stage.perform}.should raise_error(InvalidStatusError, "Status is already completed")
200
+ end
201
+
187
202
  end
188
203
 
189
204
  describe "- execution (in progress)" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dtsato-pipeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danilo Sato
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-08-12 00:00:00 -07:00
12
+ date: 2009-08-18 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency