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,119 @@
1
+ module Orchestra
2
+ class Conductor
3
+ attr :observers, :services, :thread_pool
4
+
5
+ def initialize services = {}
6
+ @services = services
7
+ @thread_pool = ThreadPool.new
8
+ @observers = Set.new
9
+ self.thread_count = Configuration.thread_count
10
+ end
11
+
12
+ def perform operation, input = {}
13
+ operation.perform self, input do |performance|
14
+ copy_observers performance
15
+ yield performance if block_given?
16
+ end
17
+ end
18
+
19
+ def record *args
20
+ recording = Recording.new
21
+ add_observer recording
22
+ perform *args do |performance|
23
+ performance.add_observer recording
24
+ end
25
+ recording
26
+ ensure
27
+ delete_observer recording
28
+ end
29
+
30
+ def add_observer observer
31
+ observers << observer
32
+ end
33
+
34
+ def delete_observer observer
35
+ observers.delete observer
36
+ end
37
+
38
+ def copy_observers observable
39
+ add_observer = observable.method :add_observer
40
+ observers.each &add_observer
41
+ end
42
+
43
+ def build_registry observable
44
+ hsh = { :conductor => self }
45
+ services.each_with_object hsh do |(service_name, _), hsh|
46
+ service = resolve_service observable, service_name
47
+ hsh[service_name] = service if service
48
+ end
49
+ end
50
+
51
+ def resolve_service observable, service_name
52
+ return nil unless services.has_key? service_name
53
+ service = Util.to_lazy_thunk services[service_name]
54
+ recording = ServiceRecorder.new observable, service_name
55
+ recording.wrap service.call self
56
+ end
57
+
58
+ def thread_count
59
+ @thread_pool.count
60
+ end
61
+
62
+ def thread_count= new_count
63
+ @thread_pool.count = new_count
64
+ end
65
+
66
+ class ServiceRecorder
67
+ attr :observable, :service_name
68
+
69
+ def initialize observable, service_name
70
+ @observable = observable
71
+ @service_name = service_name
72
+ @record = []
73
+ end
74
+
75
+ def << record
76
+ observable.changed
77
+ observable.notify_observers :service_accessed, service_name, record
78
+ @record << record
79
+ end
80
+
81
+ def each &block
82
+ @record.each &block
83
+ end
84
+
85
+ def wrap raw_service
86
+ Wrapper.new raw_service, self
87
+ end
88
+
89
+ class Wrapper < Delegator
90
+ attr_accessor :service
91
+ alias_method :__getobj__, :service
92
+ alias_method :__setobj__, :service=
93
+
94
+ def initialize service, recording
95
+ super service
96
+ @recording = recording
97
+ end
98
+
99
+ def kind_of? klass
100
+ super or service.kind_of? klass
101
+ end
102
+
103
+ def method_missing meth, *args
104
+ super.tap do |result|
105
+ @recording << {
106
+ :method => meth.to_s,
107
+ :input => args,
108
+ :output => result,
109
+ }
110
+ end
111
+ end
112
+
113
+ def inspect
114
+ "#<#{self.class.name} service=#{service.inspect}>"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,12 @@
1
+ module Orchestra
2
+ module Configuration
3
+ extend self
4
+
5
+ attr_accessor :thread_count
6
+
7
+ def reset
8
+ self.thread_count = 1
9
+ end
10
+ reset
11
+ end
12
+ end
@@ -0,0 +1,72 @@
1
+ module Orchestra
2
+ module DSL
3
+ module Nodes
4
+ class Builder
5
+ attr_accessor :collection, :perform_block
6
+
7
+ attr :defaults, :dependencies, :provisions
8
+
9
+ def initialize
10
+ @defaults = {}
11
+ @dependencies = []
12
+ @provisions = []
13
+ end
14
+
15
+ def build_node
16
+ Node::InlineNode.new(
17
+ :collection => collection,
18
+ :defaults => defaults,
19
+ :dependencies => dependencies,
20
+ :perform_block => perform_block,
21
+ :provides => provisions,
22
+ )
23
+ end
24
+ end
25
+
26
+ class Context < BasicObject
27
+ def self.evaluate builder, &block
28
+ context = new builder
29
+ context.instance_eval &block
30
+ end
31
+
32
+ attr :collection, :perform
33
+
34
+ def initialize builder
35
+ @builder = builder
36
+ end
37
+
38
+ def depends_on *dependencies
39
+ defaults, dependencies = Util.extract_hash dependencies
40
+ @builder.dependencies.concat dependencies
41
+ defaults.each do |key, default|
42
+ @builder.dependencies << key
43
+ @builder.defaults[key] = Util.to_lazy_thunk default
44
+ end
45
+ end
46
+
47
+ def modifies provision, args = {}
48
+ collection, _ = Util.extract_key_args args, :collection => false
49
+ if collection
50
+ iterates_over provision
51
+ else
52
+ depends_on provision
53
+ end
54
+ provides provision
55
+ end
56
+
57
+ def provides *provisions
58
+ @builder.provisions.concat provisions
59
+ end
60
+
61
+ def perform &block
62
+ @builder.perform_block = block
63
+ end
64
+
65
+ def iterates_over dependency
66
+ @builder.dependencies << dependency
67
+ @builder.collection = dependency
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,134 @@
1
+ module Orchestra
2
+ module DSL
3
+ class ObjectAdapter
4
+ def self.build_node object, args = {}
5
+ method_name = args.delete :method do :perform end
6
+ collection = args.delete :iterates_over
7
+ adapter_type = determine_type object, method_name
8
+ adapter = adapter_type.new object, method_name, collection
9
+ NodeFactory.build adapter, args
10
+ end
11
+
12
+ def self.determine_type object, method_name
13
+ if object.public_methods.include? method_name
14
+ SingletonAdapter
15
+ elsif object.kind_of? Class
16
+ ClassAdapter
17
+ else
18
+ SingletonAdapter
19
+ end
20
+ end
21
+
22
+ attr :collection, :object, :method_name
23
+
24
+ def initialize object, method_name, collection
25
+ @collection = collection
26
+ @method_name = method_name || :perform
27
+ @object = object
28
+ end
29
+
30
+ def build_context state
31
+ ExecutionContext.new self, state
32
+ end
33
+
34
+ def collection?
35
+ @collection ? true : false
36
+ end
37
+
38
+ def context_class
39
+ @context_class ||= Node.build_execution_context_class dependencies
40
+ end
41
+
42
+ def dependencies
43
+ [collection, *object_method.dependencies].compact
44
+ end
45
+ end
46
+
47
+ class SingletonAdapter < ObjectAdapter
48
+ def validate!
49
+ unless object.methods.include? method_name
50
+ raise NotImplementedError,
51
+ "#{object} does not implement method `#{method_name}'"
52
+ end
53
+ if collection?
54
+ raise ArgumentError,
55
+ "#{object} is a singleton; cannot iterate over collection #{collection.inspect}"
56
+ end
57
+ end
58
+
59
+ def perform state
60
+ deps = object_method.dependencies
61
+ input = state.select do |key, _| deps.include? key end
62
+ Invokr.invoke :method => method_name, :on => object, :with => input
63
+ end
64
+
65
+ def object_method
66
+ Invokr.query_method object.method method_name
67
+ end
68
+ end
69
+
70
+ class ClassAdapter < ObjectAdapter
71
+ def validate!
72
+ return if object.instance_methods.include? method_name
73
+ raise NotImplementedError,
74
+ "#{object} does not implement instance method `#{method_name}'"
75
+ end
76
+
77
+ def perform state, maybe_item = nil
78
+ instance = Invokr.inject object, :using => state
79
+ args = [method_name]
80
+ args << maybe_item if collection?
81
+ instance.public_send *args
82
+ end
83
+
84
+ def object_method
85
+ Invokr.query_method object.instance_method :initialize
86
+ end
87
+ end
88
+
89
+ class NodeFactory
90
+ def self.build *args
91
+ instance = new *args
92
+ instance.build_node
93
+ end
94
+
95
+ attr :adapter, :compact, :provides, :thread_count
96
+
97
+ def initialize adapter, args = {}
98
+ @adapter = adapter
99
+ @provides, @compact, @thread_count = Util.extract_key_args args,
100
+ :provides => nil, :compact => false, :thread_count => nil
101
+ end
102
+
103
+ def build_node
104
+ adapter.validate!
105
+ Node::DelegateNode.new adapter, build_node_args
106
+ end
107
+
108
+ def build_node_args
109
+ hsh = {
110
+ :dependencies => adapter.dependencies,
111
+ :provides => Array(provides),
112
+ }
113
+ hsh[:collection] = adapter.collection if adapter.collection?
114
+ hsh
115
+ end
116
+ end
117
+
118
+ class ExecutionContext
119
+ def initialize adapter, state
120
+ @__adapter__ = adapter
121
+ @__state__ = state
122
+ return unless adapter.collection?
123
+ self.singleton_class.send :define_method, :fetch_collection do
124
+ @__state__.fetch adapter.collection
125
+ end
126
+ end
127
+
128
+ def perform *args
129
+ @__adapter__.perform @__state__, *args
130
+ end
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,108 @@
1
+ module Orchestra
2
+ module DSL
3
+ module Operations
4
+ class Builder
5
+ attr_writer :command, :result
6
+
7
+ def initialize
8
+ @nodes = {}
9
+ end
10
+
11
+ def build_operation
12
+ raise ArgumentError, "Must supply a result" if @result.nil?
13
+ raise ArgumentError, "Must supply at least one node" if @nodes.empty?
14
+ Operation.new(
15
+ :command => @command,
16
+ :nodes => @nodes,
17
+ :result => @result,
18
+ )
19
+ end
20
+
21
+ def add_node name_or_object, args = {}, &block
22
+ name, node = case name_or_object
23
+ when nil then build_anonymous_node block
24
+ when Operation then build_embedded_operation_node name_or_object
25
+ when ::String, ::Symbol then build_inline_node name_or_object, block
26
+ else build_object_node name_or_object, args
27
+ end
28
+ node.provisions << name.to_sym if node.provisions.empty?
29
+ set_node name.to_sym, node
30
+ end
31
+
32
+ def set_node name, node
33
+ if @nodes.has_key? name
34
+ raise ArgumentError, "There are duplicate nodes named #{name.inspect}"
35
+ end
36
+ @nodes[name] = node
37
+ node.freeze
38
+ end
39
+
40
+ def build_anonymous_node block
41
+ node = Node::InlineNode.build &block
42
+ unless node.provisions.size == 1
43
+ raise ArgumentError, "Could not infer name for node from a provision"
44
+ end
45
+ name = node.provisions.fetch 0
46
+ [name, node]
47
+ end
48
+
49
+ def build_embedded_operation_node operation
50
+ name = object_name operation
51
+ [name || operation.result, operation]
52
+ end
53
+
54
+ def build_inline_node name, block
55
+ node = Node::InlineNode.build &block
56
+ [name, node]
57
+ end
58
+
59
+ def build_object_node object, args
60
+ name = object_name object
61
+ node = ObjectAdapter.build_node object, args
62
+ [name, node]
63
+ end
64
+
65
+ private
66
+
67
+ def object_name object
68
+ object.name and Util.to_snake_case Util.demodulize object.name
69
+ end
70
+ end
71
+
72
+ class Context < BasicObject
73
+ def self.evaluate builder, &block
74
+ context = new builder
75
+ context.instance_eval &block
76
+ end
77
+
78
+ attr :nodes
79
+
80
+ def initialize builder
81
+ @builder = builder
82
+ end
83
+
84
+ def node *args, &block
85
+ @builder.add_node *args, &block
86
+ nil
87
+ end
88
+
89
+ def result= result
90
+ @builder.result = result
91
+ nil
92
+ end
93
+
94
+ def result name = nil, &block
95
+ node = @builder.add_node name, &block
96
+ name ||= node.provisions.fetch 0
97
+ self.result = name
98
+ end
99
+
100
+ def finally name = :__finally__, &block
101
+ @builder.add_node name, &block
102
+ @builder.command = true
103
+ self.result = name
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,44 @@
1
+ module Orchestra
2
+ class Error < StandardError
3
+ def list_out list
4
+ list = list.map &:inspect
5
+ return list.fetch 0 if list.size == 1
6
+ list.fetch 0
7
+ second_to_last, last = list.slice! -2..-1
8
+ str = list.join ', '
9
+ str << ', ' unless str.empty?
10
+ str << "#{second_to_last} and #{last}"
11
+ str
12
+ end
13
+ end
14
+
15
+ class MissingProvisionError < Error
16
+ def initialize missing_provisions
17
+ @missing_provisions = missing_provisions
18
+ end
19
+
20
+ def to_s
21
+ "failed to supply output: #{list_out @missing_provisions}"
22
+ end
23
+ end
24
+
25
+ class CircularDependencyError < Error
26
+ def to_s
27
+ "Circular dependency detected! Check your dependencies/provides"
28
+ end
29
+ end
30
+
31
+ class MissingInputError < Error
32
+ def initialize missing_input
33
+ @missing_input = missing_input
34
+ end
35
+
36
+ def count
37
+ @missing_input.count
38
+ end
39
+
40
+ def to_s
41
+ "Missing input#{'s' unless count == 1} #{list_out @missing_input}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,61 @@
1
+ module Orchestra
2
+ class Node
3
+ class Output
4
+ attr :hsh, :node, :raw
5
+
6
+ def self.process node, raw
7
+ instance = new node, raw
8
+ instance.massage
9
+ instance.hsh
10
+ end
11
+
12
+ def initialize node, raw
13
+ @node = node
14
+ @raw = raw
15
+ end
16
+
17
+ def provisions
18
+ node.provisions
19
+ end
20
+
21
+ def collection?
22
+ node.collection?
23
+ end
24
+
25
+ def massage
26
+ @raw.compact! if collection?
27
+ @hsh = coerce_to_hash
28
+ prune
29
+ ensure_all_provisions_supplied!
30
+ end
31
+
32
+ def coerce_to_hash
33
+ return Hash(raw) unless provisions.size == 1
34
+ return raw if all_provisions_supplied? raw if raw.kind_of? Hash
35
+ raise MissingProvisionError.new provisions if raw.nil?
36
+ { provisions.first => raw }
37
+ end
38
+
39
+ def all_provisions_supplied? hsh = @hsh
40
+ provisions.all? &included_in_output(hsh)
41
+ end
42
+
43
+ def missing_provisions
44
+ provisions.reject &included_in_output
45
+ end
46
+
47
+ def included_in_output hsh = @hsh
48
+ hsh.keys.method :include?
49
+ end
50
+
51
+ def prune
52
+ hsh.select! do |key, _| provisions.include? key end
53
+ end
54
+
55
+ def ensure_all_provisions_supplied!
56
+ return if all_provisions_supplied?
57
+ raise MissingProvisionError.new missing_provisions
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,130 @@
1
+ module Orchestra
2
+ class Node
3
+ autoload :Output, "orchestra/node/output"
4
+
5
+ attr :collection, :dependencies, :provisions
6
+
7
+ def initialize args = {}
8
+ @provisions,
9
+ @collection,
10
+ @dependencies = Util.extract_key_args(
11
+ args,
12
+ :provides => [],
13
+ :collection => nil,
14
+ :dependencies => [],
15
+ )
16
+ end
17
+
18
+ def required_dependencies
19
+ dependencies - optional_dependencies
20
+ end
21
+
22
+ def optional_dependencies
23
+ defaults.keys
24
+ end
25
+
26
+ def collection?
27
+ collection ? true : false
28
+ end
29
+
30
+ def perform input = {}
31
+ performance = Performance.new Conductor.new, {}, input
32
+ Performance::Movement.perform self, performance
33
+ end
34
+
35
+ def process raw_output
36
+ Output.process self, raw_output
37
+ end
38
+
39
+ class DelegateNode < Node
40
+ attr :adapter
41
+
42
+ def initialize adapter, args = {}
43
+ @adapter = adapter
44
+ super args
45
+ end
46
+
47
+ def build_context input
48
+ adapter.build_context input
49
+ end
50
+
51
+ def optional_dependencies
52
+ adapter.object_method.optional_dependencies
53
+ end
54
+ end
55
+
56
+ class InlineNode < Node
57
+ def self.build &block
58
+ builder = DSL::Nodes::Builder.new
59
+ DSL::Nodes::Context.evaluate builder, &block
60
+ builder.build_node
61
+ end
62
+
63
+ attr :context_class, :defaults, :perform_block
64
+
65
+ def initialize args = {}
66
+ @defaults = args.delete :defaults do {} end
67
+ @perform_block = args.fetch :perform_block
68
+ args.delete :perform_block
69
+ super args
70
+ @context_class = build_execution_context_class
71
+ validate!
72
+ end
73
+
74
+ def validate!
75
+ unless perform_block
76
+ raise ArgumentError, "expected inline node to define a perform block"
77
+ end
78
+ end
79
+
80
+ def build_execution_context_class
81
+ context = Class.new ExecutionContext
82
+ context.class_exec dependencies, collection do |deps, collection|
83
+ deps.each do |dep| define_dependency dep end
84
+ alias_method :fetch_collection, collection if collection
85
+ end
86
+ context
87
+ end
88
+
89
+ def build_context input
90
+ state = apply_defaults input
91
+ execution_context = context_class.new state, perform_block
92
+ end
93
+
94
+ def apply_defaults input
95
+ defaults.each do |key, thunk|
96
+ next if input.has_key? key
97
+ input[key] = thunk.call
98
+ end
99
+ input
100
+ end
101
+
102
+ def optional_dependencies
103
+ defaults.keys
104
+ end
105
+
106
+ class ExecutionContext
107
+ def self.define_dependency dep
108
+ define_method dep do
109
+ ivar = "@#{dep}"
110
+ return instance_variable_get ivar if instance_variable_defined? ivar
111
+ instance_variable_set ivar, @__state__[dep]
112
+ end
113
+ end
114
+
115
+ def initialize state, perform_block
116
+ @__perform_block__ = perform_block
117
+ @__state__ = state
118
+ end
119
+
120
+ def perform item = nil
121
+ if @__perform_block__.arity == 0
122
+ instance_exec &@__perform_block__
123
+ else
124
+ instance_exec item, &@__perform_block__
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end