ntl-orchestra 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +11 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +539 -0
  6. data/Rakefile +21 -0
  7. data/bin/rake +16 -0
  8. data/lib/orchestra/conductor.rb +119 -0
  9. data/lib/orchestra/configuration.rb +12 -0
  10. data/lib/orchestra/dsl/nodes.rb +72 -0
  11. data/lib/orchestra/dsl/object_adapter.rb +134 -0
  12. data/lib/orchestra/dsl/operations.rb +108 -0
  13. data/lib/orchestra/errors.rb +44 -0
  14. data/lib/orchestra/node/output.rb +61 -0
  15. data/lib/orchestra/node.rb +130 -0
  16. data/lib/orchestra/operation.rb +49 -0
  17. data/lib/orchestra/performance.rb +137 -0
  18. data/lib/orchestra/recording.rb +83 -0
  19. data/lib/orchestra/run_list.rb +171 -0
  20. data/lib/orchestra/thread_pool.rb +163 -0
  21. data/lib/orchestra/util.rb +98 -0
  22. data/lib/orchestra/version.rb +3 -0
  23. data/lib/orchestra.rb +35 -0
  24. data/orchestra.gemspec +26 -0
  25. data/test/examples/fizz_buzz.rb +32 -0
  26. data/test/examples/invitation_service.rb +118 -0
  27. data/test/integration/multithreading_test.rb +38 -0
  28. data/test/integration/recording_telemetry_test.rb +86 -0
  29. data/test/integration/replayable_operation_test.rb +53 -0
  30. data/test/lib/console.rb +103 -0
  31. data/test/lib/test_runner.rb +19 -0
  32. data/test/support/telemetry_recorder.rb +49 -0
  33. data/test/test_helper.rb +16 -0
  34. data/test/unit/conductor_test.rb +25 -0
  35. data/test/unit/dsl_test.rb +122 -0
  36. data/test/unit/node_test.rb +122 -0
  37. data/test/unit/object_adapter_test.rb +100 -0
  38. data/test/unit/operation_test.rb +224 -0
  39. data/test/unit/run_list_test.rb +131 -0
  40. data/test/unit/thread_pool_test.rb +105 -0
  41. data/test/unit/util_test.rb +20 -0
  42. data/tmp/.keep +0 -0
  43. metadata +159 -0
