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,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