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
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
0.0.1
|
2
|
+
=====
|
3
|
+
|
4
|
+
Initial Release:
|
5
|
+
* Execution of sequential user-defined stages in an asynchronous pipeline
|
6
|
+
* Persistence of pipeline instances and stages
|
7
|
+
* Error recovery strategies:
|
8
|
+
* Irrecoverable errors fail the entire pipeline
|
9
|
+
* Recoverable errors are automatically retried (using dj's exponential retry strategy)
|
10
|
+
* Recoverable errors that require user input pause the pipeline for further retry
|
11
|
+
* Cancelling of a paused pipeline
|
12
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 dtsato.com, Danilo Sato
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
= Pipeline
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Pipeline is a Rails plugin/gem to run asynchronous processes in a configurable pipeline.
|
6
|
+
|
7
|
+
== Features
|
8
|
+
|
9
|
+
* Execution of sequential user-defined stages in an asynchronous pipeline
|
10
|
+
* Persistence of pipeline instances and stages
|
11
|
+
* Error recovery strategies:
|
12
|
+
* Irrecoverable errors fail the entire pipeline
|
13
|
+
* Recoverable errors are automatically retried (using dj's exponential retry strategy)
|
14
|
+
* Recoverable errors that require user input pause the pipeline for further retry
|
15
|
+
* Cancelling of a paused pipeline
|
16
|
+
|
17
|
+
== Installation
|
18
|
+
|
19
|
+
Add the following lines to your config/environment.rb file:
|
20
|
+
|
21
|
+
config.gem "dtsato-pipeline", :version => ">= 0.0.1", :source => "http://gems.github.com"
|
22
|
+
|
23
|
+
Run the following:
|
24
|
+
|
25
|
+
rake gems:install
|
26
|
+
rake gems:unpack # Optional, if you want to vendor the gem
|
27
|
+
script/generate pipeline # To generate the migration scripts that will store pipelines
|
28
|
+
rake db:migrate
|
29
|
+
|
30
|
+
You will also need to run your Delayed Job workers that will process the pipeline jobs in the background:
|
31
|
+
|
32
|
+
rake jobs:work
|
33
|
+
|
34
|
+
== Dependencies
|
35
|
+
|
36
|
+
* Rails
|
37
|
+
* Delayed job (http://github.com/collectiveidea/delayed_job/tree/master)
|
38
|
+
|
39
|
+
== Usage
|
40
|
+
|
41
|
+
Check <tt>examples</tt> for more examples (including error-recovery and cancelling)
|
42
|
+
|
43
|
+
class Step1 < Pipeline::Stage::Base
|
44
|
+
def perform
|
45
|
+
puts("Started step 1")
|
46
|
+
sleep 2
|
47
|
+
puts("Finished step 1")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Step2 < Pipeline::Stage::Base
|
52
|
+
def perform
|
53
|
+
puts("Started step 2")
|
54
|
+
sleep 3
|
55
|
+
puts("Finished step 2")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class TwoStepPipeline < Pipeline::Base
|
60
|
+
define_stages Step1 >> Step2
|
61
|
+
end
|
62
|
+
|
63
|
+
Pipeline.start(TwoStepPipeline.new)
|
64
|
+
|
65
|
+
== Copyright
|
66
|
+
|
67
|
+
Copyright (c) 2009 Danilo Sato. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "pipeline"
|
8
|
+
gem.summary = "A Rails plugin/gem to run asynchronous processes in a configurable pipeline"
|
9
|
+
gem.email = "danilo@dtsato.com"
|
10
|
+
gem.homepage = "http://github.com/dtsato/pipeline"
|
11
|
+
gem.authors = ["Danilo Sato"]
|
12
|
+
gem.description = "Pipeline is a Rails plugin/gem to run asynchronous processes in a configurable pipeline."
|
13
|
+
|
14
|
+
gem.has_rdoc = true
|
15
|
+
gem.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
|
16
|
+
gem.extra_rdoc_files = ["README.rdoc"]
|
17
|
+
|
18
|
+
gem.test_files = Dir['spec/**/*']
|
19
|
+
|
20
|
+
gem.add_dependency('activerecord', '>= 2.0')
|
21
|
+
gem.add_dependency('collectiveidea-delayed_job', '>= 1.8.0')
|
22
|
+
|
23
|
+
gem.rubyforge_project = "pipeline"
|
24
|
+
end
|
25
|
+
|
26
|
+
rescue LoadError
|
27
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
28
|
+
end
|
29
|
+
|
30
|
+
# These are new tasks
|
31
|
+
begin
|
32
|
+
require 'rake/contrib/sshpublisher'
|
33
|
+
namespace :rubyforge do
|
34
|
+
|
35
|
+
desc "Release gem and RDoc documentation to RubyForge"
|
36
|
+
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
37
|
+
|
38
|
+
namespace :release do
|
39
|
+
desc "Publish RDoc to RubyForge."
|
40
|
+
task :docs => [:rdoc] do
|
41
|
+
config = YAML.load(
|
42
|
+
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
43
|
+
)
|
44
|
+
|
45
|
+
host = "#{config['username']}@rubyforge.org"
|
46
|
+
remote_dir = "/var/www/gforge-projects/pipeline/"
|
47
|
+
local_dir = 'rdoc'
|
48
|
+
|
49
|
+
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue LoadError
|
54
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
55
|
+
end
|
56
|
+
|
57
|
+
require 'spec/rake/spectask'
|
58
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
59
|
+
spec.libs << 'lib' << 'spec'
|
60
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
61
|
+
spec.spec_opts = ['--options', "\"spec/spec.opts\""]
|
62
|
+
end
|
63
|
+
|
64
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
65
|
+
spec.libs << 'lib' << 'spec'
|
66
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
67
|
+
spec.rcov_opts = lambda do
|
68
|
+
IO.readlines("spec/rcov.opts").map {|l| l.chomp.split " "}.flatten
|
69
|
+
end
|
70
|
+
spec.rcov = true
|
71
|
+
end
|
72
|
+
|
73
|
+
begin
|
74
|
+
require "synthesis/task"
|
75
|
+
|
76
|
+
desc "Run Synthesis on specs"
|
77
|
+
Synthesis::Task.new("spec:synthesis") do |t|
|
78
|
+
t.adapter = :rspec
|
79
|
+
t.pattern = 'spec/**/*_spec.rb'
|
80
|
+
t.ignored = ['Pipeline::FakePipeline', 'Delayed::Job']
|
81
|
+
end
|
82
|
+
rescue LoadError
|
83
|
+
desc 'Synthesis rake task not available'
|
84
|
+
task "spec:synthesis" do
|
85
|
+
abort 'Synthesis rake task is not available. Be sure to install synthesis as a gem'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
require 'rake/rdoctask'
|
90
|
+
Rake::RDocTask.new do |rdoc|
|
91
|
+
if File.exist?('VERSION.yml')
|
92
|
+
config = YAML.load(File.read('VERSION.yml'))
|
93
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
94
|
+
else
|
95
|
+
version = ""
|
96
|
+
end
|
97
|
+
|
98
|
+
rdoc.rdoc_dir = 'rdoc'
|
99
|
+
rdoc.title = "pipeline #{version}"
|
100
|
+
rdoc.rdoc_files.include('README*')
|
101
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
102
|
+
end
|
103
|
+
|
104
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'init')
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'spec', 'database_integration_helper')
|
3
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
4
|
+
|
5
|
+
class Step1 < Pipeline::Stage::Base
|
6
|
+
def run
|
7
|
+
puts("Started step 1")
|
8
|
+
# Will fail on the first time, but pass on the second
|
9
|
+
if attempts == 1
|
10
|
+
puts("Raising auto-recoverable error")
|
11
|
+
raise Pipeline::RecoverableError.new
|
12
|
+
end
|
13
|
+
puts("Finished step 1")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Step2 < Pipeline::Stage::Base
|
18
|
+
def run
|
19
|
+
puts("Started step 2")
|
20
|
+
# Will fail on the first time, but pass on the second
|
21
|
+
if attempts == 1
|
22
|
+
puts("Raising another auto-recoverable error")
|
23
|
+
raise Pipeline::RecoverableError.new
|
24
|
+
end
|
25
|
+
puts("Finished step 2")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class TwoStepPipeline < Pipeline::Base
|
30
|
+
define_stages Step1 >> Step2
|
31
|
+
end
|
32
|
+
|
33
|
+
Pipeline.start(TwoStepPipeline.new)
|
34
|
+
|
35
|
+
Delayed::Worker.new.start
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'init')
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'spec', 'database_integration_helper')
|
3
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
4
|
+
|
5
|
+
class Step1 < Pipeline::Stage::Base
|
6
|
+
def run
|
7
|
+
puts("Started step 1")
|
8
|
+
puts("Raising user-recoverable error")
|
9
|
+
raise Pipeline::RecoverableError.new("require your action", true)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Step2 < Pipeline::Stage::Base
|
14
|
+
def run
|
15
|
+
puts("Started step 2")
|
16
|
+
sleep 3
|
17
|
+
puts("Finished step 2")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class TwoStepPipeline < Pipeline::Base
|
22
|
+
define_stages Step1 >> Step2
|
23
|
+
end
|
24
|
+
|
25
|
+
id = Pipeline.start(TwoStepPipeline.new)
|
26
|
+
|
27
|
+
Delayed::Worker.new.start
|
28
|
+
|
29
|
+
# CTRL-C to execute the cancelling, since we want to cancel after the stage failed, but
|
30
|
+
# Worker is blocking the process on the previous line
|
31
|
+
Pipeline.cancel(id)
|
32
|
+
p Pipeline::Base.find(id)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'init')
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'spec', 'database_integration_helper')
|
3
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
4
|
+
|
5
|
+
class Step1 < Pipeline::Stage::Base
|
6
|
+
def run
|
7
|
+
puts("Started step 1")
|
8
|
+
sleep 2
|
9
|
+
puts("Finished step 1")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Step2 < Pipeline::Stage::Base
|
14
|
+
def run
|
15
|
+
puts("Started step 2")
|
16
|
+
sleep 3
|
17
|
+
puts("Finished step 2")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class TwoStepPipeline < Pipeline::Base
|
22
|
+
define_stages Step1 >> Step2
|
23
|
+
end
|
24
|
+
|
25
|
+
Pipeline.start(TwoStepPipeline.new)
|
26
|
+
|
27
|
+
Delayed::Worker.new.start
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'init')
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'spec', 'database_integration_helper')
|
3
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
4
|
+
|
5
|
+
class Step1 < Pipeline::Stage::Base
|
6
|
+
def run
|
7
|
+
puts("Started step 1")
|
8
|
+
sleep 2
|
9
|
+
puts("Finished step 1")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Step2 < Pipeline::Stage::Base
|
14
|
+
def run
|
15
|
+
puts("Started step 2")
|
16
|
+
# Will fail on the first time, but pass on the second
|
17
|
+
if attempts == 1
|
18
|
+
puts("Raising user-recoverable error")
|
19
|
+
raise Pipeline::RecoverableError.new("require your action", true)
|
20
|
+
end
|
21
|
+
puts("Finished step 2")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class TwoStepPipeline < Pipeline::Base
|
26
|
+
define_stages Step1 >> Step2
|
27
|
+
end
|
28
|
+
|
29
|
+
id = Pipeline.start(TwoStepPipeline.new)
|
30
|
+
|
31
|
+
Pipeline.resume(id)
|
32
|
+
|
33
|
+
Delayed::Worker.new.start
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class PipelineGenerator < Rails::Generator::Base
|
2
|
+
|
3
|
+
def manifest
|
4
|
+
record do |m|
|
5
|
+
m.migration_template "pipeline_instances_migration.rb", 'db/migrate',
|
6
|
+
:migration_file_name => "create_pipeline_instances"
|
7
|
+
m.migration_template "pipeline_stages_migration.rb", 'db/migrate',
|
8
|
+
:migration_file_name => "create_pipeline_stages"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreatePipelineInstances < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :pipeline_instances, :force => true do |t|
|
4
|
+
t.string :type # For single table inheritance
|
5
|
+
t.string :status # Current status of the pipeline
|
6
|
+
t.integer :attempts, :default => 0 # Number of times this pipeline was executed
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
drop_table :pipeline_instances
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreatePipelineStages < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :pipeline_stages, :force => true do |t|
|
4
|
+
t.references :pipeline_instance # Pipeline that holds this stage
|
5
|
+
t.string :type # For single table inheritance
|
6
|
+
t.string :name # Name of the stage
|
7
|
+
t.string :status # Current status of the stage
|
8
|
+
t.text :message # Message that describes current status
|
9
|
+
t.integer :attempts, :default => 0 # Number of times this stage was executed
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.down
|
17
|
+
drop_table :pipeline_stages
|
18
|
+
end
|
19
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'lib', 'pipeline')
|
data/lib/pipeline.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'activerecord'
|
3
|
+
gem 'collectiveidea-delayed_job'
|
4
|
+
autoload :Delayed, 'delayed_job'
|
5
|
+
|
6
|
+
$: << File.dirname(__FILE__)
|
7
|
+
require 'pipeline/core_ext/symbol_attribute'
|
8
|
+
require 'pipeline/core_ext/transactional_attribute'
|
9
|
+
require 'pipeline/api_methods'
|
10
|
+
require 'pipeline/base'
|
11
|
+
require 'pipeline/errors'
|
12
|
+
require 'pipeline/stage/base'
|
13
|
+
|
14
|
+
module Pipeline
|
15
|
+
extend(ApiMethods)
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Pipeline
|
2
|
+
module ApiMethods
|
3
|
+
def start(pipeline)
|
4
|
+
raise InvalidPipelineError.new("Invalid pipeline") unless pipeline.is_a?(Pipeline::Base)
|
5
|
+
pipeline.save!
|
6
|
+
Delayed::Job.enqueue(pipeline)
|
7
|
+
pipeline.id
|
8
|
+
end
|
9
|
+
|
10
|
+
def resume(id)
|
11
|
+
pipeline = Base.find(id)
|
12
|
+
raise InvalidPipelineError.new("Invalid pipeline") unless pipeline
|
13
|
+
raise InvalidStatusError.new(pipeline.status) unless pipeline.ok_to_resume?
|
14
|
+
Delayed::Job.enqueue(pipeline)
|
15
|
+
end
|
16
|
+
|
17
|
+
def cancel(id)
|
18
|
+
pipeline = Base.find(id)
|
19
|
+
raise InvalidPipelineError.new("Invalid pipeline") unless pipeline
|
20
|
+
pipeline.cancel
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|