@@ -0,0 +1,49 @@
1
+ module Orchestra
2
+ class Operation < Module
3
+ extend Forwardable
4
+
5
+ def_delegators :@default_run_list, :node_names, :provisions, :dependencies,
6
+ :optional_dependencies, :required_dependencies
7
+
8
+ attr :registry, :result, :nodes
9
+
10
+ def initialize args = {}
11
+ @result, @command, @nodes = Util.extract_key_args args,
12
+ :result, :command => false, :nodes => {}
13
+ @default_run_list = RunList.build nodes, result, []
14
+ end
15
+
16
+ def process output
17
+ output.select do |key, _| key = result end
18
+ end
19
+
20
+ def start_performance *args
21
+ conductor, input = extract_args args
22
+ run_list = RunList.build nodes, result, input.keys
23
+ performance = Performance.new conductor, run_list, input
24
+ yield performance if block_given?
25
+ performance.publish :operation_entered, name, input
26
+ performance
27
+ end
28
+
29
+ def perform *args, &block
30
+ performance = start_performance *args, &block
31
+ performance.perform
32
+ output = performance.extract_result result
33
+ performance.publish :operation_exited, name, output
34
+ @command ? nil : output
35
+ end
36
+
37
+ def command?
38
+ @command ? true : false
39
+ end
40
+
41
+ private
42
+
43
+ def extract_args args
44
+ conductor = args.size > 1 ? args.shift : Conductor.new
45
+ input = args.fetch 0 do {} end
46
+ [conductor, input]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,137 @@
1
+ module Orchestra
2
+ class Performance
3
+ include Observable
4
+ extend Forwardable
5
+
6
+ def_delegators :@run_list, :node_names, :provisions, :dependencies,
7
+ :optional_dependencies, :required_dependencies
8
+
9
+ attr :conductor, :input, :state, :registry, :run_list
10
+
11
+ def initialize conductor, run_list, input
12
+ @conductor = conductor
13
+ @input = input.dup
14
+ @run_list = run_list
15
+ @registry = conductor.build_registry self
16
+ @state = registry.merge input
17
+ end
18
+
19
+ def perform
20
+ ensure_inputs_are_present!
21
+ run_list.each do |name, node| process name, node end
22
+ rescue => error
23
+ publish :error_raised, error
24
+ raise error
25
+ end
26
+
27
+ def process name, node
28
+ input = input_for node
29
+ publish :node_entered, name, input
30
+ output = perform_node node
31
+ publish :node_exited, name, output
32
+ state.merge! output
33
+ end
34
+
35
+ def perform_node node
36
+ Movement.perform node, self
37
+ end
38
+
39
+ def ensure_inputs_are_present!
40
+ has_dep = state.method :[]
41
+ missing_input = required_dependencies.reject &has_dep
42
+ raise MissingInputError.new missing_input unless missing_input.empty?
43
+ end
44
+
45
+ def input_for node
46
+ state.reject do |key, val|
47
+ registry[key] == val or not node.dependencies.include? key
48
+ end
49
+ end
50
+
51
+ def extract_result result
52
+ state.fetch result
53
+ end
54
+
55
+ def publish event, *payload
56
+ changed
57
+ notify_observers event, *payload
58
+ end
59
+
60
+ def thread_pool
61
+ conductor.thread_pool
62
+ end
63
+
64
+ class Movement
65
+ def self.perform node, *args
66
+ if node.is_a? Operation
67
+ klass = EmbeddedOperation
68
+ else
69
+ klass = node.collection ? CollectionMovement : self
70
+ end
71
+ instance = klass.new node, *args
72
+ node.process instance.perform
73
+ end
74
+
75
+ attr :context, :node, :performance
76
+
77
+ def initialize node, performance
78
+ @node = node
79
+ @performance = performance
80
+ @context = build_context performance
81
+ end
82
+
83
+ def perform
84
+ context.perform
85
+ end
86
+
87
+ def build_context performance
88
+ node.build_context performance.state
89
+ end
90
+ end
91
+
92
+ class CollectionMovement < Movement
93
+ def perform
94
+ batch, output = prepare_collection
95
+ jobs = enqueue_jobs batch do |result, index| output[index] = result end
96
+ jobs.each &:wait
97
+ output
98
+ end
99
+
100
+ def enqueue_jobs batch, &block
101
+ batch.map.with_index do |element, index|
102
+ enqueue_job element, index, &block
103
+ end
104
+ end
105
+
106
+ def enqueue_job element, index
107
+ performance.thread_pool.enqueue do
108
+ result = context.perform element
109
+ yield [result, index]
110
+ end
111
+ end
112
+
113
+ def prepare_collection
114
+ batch = context.fetch_collection
115
+ output = [nil] * batch.size
116
+ [batch, output]
117
+ end
118
+ end
119
+
120
+ class EmbeddedOperation < Movement
121
+ def perform
122
+ super
123
+ context.state.select do |k,_| k == node.result end
124
+ end
125
+
126
+ def build_context performance
127
+ conductor = performance.registry[:conductor]
128
+ copy_observers = conductor.method :copy_observers
129
+ node.start_performance conductor, input, &copy_observers
130
+ end
131
+
132
+ def input
133
+ performance.state
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,83 @@
1
+ module Orchestra
2
+ class Recording
3
+ attr :input, :output, :services
4
+
5
+ def initialize
6
+ @services = Hash.new do |hsh, service_name| hsh[service_name] = [] end
7
+ end
8
+
9
+ def update event_name, *args
10
+ case event_name
11
+ when :service_accessed then
12
+ service_name, recording = args
13
+ @services[service_name] << recording
14
+ when :operation_entered then
15
+ _, @input = args
16
+ when :operation_exited then
17
+ _, @output = args
18
+ else
19
+ end
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ :input => input,
25
+ :output => output,
26
+ :service_recordings => services,
27
+ }
28
+ end
29
+
30
+ def self.replay operation, input, service_recordings
31
+ replayed_services = {}
32
+ service_recordings.each do |svc, service_recording|
33
+ replayed_services[svc] = Playback.build service_recording
34
+ end
35
+ conductor = Conductor.new replayed_services
36
+ conductor.perform operation, input
37
+ end
38
+
39
+ class Playback < BasicObject
40
+ attr :mocks
41
+
42
+ def initialize mocks
43
+ @mocks = mocks
44
+ end
45
+
46
+ def respond_to? meth
47
+ mocks.has_key? meth
48
+ end
49
+
50
+ def self.build service_recording
51
+ factory = Factory.new
52
+ factory.build service_recording
53
+ end
54
+
55
+ class Factory
56
+ attr :klass, :mocks
57
+
58
+ def initialize
59
+ @klass = Class.new Playback
60
+ @mocks = Hash.new do |hsh, meth| hsh[meth] = {} end
61
+ end
62
+
63
+ def build service_recording
64
+ record = method :<<
65
+ service_recording.each &record
66
+ klass.new mocks
67
+ end
68
+
69
+ def << record
70
+ method = record[:method].to_sym
71
+ unless klass.instance_methods.include? method
72
+ klass.send :define_method, method do |*args| mocks[method][args] end
73
+ end
74
+ mocks[method][record[:input]] = record[:output]
75
+ end
76
+
77
+ def singleton
78
+ singleton = class << instance ; self end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,171 @@
1
+ module Orchestra
2
+ class RunList
3
+ def self.build nodes, result, input_names
4
+ builder = Builder.new result, input_names
5
+ builder.merge! nodes
6
+ builder.build
7
+ end
8
+
9
+ include Enumerable
10
+
11
+ def initialize nodes
12
+ @nodes = nodes
13
+ @nodes.freeze
14
+ freeze
15
+ end
16
+
17
+ def each &block
18
+ return to_enum :each unless block_given?
19
+ @nodes.each &block
20
+ end
21
+
22
+ def node_names
23
+ @nodes.keys
24
+ end
25
+
26
+ def dependencies
27
+ optional = collect_from_nodes :optional_dependencies
28
+ required = collect_from_nodes :required_dependencies
29
+ (optional + required).uniq
30
+ end
31
+
32
+ def optional_dependencies
33
+ collect_from_nodes :optional_dependencies
34
+ end
35
+
36
+ def provisions
37
+ collect_from_nodes :provisions
38
+ end
39
+
40
+ def required_dependencies
41
+ required_deps = collect_from_nodes :required_dependencies
42
+ required_deps - optional_dependencies - provisions
43
+ end
44
+
45
+ private
46
+
47
+ def collect_from_nodes method_name
48
+ set = @nodes.each_with_object Set.new do |(_, node), set|
49
+ deps = node.public_send method_name
50
+ deps.each &set.method(:<<)
51
+ end
52
+ set.to_a.tap &:sort!
53
+ end
54
+
55
+ class Builder
56
+ attr :input_names, :result
57
+
58
+ def initialize result, input_names = []
59
+ @input_names = input_names
60
+ @nodes_hash = {}
61
+ @required = [result]
62
+ @result = result
63
+ freeze
64
+ end
65
+
66
+ def merge! nodes
67
+ nodes.each do |name, node|
68
+ self[name] = node
69
+ end
70
+ end
71
+
72
+ def []= name, node
73
+ @nodes_hash[name] = node
74
+ end
75
+
76
+ def node_names
77
+ @nodes_hash.keys
78
+ end
79
+
80
+ def nodes
81
+ @nodes_hash.values
82
+ end
83
+
84
+ def build
85
+ sort!
86
+ prune!
87
+ RunList.new @nodes_hash
88
+ end
89
+
90
+ def sort!
91
+ sorter = Sorter.new @nodes_hash
92
+ sorter.sort!
93
+ end
94
+
95
+ def prune!
96
+ nodes.reverse_each.with_object [] do |node, removed|
97
+ removed.<< remove node and next unless required? node
98
+ require node
99
+ end
100
+ end
101
+
102
+ def remove node
103
+ @nodes_hash.reject! do |_, n| n == node end
104
+ node
105
+ end
106
+
107
+ def require node
108
+ supplied_by_input = input_names.method :include?
109
+ deps = node.required_dependencies.reject &supplied_by_input
110
+ @required.concat deps
111
+ true
112
+ end
113
+
114
+ def required? node
115
+ required = @required.method :include?
116
+ node.provisions.any? &required
117
+ end
118
+
119
+ class Sorter
120
+ include TSort
121
+
122
+ def initialize nodes_hash
123
+ @nodes = nodes_hash
124
+ end
125
+
126
+ def sort!
127
+ build_dependency_tree
128
+ tsort.each do |name|
129
+ @nodes[name] = @nodes.delete name
130
+ end
131
+ rescue TSort::Cyclic
132
+ raise CircularDependencyError.new
133
+ end
134
+
135
+ def build_dependency_tree
136
+ @hsh = @nodes.each_with_object Hash.new do |(name, node), hsh|
137
+ hsh[name] = build_dependencies_for node
138
+ end
139
+ end
140
+
141
+ def build_dependencies_for node
142
+ node.required_dependencies.each_with_object Set.new do |dep, set|
143
+ provider = provider_for dep
144
+ set << provider if provider
145
+ end
146
+ end
147
+
148
+ def tsort_each_node(&block)
149
+ @hsh.each_key &block
150
+ end
151
+
152
+ def tsort_each_child(name, &block)
153
+ deps = @hsh.fetch name
154
+ deps.each &block
155
+ end
156
+
157
+ def provider_for dep
158
+ @nodes.each do |name, node|
159
+ provisions = effective_provisions_for node, dep
160
+ return name if provisions.include? dep
161
+ end
162
+ nil
163
+ end
164
+
165
+ def effective_provisions_for node, dep
166
+ node.optional_dependencies | node.provisions
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,163 @@
1
+ module Orchestra
2
+ class ThreadPool
3
+ def self.build count
4
+ instance = new
5
+ instance.count = count
6
+ instance
7
+ end
8
+
9
+ def self.default
10
+ build 1
11
+ end
12
+
13
+ attr :queue, :timeout
14
+
15
+ def initialize args = {}
16
+ @timeout, _ = Util.extract_key_args args, :timeout_ms => 1000
17
+ @threads = Set.new
18
+ @dead_pool = Set.new
19
+ @pool_lock = Mutex.new
20
+ @queue = Queue.new
21
+ @jobs = {}
22
+ end
23
+
24
+ def enqueue &work
25
+ job = Job.new work
26
+ job.add_observer self
27
+ while_locked do queue << job end
28
+ job
29
+ end
30
+
31
+ def perform &work
32
+ job = enqueue &work
33
+ job.wait
34
+ end
35
+
36
+ def count
37
+ threads.size
38
+ end
39
+
40
+ def count= new_count
41
+ while_locked do
42
+ loop do
43
+ case @threads.size <=> new_count
44
+ when 0 then return
45
+ when -1 then add_thread!
46
+ when 1 then remove_thread!
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def add_thread
53
+ while_locked do add_thread! end
54
+ end
55
+
56
+ def remove_thread
57
+ while_locked do remove_thread! end
58
+ end
59
+
60
+ def shutdown
61
+ self.count = 0
62
+ end
63
+
64
+ def status
65
+ while_locked do @threads.map &:status end
66
+ end
67
+
68
+ def threads
69
+ while_locked do @threads end
70
+ end
71
+
72
+ def update event, *;
73
+ reap_dead_pool if event == :failed
74
+ end
75
+
76
+ def with_timeout &block
77
+ Timeout.timeout Rational(timeout, 1000), &block
78
+ end
79
+
80
+ private
81
+
82
+ def add_thread!
83
+ wait_for_thread_count_to_change do
84
+ thr = Thread.new &method(:thread_loop)
85
+ @threads << thr
86
+ end
87
+ true
88
+ end
89
+
90
+ def reap_dead_pool
91
+ @dead_pool.each &:join
92
+ @dead_pool.clear
93
+ end
94
+
95
+ def remove_thread!
96
+ wait_for_thread_count_to_change do queue << :terminate end
97
+ sleep Rational(1, 10000) # FIXME
98
+ true
99
+ end
100
+
101
+ class Job
102
+ include Observable
103
+
104
+ attr :block, :error
105
+
106
+ def initialize block
107
+ @block = block
108
+ @output_queue = Queue.new
109
+ end
110
+
111
+ def done?
112
+ not @output_queue.empty?
113
+ end
114
+
115
+ def perform
116
+ @output_queue.push block.call
117
+ end
118
+
119
+ def set_error error
120
+ @error = error
121
+ @output_queue.push Failed
122
+ end
123
+
124
+ def wait
125
+ result = @output_queue.pop
126
+ changed
127
+ if result == Failed
128
+ notify_observers :failed, error
129
+ raise error
130
+ else
131
+ notify_observers :finished, result
132
+ end
133
+ result
134
+ end
135
+
136
+ Failed = Module.new
137
+ end
138
+
139
+ def thread_loop
140
+ Thread.current.abort_on_exception = false
141
+ until (job = queue.pop) == :terminate
142
+ job.perform
143
+ end
144
+ rescue => error
145
+ add_thread!
146
+ job.set_error error
147
+ ensure
148
+ @threads.delete Thread.current
149
+ @dead_pool << Thread.current
150
+ end
151
+
152
+ def wait_for_thread_count_to_change
153
+ old_count = queue.num_waiting
154
+ yield
155
+ ensure
156
+ Thread.pass while queue.num_waiting == old_count
157
+ end
158
+
159
+ def while_locked &block
160
+ @pool_lock.synchronize do with_timeout &block end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,98 @@
1
+ module Orchestra
2
+ module Util
3
+ extend self
4
+
5
+ def extract_key_args hsh, *args
6
+ defaults, args = extract_hash args
7
+ unknown_args = hsh.keys - (args + defaults.keys)
8
+ missing_args = args - hsh.keys
9
+ unless unknown_args.empty? and missing_args.empty?
10
+ raise ArgumentError, key_arg_error(unknown_args, missing_args)
11
+ end
12
+ (args + defaults.keys).map do |arg|
13
+ hsh.fetch arg do defaults.fetch arg end
14
+ end
15
+ end
16
+
17
+ def extract_hash ary
18
+ if ary.last.is_a? Hash
19
+ hsh = ary.pop
20
+ else
21
+ hsh = {}
22
+ end
23
+ [hsh, ary]
24
+ end
25
+
26
+ def recursively_symbolize obj
27
+ case obj
28
+ when Array
29
+ obj.map &method(:recursively_symbolize)
30
+ when Hash then
31
+ obj.each_with_object Hash.new do |(k, v), out_hsh|
32
+ out_hsh[k.to_sym] = recursively_symbolize v
33
+ end
34
+ else obj
35
+ end
36
+ end
37
+
38
+ def to_lazy_thunk obj
39
+ if obj.respond_to? :to_proc and not obj.is_a? Symbol
40
+ obj
41
+ else
42
+ Proc.new do obj end
43
+ end
44
+ end
45
+
46
+ def to_camel_case str
47
+ str = "_#{str}"
48
+ str.gsub!(%r{_[a-z]}) { |snake| snake.slice(1).upcase }
49
+ str.gsub!('/', '::')
50
+ str
51
+ end
52
+
53
+ def to_snake_case str
54
+ str = str.gsub '::', '/'
55
+ # Convert FOOBar => FooBar
56
+ str.gsub! %r{[[:upper:]]{2,}} do |uppercase|
57
+ bit = uppercase[0]
58
+ bit << uppercase[1...-1].downcase
59
+ bit << uppercase[-1]
60
+ bit
61
+ end
62
+ # Convert FooBar => foo_bar
63
+ str.gsub! %r{[[:lower:]][[:upper:]]+[[:lower:]]} do |camel|
64
+ bit = camel[0]
65
+ bit << '_'
66
+ bit << camel[1..-1].downcase
67
+ end
68
+ str.downcase!
69
+ str
70
+ end
71
+
72
+ def demodulize str
73
+ split_namespaces(str).last
74
+ end
75
+
76
+ def deconstantize str
77
+ split_namespaces(str).first
78
+ end
79
+
80
+ private
81
+
82
+ def split_namespaces name
83
+ name.split '::'
84
+ end
85
+
86
+ def key_arg_error unknown, missing
87
+ str = "bad arguments. "
88
+ if unknown.any?
89
+ str.concat " unknown: #{unknown.join ', '}"
90
+ str.concat "; " if missing.any?
91
+ end
92
+ if missing.any?
93
+ str.concat " missing: #{missing.join ', '}"
94
+ end
95
+ str
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,3 @@
1
+ module Orchestra
2
+ VERSION = "0.9.0" unless defined? VERSION
3
+ end