orchestrated 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Orchestrated::Orchestration_state.png +0 -0
- data/README.markdown +21 -8
- data/lib/generators/orchestrated/templates/migration.rb +5 -4
- data/lib/orchestrated/completion.rb +56 -17
- data/lib/orchestrated/dependency.rb +27 -0
- data/lib/orchestrated/orchestration.rb +22 -27
- data/lib/orchestrated/version.rb +1 -1
- data/lib/orchestrated.rb +1 -0
- data/spec/spec_helper.rb +4 -3
- data/spec/unit/completion_spec.rb +7 -7
- data/spec/unit/delayed_job_spec.rb +1 -1
- data/spec/unit/failure_spec.rb +3 -3
- data/spec/unit/rspec_spec.rb +1 -1
- metadata +3 -2
Binary file
|
data/README.markdown
CHANGED
@@ -60,20 +60,25 @@ Declaring ```acts_as_orchestrated``` on your class gives it two methods:
|
|
60
60
|
* ```orchestrated```—call this to specify your workflow prerequisite, and designate a workflow step
|
61
61
|
* ```orchestration```—call this in the context of a workflow step (execution) to access orchestration (and prerequisite) context
|
62
62
|
|
63
|
-
After that you can orchestrate any method on such a class. Let's say you needed to download files from
|
63
|
+
After that you can orchestrate any method on such a class. Let's say you needed to download a couple files from remote systems (a slow process), merge their content and then load the results into your system. Imagine you had had a Downloader class that knew how to download and an Xform class that knew how to merge the content and load the results into your system. You might write an orchestration like this:
|
64
64
|
|
65
65
|
```ruby
|
66
66
|
xform = Xform.new
|
67
67
|
xform.orchestrated(
|
68
68
|
xform.orchestrated(
|
69
|
-
|
70
|
-
|
69
|
+
Orchestrated::LastCompletion.new(
|
70
|
+
Downloader.new.orchestrated.download(
|
71
|
+
:from=>'http://fred.com/stuff', :to=>'fred_records'
|
72
|
+
),
|
73
|
+
Downloader.new.orchestrated.download(
|
74
|
+
:from=>'http://sally.com/stuff', :to=>'sally_records'
|
75
|
+
)
|
71
76
|
)
|
72
|
-
).
|
73
|
-
).load('
|
77
|
+
).merge(['fred_records', 'sally_records'], 'combined_records')
|
78
|
+
).load('combined_records')
|
74
79
|
```
|
75
80
|
|
76
|
-
The next time you process
|
81
|
+
The next time you process delayed jobs, the :download messages will be delivered to a couple Downloaders. After the last download completes, the next time a delayed job is processed, the :merge message will be delivered to an Xform object. And on it goes…
|
77
82
|
|
78
83
|
What happened there? The pattern is:
|
79
84
|
|
@@ -108,8 +113,6 @@ An orchestration can be in one of six (6) states:
|
|
108
113
|
|
109
114
|
![Alt text](https://github.com/paydici/orchestrated/raw/master/Orchestrated::Orchestration_state.png 'Orchestration States')
|
110
115
|
|
111
|
-
You'll never see an orchestration in the "new" state, it's for internal use in the framework. But all the others are interesting.
|
112
|
-
|
113
116
|
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)).
|
114
117
|
|
115
118
|
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.
|
@@ -144,3 +147,13 @@ Contributing
|
|
144
147
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
145
148
|
4. Push to the branch (`git push origin my-new-feature`)
|
146
149
|
5. Create new Pull Request
|
150
|
+
|
151
|
+
Future Work
|
152
|
+
-----------
|
153
|
+
|
154
|
+
Some possible avenues for exploration:
|
155
|
+
|
156
|
+
* orchestrated option: :max_attempts to configure max_attempts on underlying delayed job instance
|
157
|
+
* orchestrated option: :max_run_time to configure max_run_time on underlying delayed job instance
|
158
|
+
* orchestrated options: :queue to specify a particular named queue for the underlying delayed job
|
159
|
+
* some way to change the run_at recalculation for failed attempts (f(n) = 5 + n**4 is not always right and what's right varies by job)
|
@@ -15,14 +15,15 @@ class CreateOrchestrated < ActiveRecord::Migration
|
|
15
15
|
# the Rails model
|
16
16
|
table.references :orchestration
|
17
17
|
end
|
18
|
-
create_table :
|
19
|
-
table.
|
20
|
-
table.references :
|
18
|
+
create_table :orchestration_dependencies do |table|
|
19
|
+
table.string :state
|
20
|
+
table.references :dependent
|
21
|
+
table.references :prerequisite
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
25
|
def self.down
|
25
|
-
drop_table :
|
26
|
+
drop_table :orchestration_dependencies
|
26
27
|
drop_table :completion_expressions
|
27
28
|
drop_table :orchestrations
|
28
29
|
end
|
@@ -7,11 +7,22 @@ module Orchestrated
|
|
7
7
|
class CompletionExpression < ActiveRecord::Base
|
8
8
|
# I'd like to make this abstract, but Rails gets confused if I do
|
9
9
|
# self.abstract_class = true
|
10
|
+
has_many :dependent_associations, :class_name => 'OrchestrationDependency', :foreign_key => 'prerequisite_id'
|
11
|
+
has_many :dependents, :through => :dependent_associations, :autosave => true
|
10
12
|
def complete?; throw 'subclass must override!';end
|
11
13
|
# for static analysis
|
12
14
|
def always_complete?; throw 'subclass must override!';end
|
13
15
|
def never_complete?; throw 'subclass must override!';end
|
14
16
|
def canceled?; throw 'subclass must override!';end
|
17
|
+
def prerequisite_complete; throw 'subclass must override!';end
|
18
|
+
def notify_dependents_of_completion
|
19
|
+
# NB: we are notifying the Join Model here (it keeps track of status)
|
20
|
+
dependent_associations.each{|d| d.prerequisite_completed}
|
21
|
+
end
|
22
|
+
def notify_dependents_of_cancellation
|
23
|
+
# NB: we are notifying the Join Model here (it keeps track of status)
|
24
|
+
dependent_associations.each{|d| d.prerequisite_canceled}
|
25
|
+
end
|
15
26
|
end
|
16
27
|
class Complete < CompletionExpression
|
17
28
|
def complete?; true; end
|
@@ -28,31 +39,43 @@ module Orchestrated
|
|
28
39
|
end
|
29
40
|
class CompositeCompletion < CompletionExpression
|
30
41
|
# self.abstract_class = true
|
31
|
-
has_many :
|
32
|
-
has_many :
|
33
|
-
def +(c); self << c; end #
|
42
|
+
has_many :prerequisite_associations, :class_name => 'OrchestrationDependency', :foreign_key => 'dependent_id'
|
43
|
+
has_many :prerequisites, :through => :prerequisite_associations, :autosave => true
|
44
|
+
def +(c); self << c; end # synonymc
|
34
45
|
end
|
35
46
|
class LastCompletion < CompositeCompletion
|
36
|
-
def complete?;
|
37
|
-
def always_complete?;
|
38
|
-
def never_complete?;
|
39
|
-
def canceled?;
|
47
|
+
def complete?; prerequisites.all?(&:complete?); end
|
48
|
+
def always_complete?; prerequisites.empty?; end
|
49
|
+
def never_complete?; prerequisites.any?(&:never_complete?); end
|
50
|
+
def canceled?; prerequisites.any?(&:canceled?); end
|
40
51
|
def <<(c)
|
41
|
-
|
52
|
+
prerequisites << c unless c.always_complete?
|
42
53
|
self
|
43
54
|
end
|
55
|
+
def prerequisite_complete
|
56
|
+
notify_dependents_of_completion unless prerequisite_associations.without_states('complete').exists?
|
57
|
+
end
|
58
|
+
def prerequisite_canceled
|
59
|
+
notify_dependents_of_cancellation
|
60
|
+
end
|
44
61
|
end
|
45
62
|
class FirstCompletion < CompositeCompletion
|
46
|
-
def complete?;
|
47
|
-
def always_complete?;
|
48
|
-
def never_complete?;
|
49
|
-
def canceled?;
|
63
|
+
def complete?; prerequisites.any?(&:complete?); end
|
64
|
+
def always_complete?; prerequisites.any?(&:always_complete?); end
|
65
|
+
def never_complete?; prerequisites.empty?; end
|
66
|
+
def canceled?; prerequisites.all?(&:canceled?); end
|
50
67
|
def <<(c)
|
51
|
-
|
68
|
+
prerequisites << c unless c.never_complete?
|
52
69
|
self
|
53
70
|
end
|
71
|
+
def prerequisite_complete
|
72
|
+
notify_dependents_of_completion
|
73
|
+
end
|
74
|
+
def prerequisite_canceled
|
75
|
+
notify_dependents_of_completion unless prerequisite_associations.without_states('canceled').exists?
|
76
|
+
end
|
54
77
|
end
|
55
|
-
class
|
78
|
+
class OrchestrationCompletionShim < CompletionExpression
|
56
79
|
# Arguably, it is "bad" to make this class derive
|
57
80
|
# from CompletionExpression since doing so introduces
|
58
81
|
# the orchestration_id into the table (that constitutes
|
@@ -61,12 +84,28 @@ module Orchestrated
|
|
61
84
|
# understand joins when computing dependents at runtime.
|
62
85
|
belongs_to :orchestration
|
63
86
|
validates_presence_of :orchestration_id
|
87
|
+
end
|
88
|
+
# wraps an Orchestration and makes it usable as a completion expression
|
89
|
+
class OrchestrationCompletion < OrchestrationCompletionShim
|
64
90
|
delegate :complete?, :canceled?, :cancel!, :to => :orchestration
|
65
91
|
def always_complete?; false; end
|
66
92
|
def never_complete?; false; end
|
93
|
+
def prerequisite_complete
|
94
|
+
notify_dependents_of_completion
|
95
|
+
end
|
96
|
+
def prerequisite_canceled
|
97
|
+
notify_dependents_of_cancellation
|
98
|
+
end
|
67
99
|
end
|
68
|
-
|
69
|
-
|
70
|
-
|
100
|
+
# registers an Orchestration's interest in a completion expression
|
101
|
+
class OrchestrationInterest < OrchestrationCompletionShim
|
102
|
+
has_one :prerequisite_association, :class_name => 'OrchestrationDependency', :foreign_key => 'dependent_id'
|
103
|
+
has_one :prerequisite, :through => :prerequisite_association, :autosave => true
|
104
|
+
def prerequisite_complete
|
105
|
+
orchestration.prerequisite_complete
|
106
|
+
end
|
107
|
+
def prerequisite_canceled
|
108
|
+
orchestration.prerequisite_canceled
|
109
|
+
end
|
71
110
|
end
|
72
111
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'state_machine'
|
2
|
+
|
3
|
+
module Orchestrated
|
4
|
+
class OrchestrationDependency < ActiveRecord::Base
|
5
|
+
# TODO: figure out why Rails thinks I'm mass-assigning this over in Orchestration when I'm not really!
|
6
|
+
attr_accessible :prerequisite_id
|
7
|
+
belongs_to :dependent, :class_name => 'CompletionExpression'
|
8
|
+
belongs_to :prerequisite, :class_name => 'CompletionExpression'
|
9
|
+
state_machine :initial => :incomplete do
|
10
|
+
state :incomplete
|
11
|
+
state :complete
|
12
|
+
state :canceled
|
13
|
+
event :prerequisite_completed do
|
14
|
+
transition :incomplete => :complete
|
15
|
+
end
|
16
|
+
event :prerequisite_canceled do
|
17
|
+
transition :incomplete => :canceled
|
18
|
+
end
|
19
|
+
after_transition any => :complete do |orchestration, transition|
|
20
|
+
orchestration.dependent.prerequisite_complete
|
21
|
+
end
|
22
|
+
after_transition any => :canceled do |orchestration, transition|
|
23
|
+
orchestration.dependent.prerequisite_canceled
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -10,14 +10,13 @@ module Orchestrated
|
|
10
10
|
|
11
11
|
serialize :handler
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
has_one :prerequisite, :class_name => 'OrchestrationInterest'
|
14
|
+
has_one :dependent, :class_name => 'OrchestrationCompletion'
|
15
15
|
|
16
|
-
|
16
|
+
belongs_to :delayed_job, :polymorphic => true # loose-ish coupling with delayed_job
|
17
17
|
|
18
18
|
complete_states = [:succeeded, :failed]
|
19
|
-
state_machine :initial => :
|
20
|
-
state :new
|
19
|
+
state_machine :initial => :waiting do
|
21
20
|
state :waiting
|
22
21
|
state :ready
|
23
22
|
state :succeeded
|
@@ -36,10 +35,12 @@ module Orchestrated
|
|
36
35
|
end
|
37
36
|
end
|
38
37
|
|
39
|
-
event :
|
40
|
-
transition
|
41
|
-
|
42
|
-
|
38
|
+
event :prerequisite_complete do
|
39
|
+
transition :waiting => :ready
|
40
|
+
end
|
41
|
+
|
42
|
+
event :prerequisite_canceled do
|
43
|
+
transition [:waiting, :ready] => :canceled
|
43
44
|
end
|
44
45
|
|
45
46
|
event :message_delivery_succeeded do
|
@@ -63,18 +64,11 @@ module Orchestrated
|
|
63
64
|
end
|
64
65
|
|
65
66
|
after_transition any => complete_states do |orchestration, transition|
|
66
|
-
|
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
|
67
|
+
orchestration.dependent.prerequisite_complete
|
71
68
|
end
|
72
69
|
|
73
70
|
after_transition [:ready, :waiting] => :canceled do |orchestration, transition|
|
74
|
-
|
75
|
-
(Orchestration.with_states(:ready, :waiting).all - [orchestration]).each do |other|
|
76
|
-
other.prerequisite_changed
|
77
|
-
end
|
71
|
+
orchestration.dependent.prerequisite_canceled
|
78
72
|
end
|
79
73
|
|
80
74
|
end
|
@@ -88,9 +82,16 @@ module Orchestrated
|
|
88
82
|
# wee! static analysis FTW!
|
89
83
|
raise 'prerequisite can never be complete' if prerequisite.never_complete?
|
90
84
|
|
91
|
-
|
92
|
-
|
93
|
-
|
85
|
+
prerequisite.save!
|
86
|
+
orchestration.save!
|
87
|
+
interest = OrchestrationInterest.new.tap do |interest|
|
88
|
+
interest.prerequisite = prerequisite
|
89
|
+
interest.orchestration = orchestration
|
90
|
+
end
|
91
|
+
interest.save!
|
92
|
+
|
93
|
+
# prime the pump for a constant prerequisite
|
94
|
+
orchestration.prerequisite_complete! if prerequisite.complete?
|
94
95
|
end
|
95
96
|
end
|
96
97
|
|
@@ -102,11 +103,5 @@ module Orchestrated
|
|
102
103
|
delayed_job.destroy# if DelayedJob.exists?(delayed_job_id)
|
103
104
|
end
|
104
105
|
|
105
|
-
alias_method :prerequisite_old_equals, :prerequisite=
|
106
|
-
def prerequisite=(*args)
|
107
|
-
prerequisite_old_equals(*args)
|
108
|
-
prerequisite_changed
|
109
|
-
end
|
110
|
-
|
111
106
|
end
|
112
107
|
end
|
data/lib/orchestrated/version.rb
CHANGED
data/lib/orchestrated.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -58,9 +58,10 @@ ActiveRecord::Schema.define do
|
|
58
58
|
# the Rails model
|
59
59
|
table.references :orchestration
|
60
60
|
end
|
61
|
-
create_table :
|
62
|
-
table.
|
63
|
-
table.references :
|
61
|
+
create_table :orchestration_dependencies do |table|
|
62
|
+
table.string :state
|
63
|
+
table.references :dependent
|
64
|
+
table.references :prerequisite
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
@@ -4,7 +4,7 @@ require 'orchestrated'
|
|
4
4
|
|
5
5
|
shared_examples_for 'literally complete' do
|
6
6
|
it 'should immediately enqueue the dependent orchestration' do
|
7
|
-
expect(DJ.job_count).to
|
7
|
+
expect(DJ.job_count).to eq(1)
|
8
8
|
end
|
9
9
|
it 'should cause the dependent orchestration to run immediately' do
|
10
10
|
First.any_instance.should_receive(:do_first_thing)
|
@@ -31,7 +31,7 @@ describe Orchestrated::CompletionExpression do
|
|
31
31
|
context 'OrchestrationCompletion' do
|
32
32
|
before(:each){Second.new.orchestrated( First.new.orchestrated.do_first_thing(3)).do_second_thing(4)}
|
33
33
|
it 'should block second orchestration until after first runs' do
|
34
|
-
expect(DJ.job_count).to
|
34
|
+
expect(DJ.job_count).to eq(1)
|
35
35
|
end
|
36
36
|
it 'should run second orchestration after first is complete' do
|
37
37
|
Second.any_instance.should_receive(:do_second_thing)
|
@@ -46,7 +46,7 @@ describe Orchestrated::CompletionExpression do
|
|
46
46
|
).do_second_thing(5)
|
47
47
|
end
|
48
48
|
it 'should immediately enqueue the dependent orchestration' do
|
49
|
-
expect(DJ.job_count).to
|
49
|
+
expect(DJ.job_count).to eq(1)
|
50
50
|
end
|
51
51
|
end
|
52
52
|
context 'given two OrchestrationCompletions' do
|
@@ -57,9 +57,9 @@ describe Orchestrated::CompletionExpression do
|
|
57
57
|
).do_second_thing(5)
|
58
58
|
end
|
59
59
|
it 'should enqueue the dependent orchestration as soon as the first prerequisite completes' do
|
60
|
-
expect(DJ.job_count).to
|
60
|
+
expect(DJ.job_count).to eq(2)
|
61
61
|
DJ.work(1)
|
62
|
-
expect(DJ.job_count).to
|
62
|
+
expect(DJ.job_count).to eq(2)
|
63
63
|
end
|
64
64
|
it 'should cause the dependent orchestration to run eventually' do
|
65
65
|
Second.any_instance.should_receive(:do_second_thing).with(5)
|
@@ -80,9 +80,9 @@ describe Orchestrated::CompletionExpression do
|
|
80
80
|
).do_second_thing(5)
|
81
81
|
end
|
82
82
|
it 'should not enqueue the dependent orchestration as soon as the first prerequisite completes' do
|
83
|
-
expect(DJ.job_count).to
|
83
|
+
expect(DJ.job_count).to eq(2)
|
84
84
|
DJ.work(1)
|
85
|
-
expect(DJ.job_count).to
|
85
|
+
expect(DJ.job_count).to eq(1)
|
86
86
|
end
|
87
87
|
it 'should not run the dependent orchestration as soon as the first prerequisite completes' do
|
88
88
|
Second.any_instance.should_not_receive(:do_second_thing)
|
data/spec/unit/failure_spec.rb
CHANGED
@@ -12,10 +12,10 @@ describe 'failure' do
|
|
12
12
|
DJ.work(1)
|
13
13
|
end
|
14
14
|
it 'should leave the orchestration in the ready state' do
|
15
|
-
expect(Orchestrated::Orchestration.with_state('ready').count).to
|
15
|
+
expect(Orchestrated::Orchestration.with_state('ready').count).to eq(1)
|
16
16
|
end
|
17
17
|
it 'should leave the orchestration in the run queue' do
|
18
|
-
expect(DJ.job_count).to
|
18
|
+
expect(DJ.job_count).to eq(1)
|
19
19
|
end
|
20
20
|
context 'on first retry' do
|
21
21
|
it 'should retry with same arguments' do
|
@@ -29,7 +29,7 @@ describe 'failure' do
|
|
29
29
|
DJ.work_now(DJ.max_attempts)
|
30
30
|
end
|
31
31
|
it 'should leave the orchestration in the failed state' do
|
32
|
-
expect(Orchestrated::Orchestration.with_state('failed').count).to
|
32
|
+
expect(Orchestrated::Orchestration.with_state('failed').count).to eq(1)
|
33
33
|
end
|
34
34
|
context 'on first subsequent retry' do
|
35
35
|
it 'should never deliver the orchestrated message again' do
|
data/spec/unit/rspec_spec.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: orchestrated
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: delayed_job_active_record
|
@@ -189,6 +189,7 @@ files:
|
|
189
189
|
- lib/orchestrated.rb
|
190
190
|
- lib/orchestrated/base.rb
|
191
191
|
- lib/orchestrated/completion.rb
|
192
|
+
- lib/orchestrated/dependency.rb
|
192
193
|
- lib/orchestrated/message_delivery.rb
|
193
194
|
- lib/orchestrated/object.rb
|
194
195
|
- lib/orchestrated/orchestration.rb
|