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 +13 -0
- data/Rakefile +1 -1
- data/TODO +1 -0
- data/VERSION +1 -1
- data/lib/pipeline/api_methods.rb +4 -2
- data/lib/pipeline/base.rb +10 -1
- data/lib/pipeline/stage/base.rb +1 -0
- data/pipeline.gemspec +2 -2
- data/spec/pipeline/api_methods_spec.rb +7 -3
- data/spec/pipeline/base_spec.rb +107 -40
- data/spec/pipeline/stage/base_spec.rb +20 -5
- metadata +2 -2
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.
|
1
|
+
0.0.7
|
data/lib/pipeline/api_methods.rb
CHANGED
@@ -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
|
|
data/lib/pipeline/stage/base.rb
CHANGED
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.
|
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-
|
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
|
54
|
-
Pipeline::Base.should_receive(:find).
|
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).
|
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
|
data/spec/pipeline/base_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
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
|
268
|
+
describe "- execution (other errors will use failure mode to pause/cancel pipeline)" do
|
225
269
|
before(:each) do
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
251
|
-
|
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
|
-
@
|
277
|
-
@
|
332
|
+
@pipeline.stages[0].attempts.should == 1
|
333
|
+
@pipeline.stages[1].attempts.should == 1
|
278
334
|
|
279
335
|
@pipeline.perform
|
280
|
-
@
|
281
|
-
@
|
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
|
-
|
327
|
-
|
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.
|
117
|
-
|
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: pipeline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
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
|
+
date: 2009-08-18 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|