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.
@@ -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