orchestrated 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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 a remote system (a slow process), transform each one, and then load it into your system. Imagine you had had a Downloader class that knew how to download and an Xform class that knew how to transform the downloaded file and load it into your system. You might write an orchestration like this:
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
- Downloader.new.orchestrated.download(
70
- :from=>'http://foo.bar.com/customer_fred', :to=>'fred_file'
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
- ).transform('fred_file', 'fred_file_processed')
73
- ).load('fred_file_processed')
77
+ ).merge(['fred_records', 'sally_records'], 'combined_records')
78
+ ).load('combined_records')
74
79
  ```
75
80
 
76
- The next time you process a delayed job, the :download message will be delivered to a Downloader. After the download is complete, the next time a delayed job is processed, the :transform message will be delivered to an Xform object.
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 :composited_completions do |table|
19
- table.references :composite_completion
20
- table.references :completion_expression
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 :composited_completions
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 :composited_completions
32
- has_many :completion_expressions, :through => :composited_completions, :source => :completion_expression
33
- def +(c); self << c; end # synonym
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?; 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
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
- completion_expressions << c unless c.always_complete?
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?; 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
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
- completion_expressions << c unless c.never_complete?
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 OrchestrationCompletion < CompletionExpression
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
- class CompositedCompletion < ActiveRecord::Base
69
- belongs_to :composite_completion
70
- belongs_to :completion_expression
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
- belongs_to :prerequisite, :class_name => 'CompletionExpression'
14
- belongs_to :delayed_job, :polymorphic => true # loose-ish coupling with delayed_job
13
+ has_one :prerequisite, :class_name => 'OrchestrationInterest'
14
+ has_one :dependent, :class_name => 'OrchestrationCompletion'
15
15
 
16
- has_many :orchestration_completions
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 => :new do
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 :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
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
- # 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
67
+ orchestration.dependent.prerequisite_complete
71
68
  end
72
69
 
73
70
  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
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
- # saves object as side effect of this assignment
92
- # also moves orchestration to :ready state
93
- orchestration.prerequisite = prerequisite
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
@@ -1,3 +1,3 @@
1
1
  module Orchestrated
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/orchestrated.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "orchestrated/version"
2
2
  require 'orchestrated/base'
3
3
  require 'orchestrated/completion'
4
+ require 'orchestrated/dependency'
4
5
  require 'orchestrated/message_delivery'
5
6
  require 'orchestrated/orchestration'
6
7
  require 'orchestrated/object'
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 :composited_completions do |table|
62
- table.references :composite_completion
63
- table.references :completion_expression
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 be(1)
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 be(1)
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 be(1)
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 be(2)
60
+ expect(DJ.job_count).to eq(2)
61
61
  DJ.work(1)
62
- expect(DJ.job_count).to be(2)
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 be(2)
83
+ expect(DJ.job_count).to eq(2)
84
84
  DJ.work(1)
85
- expect(DJ.job_count).to be(1)
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)
@@ -52,7 +52,7 @@ describe Delayed::Job do
52
52
  job.custom_action
53
53
  end
54
54
  it 'should start with an empty queue' do
55
- expect(DJ.job_count).to be(0)
55
+ expect(DJ.job_count).to eq(0)
56
56
  end
57
57
  it 'should enqueue a job' do
58
58
  expect {
@@ -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 be(1)
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 be(1)
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 be(1)
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
@@ -14,7 +14,7 @@ describe 'rspec' do
14
14
  block = block_arg
15
15
  }
16
16
  Foo.new.bump(1){|hello| puts hello}
17
- expect(x).to be(1)
17
+ expect(x).to eq(1)
18
18
  expect(block).to be_kind_of(Proc)
19
19
  end
20
20
  end
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.3
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-03 00:00:00.000000000 Z
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