orchestrated 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in orchestrated.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Paydici Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,133 @@
1
+ Orchestrated
2
+ ============
3
+
4
+ The [delayed_job](https://github.com/collectiveidea/delayed_job) Ruby Gem provides a job queuing system for Ruby. It implements an elegant API for delaying execution of any object method. Not only is the execution of the method (message delivery) delayed in time, it is potentially shifted in space. By shifting in space, i.e. running in a separate virtual machine, possibly on a separate computer, multiple CPUs can be brought to bear on a computing problem.
5
+
6
+ By breaking up otherwise serial execution into multiple queued jobs, a program can be made more scalable. Processing of (distributed) queues has a long and successful history in data processing for this reason.
7
+
8
+ Queuing works well for simple tasks. By simple I mean, the task can be done all at once, in one piece. It has no dependencies on other tasks. This works well for performing a file upload task in the background (to avoid tying up a Ruby virtual machine process/thread). More complex (compound) multi-part tasks, however, do not fit this model. Examples of complex (compound) tasks include:
9
+
10
+ 1. pipelined (multi-step) generation of complex PDF documents
11
+ 2. extract/transfer/load (ETL) jobs that may load thousands of database records
12
+
13
+ If we would like to scale these compound operations, breaking them into smaller parts, and managing the execution of those parts across many computers, we need an "orchestrator". This project implements just such a framework, called "Orchestrated".
14
+
15
+ Installation
16
+ ------------
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'orchestrated'
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install orchestrated
29
+
30
+ The API
31
+ -------
32
+
33
+ To orchestrate (methods) on your own classes you simply call ```acts_as_orchestrated``` in the class definition like this:
34
+
35
+ ```ruby
36
+ class StatementGenerator
37
+
38
+ acts_as_orchestrated
39
+
40
+ def generate(statement_id)
41
+ ...
42
+ end
43
+
44
+ def render(statement_id)
45
+ ...
46
+ end
47
+
48
+ end
49
+ ```
50
+
51
+ Declaring ```acts_as_orchestrated``` on your class gives it two methods:
52
+
53
+ * ```orchestrated```—call this to specify your workflow prerequisite, and designate a workflow step
54
+ * ```orchestration```—call this in the context of a workflow step (execution) to access orchestration (and prerequisite) context
55
+
56
+ After that you can orchestrate any method on such a class e.g.
57
+
58
+ ```ruby
59
+ gen = StatementGenerator.new
60
+ gen.orchestrated( orchestrated.generate(stmt_id) ).render(stmt_id)
61
+ ```
62
+
63
+ The next time you process a delayed job, the :generate message will be delivered. The time after that, the :render message will be delivered.
64
+
65
+ What happened there? The pattern is:
66
+
67
+ 1. create an orchestrated object (instantiate it)
68
+ 2. call orchestrated on it: this returns an "orchestration"
69
+ 3. send a message to the orchestration (returned in the second step)
70
+
71
+ Now the messages you can send in (3) are limited to the messages that your object can respond to. The message will be "remembered" by the framework and "replayed" (on a new instance of your object) somewhere on the network (later).
72
+
73
+ Not accidentally, this is similar to the way [delayed_job](https://github.com/collectiveidea/delayed_job)'s delay method works. Under the covers, orchestrated is conspiring with [delayed_job](https://github.com/collectiveidea/delayed_job) when it comes time to actually execute a workflow step. Before that time though, orchestrated keeps track of everything.
74
+
75
+ Key Concept: Prerequisites (Completion Expressions)
76
+ ---------------------------------------------------
77
+
78
+ Unlike [delayed_job](https://github.com/collectiveidea/delayed_job) ```delay```, the orchestrated ```orchestrated``` method takes an optional parameter: the prerequisite. The prerequisite determines when your workflow step is ready to run.
79
+
80
+ The return value from "orchestrate" is itself a ready-to-use prerequisite. You saw this in the statement generation example above. The result of the first ```orchestrated``` call was sent as an argument to the second. In this way, the second workflow step was suspended until after the first one finished. You may have also noticed from that example that if you specify no prerequisite then the step will be ready to run immediately, as was the case for the "generate" call).
81
+
82
+ There are five kinds of prerequisite in all. Some of them are used for combining others. The prerequisites types, also known as "completion expressions" are:
83
+
84
+ 1. ```OrchestrationCompletion```—returned by "orchestrate", complete when its associated orchestration is complete
85
+ 2. ```Complete```—always complete
86
+ 3. ```FirstCompletion```—aggregates other completions: complete after the first one completes
87
+ 4. ```LastCompletion```—aggregates other completions: complete after all of them are complete
88
+
89
+ See the completion_spec for examples of how to combine these different prerequisite types into completion expressions.
90
+
91
+ Key Concept: Orchestration State
92
+ --------------------------------
93
+
94
+ An orchestration can be in one of six (6) states:
95
+
96
+ ![Alt text](https://github.com/paydici/orchestrated/raw/master/Orchestrated::Orchestration_state.png 'Orchestration States')
97
+
98
+ You'll never see an orchestration in the "new" state, it's for internal use in the framework. But all the others are interesting.
99
+
100
+ When you create a new orchestration that is waiting on a prerequisite that is not complete yet, the orchestration will be in the "waiting" state. Some time later, if that prerequisite completes, then your orchestration will become "ready". A "ready" orchestration is automatically queued to run by the framework (via [delayed_job](https://github.com/collectiveidea/delayed_job)).
101
+
102
+ A "ready" orchestration will use [delayed_job](https://github.com/collectiveidea/delayed_job) to delivery its (delayed) message. In the context of such a message delivery (inside your object method e.g. StatementGenerator#generate or StatementGenerator#render) you can rely on the ability to access the current Orchestration (context) object via the "orchestration" accessor.
103
+
104
+ After your workflow step executes, the orchestration moves into either the "succeeded" or "failed" state.
105
+
106
+ When an orchestration is "ready" or "waiting" it may be canceled by sending it the ```cancel!``` message. This moves it to the "canceled" state and prevents delivery of the orchestrated message (in the future).
107
+
108
+ It is important to understand that both of the states: "succeeded" and "failed" are part of a "super-state": "complete". When an orchestration is in either of those two states, it will return ```true``` in response to the ```complete?``` message.
109
+
110
+ It is not just successful completion of orchestrated methods that causes dependent ones to run—a "failed" orchestration is complete too! If you have an orchestration that actually requires successful completion of its prerequisite then it can inspect the prerequisite as needed. It's accessible through the ```orchestration`` accessor (on the orchestrated object).
111
+
112
+ Failure (An Option)
113
+ -------------------
114
+
115
+ Orchestration is built atop [delayed_job](https://github.com/collectiveidea/delayed_job) and borrows [delayed_job](https://github.com/collectiveidea/delayed_job)'s failure semantics. Neither framework imposes any special constraints on the (delayed or orchestrated) methods. In particular, there are no special return values to signal "failure". Orchestration adopts [delayed_job](https://github.com/collectiveidea/delayed_job)'s semantics for failure detection: a method that raises an exception has failed. After a certain number of retries (configurable in [delayed_job](https://github.com/collectiveidea/delayed_job)) the jobs is deemed permanently failed. When that happens, the corresponding orchestration is marked "failed".
116
+
117
+ See the failure_spec if you'd like to understand more.
118
+
119
+ Cancelling an Orchestration
120
+ ---------------------------
121
+
122
+ An orchestration can be canceled by sending the (orchestration completion) the ```cancel!``` message. This will prevent the orchestrated method from running (in the future). It will also cancel dependent workflow steps.
123
+
124
+ The cancellation_spec spells out more of the details.
125
+
126
+ Contributing
127
+ ------------
128
+
129
+ 1. Fork it
130
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
131
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
132
+ 4. Push to the branch (`git push origin my-new-feature`)
133
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,41 @@
1
+ module Orchestrated
2
+
3
+ class Proxy
4
+ def initialize(prerequisite, target)
5
+ @prerequisite = prerequisite
6
+ @target = target
7
+ end
8
+ def method_missing(sym, *args)
9
+ raise 'cannot orchestrate with blocks because they are not portable across processes' if block_given?
10
+ OrchestrationCompletion.new do |completion|
11
+ completion.orchestration = Orchestration.create( @target, sym, args, @prerequisite)
12
+ end.tap do |completion|
13
+ completion.save!
14
+ end
15
+ end
16
+ end
17
+
18
+ class << self
19
+ #snarfed from Ruby On Rails
20
+ def underscore(camel_cased_word)
21
+ camel_cased_word.to_s.gsub(/::/, '/').
22
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
23
+ tr("-", "_").
24
+ downcase
25
+ end
26
+ def belongs_to clazz
27
+ # borrowed from Ick
28
+ method_name = self.underscore(self.name.split('::')[-1])
29
+ unless clazz.method_defined?(method_name)
30
+ clazz.class_eval "
31
+ def #{method_name}(prerequisite=Complete.new)
32
+ raise 'orchestrate does not take a block' if block_given?
33
+ raise %[cannot use \#{prerequisite.class.name} as a prerequisite] unless
34
+ prerequisite.kind_of?(CompletionExpression)
35
+ Proxy.new(prerequisite, self)
36
+ end"
37
+ end
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,72 @@
1
+ require 'active_record'
2
+
3
+ module Orchestrated
4
+ # a little ditty to support the completion algebra
5
+ # a composite!
6
+ # Completion is used as a prerequisite (prerequisites) for message passing
7
+ class CompletionExpression < ActiveRecord::Base
8
+ # I'd like to make this abstract, but Rails gets confused if I do
9
+ # self.abstract_class = true
10
+ def complete?; throw 'subclass must override!';end
11
+ # for static analysis
12
+ def always_complete?; throw 'subclass must override!';end
13
+ def never_complete?; throw 'subclass must override!';end
14
+ def canceled?; throw 'subclass must override!';end
15
+ end
16
+ class Complete < CompletionExpression
17
+ def complete?; true; end
18
+ def always_complete?; true; end
19
+ def never_complete?; false; end
20
+ def canceled?; false; end
21
+ end
22
+ # Only known use is in testing the framework
23
+ class Incomplete < CompletionExpression
24
+ def complete?; false; end
25
+ def always_complete?; false; end
26
+ def never_complete?; true; end
27
+ def canceled?; false; end
28
+ end
29
+ class CompositeCompletion < CompletionExpression
30
+ # self.abstract_class = true
31
+ has_many :composited_completions
32
+ has_many :completion_expressions, :through => :composited_completions, :source => :completion_expression
33
+ def +(c); self << c; end # synonym
34
+ end
35
+ class LastCompletion < CompositeCompletion
36
+ def complete?; completion_expressions.all?(&:complete?); end
37
+ def always_complete?; completion_expressions.empty?; end
38
+ def never_complete?; completion_expressions.any?(&:never_complete?); end
39
+ def canceled?; completion_expressions.any?(&:canceled?); end
40
+ def <<(c)
41
+ completion_expressions << c unless c.always_complete?
42
+ self
43
+ end
44
+ end
45
+ class FirstCompletion < CompositeCompletion
46
+ def complete?; completion_expressions.any?(&:complete?); end
47
+ def always_complete?; completion_expressions.any?(&:always_complete?); end
48
+ def never_complete?; completion_expressions.empty?; end
49
+ def canceled?; completion_expressions.all?(&:canceled?); end
50
+ def <<(c)
51
+ completion_expressions << c unless c.never_complete?
52
+ self
53
+ end
54
+ end
55
+ class OrchestrationCompletion < CompletionExpression
56
+ # Arguably, it is "bad" to make this class derive
57
+ # from CompletionExpression since doing so introduces
58
+ # the orchestration_id into the table (that constitutes
59
+ # denormalization since no other types need that field).
60
+ # The alternative is that we have to do difficult-to-
61
+ # understand joins when computing dependents at runtime.
62
+ belongs_to :orchestration
63
+ validates_presence_of :orchestration_id
64
+ delegate :complete?, :canceled?, :cancel!, :to => :orchestration
65
+ def always_complete?; false; end
66
+ def never_complete?; false; end
67
+ end
68
+ class CompositedCompletion < ActiveRecord::Base
69
+ belongs_to :composite_completion
70
+ belongs_to :completion_expression
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ module Orchestrated
2
+ class MessageDelivery
3
+ attr_accessor :orchestrated, :method_name, :args, :orchestration_id
4
+
5
+ def initialize(orchestrated, method_name, args, orchestration_id)
6
+ raise 'all arguments to MessageDelivery constructor are required' unless
7
+ orchestrated and method_name and args and orchestration_id
8
+ self.orchestrated = orchestrated
9
+ self.method_name = method_name
10
+ self.args = args
11
+ self.orchestration_id = orchestration_id
12
+ end
13
+
14
+ def perform
15
+ orchestration = Orchestration.find(self.orchestration_id)
16
+
17
+ orchestrated.orchestration = orchestration
18
+ orchestrated.send(method_name, *args)
19
+ orchestrated.orchestration = nil
20
+
21
+ orchestration.message_delivery_succeeded
22
+ end
23
+
24
+ # delayed_job hands us this message after max_attempts are exhausted
25
+ def failure
26
+ orchestration = Orchestration.find(self.orchestration_id)
27
+ orchestration.message_delivery_failed
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ module Orchestrated
2
+ module InstanceMethods
3
+ # set by the framework (Orchestration) before
4
+ # an orchestrated method is called
5
+ # cleared (nil) outside such a call
6
+ attr_accessor :orchestration
7
+ end
8
+ class ::Object
9
+ class << self
10
+ def acts_as_orchestrated
11
+ Orchestrated.belongs_to self # define "orchestrated instance method"
12
+ include InstanceMethods
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,112 @@
1
+ require 'active_record'
2
+ require 'state_machine'
3
+ require 'delayed_job'
4
+ require 'delayed_job_active_record'
5
+
6
+ module Orchestrated
7
+ class Orchestration < ActiveRecord::Base
8
+
9
+ Handler = Struct.new('Handler', :value, :sym, :args)
10
+
11
+ serialize :handler
12
+
13
+ belongs_to :prerequisite, :class_name => 'CompletionExpression'
14
+ belongs_to :delayed_job, :polymorphic => true # loose-ish coupling with delayed_job
15
+
16
+ has_many :orchestration_completions
17
+
18
+ complete_states = [:succeeded, :failed]
19
+ state_machine :initial => :new do
20
+ state :new
21
+ state :waiting
22
+ state :ready
23
+ state :succeeded
24
+ state :failed
25
+ state :canceled
26
+
27
+ state all - complete_states do
28
+ def complete?
29
+ false
30
+ end
31
+ end
32
+
33
+ state *complete_states do
34
+ def complete?
35
+ true
36
+ end
37
+ end
38
+
39
+ event :prerequisite_changed do
40
+ transition [:new, :waiting] => :ready, :if => lambda {|orchestration| orchestration.prerequisite.complete?}
41
+ transition [:ready, :waiting] => :canceled, :if => lambda {|orchestration| orchestration.prerequisite.canceled?}
42
+ transition :new => :waiting # otherwise
43
+ end
44
+
45
+ event :message_delivery_succeeded do
46
+ transition :ready => :succeeded
47
+ end
48
+
49
+ event :message_delivery_failed do
50
+ transition :ready => :failed
51
+ end
52
+
53
+ event :cancel do
54
+ transition [:waiting, :ready] => :canceled
55
+ end
56
+
57
+ after_transition any => :ready do |orchestration, transition|
58
+ orchestration.enqueue
59
+ end
60
+
61
+ after_transition :ready => :canceled do |orchestration, transition|
62
+ orchestration.dequeue
63
+ end
64
+
65
+ after_transition any => complete_states do |orchestration, transition|
66
+ # completion may make other orchestrations ready to run…
67
+ # TODO: this is a prime target for benchmarking
68
+ (Orchestration.with_state('waiting').all - [orchestration]).each do |other|
69
+ other.prerequisite_changed
70
+ end
71
+ end
72
+
73
+ after_transition [:ready, :waiting] => :canceled do |orchestration, transition|
74
+ # cancellation may cancel other orchestrations
75
+ (Orchestration.with_states(:ready, :waiting).all - [orchestration]).each do |other|
76
+ other.prerequisite_changed
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ def self.create( value, sym, args, prerequisite)
83
+ # set prerequisite in new call so it is passed to state_machine :initial proc
84
+ new.tap do |orchestration|
85
+
86
+ orchestration.handler = Handler.new( value, sym, args)
87
+
88
+ # wee! static analysis FTW!
89
+ raise 'prerequisite can never be complete' if prerequisite.never_complete?
90
+
91
+ # saves object as side effect of this assignment
92
+ # also moves orchestration to :ready state
93
+ orchestration.prerequisite = prerequisite
94
+ end
95
+ end
96
+
97
+ def enqueue
98
+ self.delayed_job = Delayed::Job.enqueue( MessageDelivery.new( handler.value, handler.sym, handler.args, self.id) )
99
+ end
100
+
101
+ def dequeue
102
+ delayed_job.destroy# if DelayedJob.exists?(delayed_job_id)
103
+ end
104
+
105
+ alias_method :prerequisite_old_equals, :prerequisite=
106
+ def prerequisite=(*args)
107
+ prerequisite_old_equals(*args)
108
+ prerequisite_changed
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,3 @@
1
+ module Orchestrated
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "orchestrated/version"
2
+ require 'orchestrated/base'
3
+ require 'orchestrated/completion'
4
+ require 'orchestrated/message_delivery'
5
+ require 'orchestrated/orchestration'
6
+ require 'orchestrated/object'
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'orchestrated/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "orchestrated"
8
+ gem.version = Orchestrated::VERSION
9
+ gem.authors = ["Bill Burcham"]
10
+ gem.email = ["bill@paydici.com"]
11
+ gem.description = %q{a workflow orchestration framework running on delayed_job and active_record}
12
+ gem.summary = %q{Orchestrated is a workflow orchestration framework running on delayed_job and active_record. In the style of delayed_job's 'delay', Orchestration lets you 'orchestrate' delivery of a message so that it will run only after others have been delivered and processed.}
13
+ gem.homepage = "https://github.com/paydici/orchestration"
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_runtime_dependency 'delayed_job_active_record', '~> 0.3'
22
+ gem.add_runtime_dependency 'activerecord', ['>= 3']
23
+ gem.add_runtime_dependency 'state_machine', ['>= 1']
24
+
25
+ gem.add_development_dependency 'rake'
26
+ gem.add_development_dependency 'rails', ['>= 3'] # for rspec-rails
27
+ gem.add_development_dependency 'rspec-rails'
28
+ # I couldn't get rspecs transactional fixtures setting to do savepoints
29
+ # in this project (which is not _really_ a Rails app). database_cleaner
30
+ # claims it'll help us clean up the database so let's try it!
31
+ gem.add_development_dependency 'database_cleaner'
32
+ gem.add_development_dependency 'sqlite3'
33
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,17 @@
1
+ # SQLite version 3.x
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem 'sqlite3'
6
+ development:
7
+ adapter: sqlite3
8
+ database: db/development.sqlite3
9
+ pool: 5
10
+ timeout: 5000
11
+
12
+ # Warning: The database defined as "test" will be erased and
13
+ # re-generated from your development database when you run "rake".
14
+ # Do not set this db to the same as development or production.
15
+ test:
16
+ adapter: sqlite3
17
+ database: ":memory:"
@@ -0,0 +1,28 @@
1
+ require 'delayed_job_active_record'
2
+
3
+ # facade for controlling delayed_job
4
+ module DJ
5
+ module_function
6
+
7
+ def job_count
8
+ Delayed::Job.count
9
+ end
10
+ def work(num=100)
11
+ Delayed::Worker.new.work_off(num)
12
+ end
13
+ def work_now(num=100)
14
+ (1..num).each do
15
+ first = Delayed::Job.first
16
+ break unless first.present?
17
+ first.tap{|job| job.run_at = 1.second.ago; job.save!}
18
+ DJ.work(1)
19
+ end
20
+ end
21
+ def clear_all_jobs
22
+ Delayed::Job.delete_all
23
+ end
24
+ def max_attempts
25
+ # configured in initializers/delayed_job_config.rb
26
+ Delayed::Worker.max_attempts
27
+ end
28
+ end
@@ -0,0 +1,110 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_group "Orchestrated", "lib/orchestrated"
4
+ end
5
+
6
+ # Get Rails environment going. Borrowed from delayed_job_active_record project, then heavily modified
7
+ # ...
8
+
9
+ $:.unshift(File.join( File.dirname(__FILE__), '../lib'))
10
+
11
+ require 'rubygems'
12
+ require 'bundler/setup'
13
+
14
+ require 'rails/all' # rspec/rails needs Rails
15
+ require 'rspec/rails' # we want transactional fixtures!
16
+
17
+ require 'logger'
18
+
19
+ require 'delayed_job'
20
+ require 'rails'
21
+
22
+ Delayed::Worker.logger = Logger.new('/tmp/dj.log')
23
+ ENV['RAILS_ENV'] = 'test'
24
+
25
+ config = YAML.load(File.read('spec/database.yml'))
26
+ ActiveRecord::Base.establish_connection config['test']
27
+ ActiveRecord::Base.logger = Delayed::Worker.logger
28
+ ActiveRecord::Migration.verbose = false
29
+
30
+ ActiveRecord::Schema.define do
31
+ create_table :delayed_jobs, :force => true do |table|
32
+ table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue
33
+ table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
34
+ table.text :handler # YAML-encoded string of the object that will do work
35
+ table.text :last_error # reason for last failure (See Note below)
36
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
37
+ table.datetime :locked_at # Set when a client is working on this object
38
+ table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
39
+ table.string :locked_by # Who is working on this object (if locked)
40
+ table.string :queue # The name of the queue this job is in
41
+ table.timestamps
42
+ end
43
+
44
+ add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
45
+
46
+ create_table :orchestrations do |table|
47
+ table.string :state
48
+ table.text :handler
49
+ table.references :prerequisite
50
+ table.references :delayed_job, :polymorphic => true
51
+ table.timestamps
52
+ end
53
+ create_table :completion_expressions do |table|
54
+ table.string :type
55
+ # only one kind of completion expression needs this
56
+ # (OrchestrationCompletion) but I didn't want to put
57
+ # it in a separate table because it would really contort
58
+ # the Rails model
59
+ table.references :orchestration
60
+ end
61
+ create_table :composited_completions do |table|
62
+ table.references :composite_completion
63
+ table.references :completion_expression
64
+ end
65
+ end
66
+
67
+ # Add this directory so the ActiveSupport autoloading works
68
+ ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
69
+
70
+ # when we run via plain old "ruby" command instead of "rspec", this
71
+ # line tells ruby to run the examples
72
+ require 'rspec/autorun'
73
+
74
+ # This is the present Ruby Gem: the one we are spec-ing/testing
75
+ require 'orchestrated'
76
+
77
+ # Requires supporting ruby files with custom matchers and macros, etc,
78
+ # in spec/support/ and its subdirectories.
79
+ Dir[File.join( File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
80
+ require 'delayed_job_facade'
81
+ require 'spec_helper_methods'
82
+
83
+ require 'database_cleaner' # see comments below
84
+
85
+ RSpec.configure do |config|
86
+ # This standard Rails approach won't work in this project (which is not
87
+ # _really_ a Rails app after all.
88
+ # config.use_transactional_fixtures = true
89
+ # So we are trying the database_cleaner gem instead:
90
+ config.before(:suite) do
91
+ DatabaseCleaner.strategy = :transaction
92
+ DatabaseCleaner.clean_with(:truncation)
93
+ end
94
+
95
+ config.before(:each) do
96
+ DatabaseCleaner.start
97
+ end
98
+
99
+ config.after(:each) do
100
+ DatabaseCleaner.clean
101
+ end
102
+
103
+ # Run specs in random order to surface order dependencies. If you find an
104
+ # order dependency and want to debug it, you can fix the order by providing
105
+ # the seed, which is printed after each run.
106
+ # --seed 1234
107
+ config.order = "random"
108
+
109
+ config.include SpecHelperMethods
110
+ end
@@ -0,0 +1,2 @@
1
+ module SpecHelperMethods
2
+ end
@@ -0,0 +1,7 @@
1
+ class Failer
2
+ acts_as_orchestrated
3
+
4
+ def always_fail(something)
5
+ raise 'I never work'
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class First
2
+
3
+ acts_as_orchestrated
4
+
5
+ def do_first_thing(other_prime)
6
+ 5 * other_prime # 5 is a prime
7
+ end
8
+
9
+ end
@@ -0,0 +1,9 @@
1
+ class Second
2
+
3
+ acts_as_orchestrated
4
+
5
+ def do_second_thing(other_prime)
6
+ 7 * other_prime # 7 is a prime
7
+ end
8
+
9
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ require 'orchestrated'
4
+
5
+ shared_examples_for 'cancellation:' do
6
+ before(:each) do
7
+ @prerequisite.cancel!
8
+ end
9
+ it 'dependent should be in the "canceled" state' do
10
+ expect(@dependent.reload.canceled?).to be_true
11
+ end
12
+ end
13
+
14
+ shared_examples_for 'cannot cancel:' do
15
+ it 'dependent should be in the "canceled" state' do
16
+ expect{@prerequisite.cancel!}.to raise_error(StateMachine::InvalidTransition)
17
+ end
18
+ end
19
+
20
+ describe 'cancellation' do
21
+ context 'directly on an orchestration' do
22
+ before(:each) do
23
+ @prerequisite = @dependent = First.new.orchestrated.do_first_thing(1)
24
+ end
25
+ context 'that is ready' do
26
+ it_should_behave_like 'cancellation:'
27
+ it 'should never subsequently deliver the orchestrated message' do
28
+ First.any_instance.should_not_receive(:do_first_thing)
29
+ DJ.work(1)
30
+ end
31
+ end
32
+ context 'that is succeeded' do
33
+ before(:each) do
34
+ @prerequisite.orchestration.state = 'succeeded'
35
+ end
36
+ it_should_behave_like 'cannot cancel:'
37
+ end
38
+ context 'that is failed' do
39
+ before(:each) do
40
+ @prerequisite.orchestration.state = 'failed'
41
+ end
42
+ it_should_behave_like 'cannot cancel:'
43
+ end
44
+ context 'that is canceled' do
45
+ before(:each) do
46
+ @prerequisite.orchestration.state = 'canceled'
47
+ end
48
+ it_should_behave_like 'cannot cancel:'
49
+ end
50
+ end
51
+ context 'of an orchestration that is depended on directly' do
52
+ before(:each) do
53
+ @dependent = Second.new.orchestrated( @prerequisite = First.new.orchestrated.do_first_thing(1)).do_second_thing(2)
54
+ end
55
+ it_should_behave_like 'cancellation:'
56
+ end
57
+ context 'of an orchestration that is depended on through a LastCompletion' do
58
+ before(:each) do
59
+ @dependent = Second.new.orchestrated(
60
+ Orchestrated::LastCompletion.new <<
61
+ (@prerequisite = First.new.orchestrated.do_first_thing(1))
62
+ ).do_second_thing(2)
63
+ end
64
+ it_should_behave_like 'cancellation:'
65
+ end
66
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ require 'orchestrated'
4
+
5
+ shared_examples_for 'literally complete' do
6
+ it 'should immediately enqueue the dependent orchestration' do
7
+ expect(DJ.job_count).to be(1)
8
+ end
9
+ it 'should cause the dependent orchestration to run immediately' do
10
+ First.any_instance.should_receive(:do_first_thing)
11
+ DJ.work(1)
12
+ end
13
+ end
14
+
15
+ describe Orchestrated::CompletionExpression do
16
+ context 'Complete' do
17
+ context 'implicitly specified' do
18
+ before(:each){ First.new.orchestrated.do_first_thing(12) }
19
+ it_should_behave_like 'literally complete'
20
+ end
21
+ context 'explicitly specified' do
22
+ before(:each){ First.new.orchestrated(Orchestrated::Complete.new).do_first_thing(12) }
23
+ it_should_behave_like 'literally complete'
24
+ end
25
+ end
26
+ context 'Incomplete' do
27
+ it 'should immediately raise an error' do
28
+ expect{First.new.orchestrated(Orchestrated::Incomplete.new).do_first_thing(12)}.to raise_error
29
+ end
30
+ end
31
+ context 'OrchestrationCompletion' do
32
+ before(:each){Second.new.orchestrated( First.new.orchestrated.do_first_thing(3)).do_second_thing(4)}
33
+ it 'should block second orchestration until after first runs' do
34
+ expect(DJ.job_count).to be(1)
35
+ end
36
+ it 'should run second orchestration after first is complete' do
37
+ Second.any_instance.should_receive(:do_second_thing)
38
+ DJ.work(2)
39
+ end
40
+ end
41
+ context 'FirstCompletion' do
42
+ context 'given a (literal) Complete' do
43
+ before(:each) do
44
+ Second.new.orchestrated( Orchestrated::FirstCompletion.new <<
45
+ Orchestrated::Complete.new
46
+ ).do_second_thing(5)
47
+ end
48
+ it 'should immediately enqueue the dependent orchestration' do
49
+ expect(DJ.job_count).to be(1)
50
+ end
51
+ end
52
+ context 'given two OrchestrationCompletions' do
53
+ before(:each) do
54
+ Second.new.orchestrated( Orchestrated::FirstCompletion.new <<
55
+ First.new.orchestrated.do_first_thing(3) <<
56
+ First.new.orchestrated.do_first_thing(4)
57
+ ).do_second_thing(5)
58
+ end
59
+ it 'should enqueue the dependent orchestration as soon as the first prerequisite completes' do
60
+ expect(DJ.job_count).to be(2)
61
+ DJ.work(1)
62
+ expect(DJ.job_count).to be(2)
63
+ end
64
+ it 'should cause the dependent orchestration to run eventually' do
65
+ Second.any_instance.should_receive(:do_second_thing).with(5)
66
+ DJ.work(3)
67
+ end
68
+ it 'should skip dependents after the first one runs' do
69
+ First.any_instance.should_not_receive(:do_first_thing).with(4)
70
+ DJ.work(3)
71
+ end
72
+ end
73
+ end
74
+ context 'LastCompletion' do
75
+ context 'given two OrchestrationCompletions' do
76
+ before(:each) do
77
+ Second.new.orchestrated( Orchestrated::LastCompletion.new <<
78
+ First.new.orchestrated.do_first_thing(3) <<
79
+ First.new.orchestrated.do_first_thing(4)
80
+ ).do_second_thing(5)
81
+ end
82
+ it 'should not enqueue the dependent orchestration as soon as the first prerequisite completes' do
83
+ expect(DJ.job_count).to be(2)
84
+ DJ.work(1)
85
+ expect(DJ.job_count).to be(1)
86
+ end
87
+ it 'should not run the dependent orchestration as soon as the first prerequisite completes' do
88
+ Second.any_instance.should_not_receive(:do_second_thing)
89
+ DJ.work(2)
90
+ end
91
+ it 'should run the dependent orchestration after all the prerequisite are complete' do
92
+ Second.any_instance.should_receive(:do_second_thing)
93
+ DJ.work(3)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ class TestJob < Struct.new(:name)
4
+
5
+ class << self
6
+ attr_accessor :called
7
+
8
+ def reset_called
9
+ @called = Hash.new { |hash, key| hash[key] = 0 }
10
+ end
11
+ end
12
+
13
+ reset_called # initialized the class
14
+
15
+ # some random method
16
+ def custom_action
17
+ self.class.called[:custom_action] += 1
18
+ end
19
+
20
+ # -------------- delayed_job lifecycle callbacks ---------------
21
+ def enqueue(job)
22
+ end
23
+
24
+ def perform
25
+ self.class.called[:perform] += 1
26
+ end
27
+
28
+ def before(job)
29
+ end
30
+
31
+ def after(job)
32
+ end
33
+
34
+ def success(job)
35
+ end
36
+
37
+ def error(job, exception)
38
+ end
39
+
40
+ def failure
41
+ end
42
+
43
+ end
44
+
45
+ describe Delayed::Job do
46
+ context 'with a job' do
47
+ let(:job) {TestJob.new('fred')}
48
+ it 'should accept rspec message hooks' do
49
+ # these hooks aren't as useful as you might think since
50
+ # the object "waked" by DJ is a different object entirely!
51
+ job.should_receive(:custom_action).and_call_original
52
+ job.custom_action
53
+ end
54
+ it 'should start with an empty queue' do
55
+ expect(DJ.job_count).to be(0)
56
+ end
57
+ it 'should enqueue a job' do
58
+ expect {
59
+ job.delay.custom_action
60
+ }.to change{DJ.job_count}.by(1)
61
+ end
62
+ context 'that is enqueued' do
63
+ before(:each) do
64
+ DJ.clear_all_jobs
65
+ job.delay.custom_action # queue exactly one job
66
+ end
67
+ it 'should dequeue the job' do
68
+ expect {
69
+ successes, failures = DJ.work
70
+ }.to change{DJ.job_count}.by(-1)
71
+ end
72
+ it 'should deliver a message' do
73
+ expect {
74
+ successes, failures = DJ.work
75
+ }.to change{TestJob.called[:custom_action]}.by(1)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ require 'orchestrated'
4
+
5
+ describe 'failure' do
6
+ context 'orchestrating a method that always fails' do
7
+ before(:each) do
8
+ Failer.new.orchestrated.always_fail('important stuff')
9
+ end
10
+ context 'after first exception from orchestrated method' do
11
+ before(:each) do
12
+ DJ.work(1)
13
+ end
14
+ it 'should leave the orchestration in the ready state' do
15
+ expect(Orchestrated::Orchestration.with_state('ready').count).to be(1)
16
+ end
17
+ it 'should leave the orchestration in the run queue' do
18
+ expect(DJ.job_count).to be(1)
19
+ end
20
+ context 'on first retry' do
21
+ it 'should retry with same arguments' do
22
+ Failer.any_instance.should_receive(:always_fail).with('important stuff')
23
+ DJ.work_now(1)
24
+ end
25
+ end
26
+ end
27
+ context 'after (Delayed::Worker.max_attempts + 1) exceptions from orchestrated method' do
28
+ before(:each) do
29
+ DJ.work_now(DJ.max_attempts)
30
+ end
31
+ it 'should leave the orchestration in the failed state' do
32
+ expect(Orchestrated::Orchestration.with_state('failed').count).to be(1)
33
+ end
34
+ context 'on first subsequent retry' do
35
+ it 'should never deliver the orchestrated message again' do
36
+ Failer.any_instance.should_not_receive(:always_fail)
37
+ DJ.work_now(1)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ require 'orchestrated'
4
+
5
+ describe Orchestrated do
6
+ context 'initializing' do
7
+ it 'should not define orchestrated on Object' do
8
+ expect(Object.public_method_defined?(:orchestrated)).to be_false
9
+ end
10
+ it 'should not define orchestrated on ActiveRecord::Base' do
11
+ expect(ActiveRecord::Base.public_method_defined?(:orchestrated)).to be_false
12
+ end
13
+ it 'should define orchestrated on First' do
14
+ expect(First.public_method_defined?(:orchestrated)).to be_true
15
+ end
16
+ end
17
+ context 'a new orchestrated object' do
18
+ let(:f){First.new}
19
+ context 'responding to messages without orchestration' do
20
+ let(:result){f.do_first_thing(2)} # 2 is a prime number
21
+ it 'should immediately invoke a non-orchestrated method and return correct result' do
22
+ expect(result).to eq(5 * 2)
23
+ end
24
+ end
25
+ context 'orchestrating with no prerequisites' do
26
+ before(:each){@result = f.orchestrated.do_first_thing(2)}
27
+ after(:each){DJ.clear_all_jobs}
28
+ it 'should not immediately invoke an orchestrated method' do
29
+ First.any_instance.should_not_receive(:do_first_thing)
30
+ end
31
+ it 'should return an Orchestration object' do
32
+ expect(@result).to be_kind_of(Orchestrated::CompletionExpression)
33
+ end
34
+ end
35
+ end
36
+ context 'invocation' do
37
+ before(:each) do
38
+ First.new.orchestrated.do_first_thing(1)
39
+ end
40
+ it 'should have access to Orchestration' do
41
+ First.any_instance.should_receive(:orchestration=).with(kind_of(Orchestrated::Orchestration))
42
+ First.any_instance.should_receive(:orchestration=).with(nil)
43
+ DJ.work(1)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ class Foo
2
+ def bump(x)
3
+ x+1
4
+ end
5
+ end
6
+
7
+ describe 'rspec' do
8
+ context 'mocking should_receive' do
9
+ it 'should pass method arguments to my block' do
10
+ x = 0
11
+ block = nil
12
+ Foo.any_instance.should_receive(:bump){|x_arg, &block_arg|
13
+ x = x_arg
14
+ block = block_arg
15
+ }
16
+ Foo.new.bump(1){|hello| puts hello}
17
+ expect(x).to be(1)
18
+ expect(block).to be_kind_of(Proc)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ require 'orchestrated'
4
+
5
+ describe 'performing static analysis' do
6
+ context 'on a FirstCompletion' do
7
+ let(:completion){Orchestrated::FirstCompletion.new}
8
+ context 'that is empty' do
9
+ # chose this behavior to align with Ruby Enumerable#any?
10
+ it 'should raise an error since it can never be complete' do
11
+ expect{Second.new.orchestrated(completion).do_second_thing(5)}.to raise_error
12
+ end
13
+ end
14
+ context 'that contains only (static) Incompletes' do
15
+ before(:each){completion<<Orchestrated::Incomplete.new}
16
+ it 'should raise an error since it can never be complete' do
17
+ expect{Second.new.orchestrated(completion).do_second_thing(5)}.to raise_error
18
+ end
19
+ end
20
+ context 'that directly containins a (static) Complete' do
21
+ before(:each){completion<<Orchestrated::Complete.new}
22
+ it 'should be complete immediately' do
23
+ expect{completion.complete?}.to be_true
24
+ end
25
+ end
26
+ end
27
+ context 'on a LastCompletion' do
28
+ let(:completion){Orchestrated::LastCompletion.new}
29
+ context 'that is empty' do
30
+ # chose this behavior to align with Ruby Enumerable#all?
31
+ it 'should be complete immediately' do
32
+ expect{completion.complete?}.to be_true
33
+ end
34
+ end
35
+ context 'that contains only (static) Completes' do
36
+ before(:each){completion<<Orchestrated::Complete.new}
37
+ it 'should be complete immediately' do
38
+ expect{completion.complete?}.to be_true
39
+ end
40
+ end
41
+ context 'that directly contains a (static) Incomplete' do
42
+ before(:each){completion<<Orchestrated::Incomplete.new}
43
+ it 'should raise an error since it can never be complete' do
44
+ expect{Second.new.orchestrated(completion).do_second_thing(5)}.to raise_error
45
+ end
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: orchestrated
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Bill Burcham
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: delayed_job_active_record
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activerecord
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '3'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: state_machine
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '1'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '1'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rails
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '3'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '3'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec-rails
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: database_cleaner
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: sqlite3
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: a workflow orchestration framework running on delayed_job and active_record
143
+ email:
144
+ - bill@paydici.com
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - .gitignore
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - Orchestrated::Orchestration_state.png
153
+ - README.markdown
154
+ - Rakefile
155
+ - lib/orchestrated.rb
156
+ - lib/orchestrated/base.rb
157
+ - lib/orchestrated/completion.rb
158
+ - lib/orchestrated/message_delivery.rb
159
+ - lib/orchestrated/object.rb
160
+ - lib/orchestrated/orchestration.rb
161
+ - lib/orchestrated/version.rb
162
+ - orchestrated.gemspec
163
+ - spec/database.yml
164
+ - spec/delayed_job_facade.rb
165
+ - spec/spec_helper.rb
166
+ - spec/spec_helper_methods.rb
167
+ - spec/support/sample_classes/failer.rb
168
+ - spec/support/sample_classes/first.rb
169
+ - spec/support/sample_classes/second.rb
170
+ - spec/unit/cancellation_spec.rb
171
+ - spec/unit/completion_spec.rb
172
+ - spec/unit/delayed_job_spec.rb
173
+ - spec/unit/failure_spec.rb
174
+ - spec/unit/orchestrated_spec.rb
175
+ - spec/unit/rspec_spec.rb
176
+ - spec/unit/static_analysis_spec.rb
177
+ homepage: https://github.com/paydici/orchestration
178
+ licenses:
179
+ - MIT
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ none: false
192
+ requirements:
193
+ - - ! '>='
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ requirements: []
197
+ rubyforge_project:
198
+ rubygems_version: 1.8.24
199
+ signing_key:
200
+ specification_version: 3
201
+ summary: Orchestrated is a workflow orchestration framework running on delayed_job
202
+ and active_record. In the style of delayed_job's 'delay', Orchestration lets you
203
+ 'orchestrate' delivery of a message so that it will run only after others have been
204
+ delivered and processed.
205
+ test_files:
206
+ - spec/database.yml
207
+ - spec/delayed_job_facade.rb
208
+ - spec/spec_helper.rb
209
+ - spec/spec_helper_methods.rb
210
+ - spec/support/sample_classes/failer.rb
211
+ - spec/support/sample_classes/first.rb
212
+ - spec/support/sample_classes/second.rb
213
+ - spec/unit/cancellation_spec.rb
214
+ - spec/unit/completion_spec.rb
215
+ - spec/unit/delayed_job_spec.rb
216
+ - spec/unit/failure_spec.rb
217
+ - spec/unit/orchestrated_spec.rb
218
+ - spec/unit/rspec_spec.rb
219
+ - spec/unit/static_analysis_spec.rb