pipeline 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ pipeline.log
7
+ pipeline.sqlite
@@ -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.
@@ -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.
@@ -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')
@@ -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