orchestrated 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Orchestrated::Orchestration_state.png +0 -0
- data/README.markdown +133 -0
- data/Rakefile +1 -0
- data/lib/orchestrated/base.rb +41 -0
- data/lib/orchestrated/completion.rb +72 -0
- data/lib/orchestrated/message_delivery.rb +31 -0
- data/lib/orchestrated/object.rb +16 -0
- data/lib/orchestrated/orchestration.rb +112 -0
- data/lib/orchestrated/version.rb +3 -0
- data/lib/orchestrated.rb +6 -0
- data/orchestrated.gemspec +33 -0
- data/spec/database.yml +17 -0
- data/spec/delayed_job_facade.rb +28 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/spec_helper_methods.rb +2 -0
- data/spec/support/sample_classes/failer.rb +7 -0
- data/spec/support/sample_classes/first.rb +9 -0
- data/spec/support/sample_classes/second.rb +9 -0
- data/spec/unit/cancellation_spec.rb +66 -0
- data/spec/unit/completion_spec.rb +97 -0
- data/spec/unit/delayed_job_spec.rb +79 -0
- data/spec/unit/failure_spec.rb +42 -0
- data/spec/unit/orchestrated_spec.rb +46 -0
- data/spec/unit/rspec_spec.rb +21 -0
- data/spec/unit/static_analysis_spec.rb +48 -0
- metadata +219 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
Binary file
|
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
|
data/lib/orchestrated.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|