ntl-orchestra 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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