arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,61 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'pluggability'
5
+ require 'arborist' unless defined?( Arborist )
6
+
7
+
8
+ # The representation of activity in the manager; events are broadcast when
9
+ # node state changes, when they're updated, and when various other operational
10
+ # actions take place, e.g., the node tree gets reloaded.
11
+ class Arborist::Event
12
+ extend Pluggability
13
+
14
+
15
+ # Pluggability API -- look for events under the specified prefix
16
+ plugin_prefixes 'arborist/event'
17
+
18
+
19
+ ### Create a new event with the specified +payload+ data.
20
+ def initialize( payload )
21
+ payload = payload.clone unless payload.nil?
22
+ @payload = payload
23
+ end
24
+
25
+
26
+ ######
27
+ public
28
+ ######
29
+
30
+ # The event payload specific to the event type
31
+ attr_reader :payload
32
+
33
+
34
+ ### Return the type of the event.
35
+ def type
36
+ return self.class.name.
37
+ sub( /.*::/, '' ).
38
+ gsub( /([a-z])([A-Z])/, '\1.\2' ).
39
+ downcase
40
+ end
41
+
42
+
43
+ ### Match operator -- returns +true+ if the other object matches this event.
44
+ def match( object )
45
+ return object.respond_to?( :event_type ) &&
46
+ ( object.event_type.nil? || object.event_type == self.type )
47
+ end
48
+ alias_method :=~, :match
49
+
50
+
51
+ ### Return the event as a Hash.
52
+ def to_hash
53
+ return {
54
+ 'type' => self.type,
55
+ 'data' => self.payload
56
+ }
57
+ end
58
+
59
+ end # class Arborist::Event
60
+
61
+
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node_matching'
6
+
7
+
8
+ # An event generated when a node is manually ACKed.
9
+ class Arborist::Event::NodeAcked < Arborist::Event
10
+ include Arborist::Event::NodeMatching
11
+
12
+
13
+ ### Create a new NodeAcked event for the specified +node+ and +ack_info+.
14
+ def initialize( node, ack_info )
15
+ super
16
+ end
17
+
18
+ end # class Arborist::Event::NodeAcked
@@ -0,0 +1,20 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node_matching'
6
+
7
+
8
+ # An event sent when one or more attributes of a node changes.
9
+ class Arborist::Event::NodeDelta < Arborist::Event
10
+ include Arborist::Event::NodeMatching
11
+
12
+
13
+ ### Create a new NodeDelta event for the specified +node+. The +delta+
14
+ ### is a Hash of:
15
+ ### attribute_name => [ old_value, new_value ]
16
+ def initialize( node, delta )
17
+ super # Overridden for the documentation
18
+ end
19
+
20
+ end # class Arborist::Event::NodeDelta
@@ -0,0 +1,34 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+
6
+
7
+ # A mixin which adds common functionality to events which related to an
8
+ # Arborist::Node.
9
+ module Arborist::Event::NodeMatching
10
+
11
+ ### Strip and save the node argument to the constructor.
12
+ def initialize( node, payload=nil )
13
+ @node = node
14
+ super( payload )
15
+ end
16
+
17
+
18
+ ######
19
+ public
20
+ ######
21
+
22
+ # The node that generated the event
23
+ attr_reader :node
24
+
25
+
26
+ ### Returns +true+ if the specified +object+ matches this event.
27
+ def match( object )
28
+ return super &&
29
+ object.respond_to?( :criteria ) && self.node.matches?( object.criteria )
30
+ end
31
+
32
+
33
+ end # module Arborist::Event::NodeMatching
34
+
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arborist/event' unless defined?( Arborist::Event )
4
+ require 'arborist/event/node_matching'
5
+
6
+
7
+ # An event sent on every node update, regardless of whether or not the update resulted in
8
+ # any changes
9
+ class Arborist::Event::NodeUpdate < Arborist::Event
10
+ include Arborist::Event::NodeMatching
11
+
12
+
13
+ ### Use the node data as this event's payload.
14
+ def payload
15
+ return self.node.to_hash
16
+ end
17
+
18
+
19
+ end # class Arborist::Event::NodeUpdate
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arborist/node' unless defined?( Arborist::Node )
4
+
5
+
6
+ # An event sent when the manager reloads the node tree.
7
+ class Arborist::Event::SysReloaded < Arborist::Event
8
+
9
+
10
+ ### Create a NodeUpdate event for the specified +node+.
11
+ def initialize( payload=Time.now )
12
+ super
13
+ end
14
+
15
+ end # class Arborist::Event::NodeUpdate
@@ -0,0 +1,21 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+
5
+ # Arborist namespace
6
+ module Arborist
7
+
8
+ class ClientError < RuntimeError; end
9
+
10
+ class RequestError < ClientError
11
+
12
+ def initialize( reason )
13
+ super( "Invalid request (#{reason})" )
14
+ end
15
+
16
+ end
17
+
18
+ class ServerError < RuntimeError; end
19
+
20
+ end # module Arborist
21
+
@@ -0,0 +1,508 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'pathname'
5
+ require 'configurability'
6
+ require 'loggability'
7
+ require 'rbczmq'
8
+
9
+ require 'arborist' unless defined?( Arborist )
10
+ require 'arborist/node'
11
+ require 'arborist/mixins'
12
+
13
+
14
+ # The main Arborist process -- responsible for coordinating all other activity.
15
+ class Arborist::Manager
16
+ extend Configurability,
17
+ Loggability,
18
+ Arborist::MethodUtilities
19
+
20
+ # Signals the manager responds to
21
+ QUEUE_SIGS = [
22
+ :INT, :TERM, :HUP, :USR1,
23
+ # :TODO: :QUIT, :WINCH, :USR2, :TTIN, :TTOU
24
+ ]
25
+
26
+ # The number of seconds to wait between checks for incoming signals
27
+ SIGNAL_INTERVAL = 0.5
28
+
29
+
30
+ ##
31
+ # Use the Arborist logger
32
+ log_to :arborist
33
+
34
+
35
+ #
36
+ # Instance methods
37
+ #
38
+
39
+ ### Create a new Arborist::Manager.
40
+ def initialize
41
+ @root = Arborist::Node.create( :root )
42
+ @nodes = {
43
+ '_' => @root,
44
+ }
45
+ @subscriptions = {}
46
+ @tree_built = false
47
+
48
+ @tree_sock = @event_sock = nil
49
+ @signal_timer = nil
50
+ @start_time = nil
51
+
52
+ Thread.main[:signal_queue] = []
53
+ @zmq_loop = nil
54
+
55
+ @api_handler = nil
56
+ @event_publisher = nil
57
+ end
58
+
59
+
60
+ ######
61
+ public
62
+ ######
63
+
64
+ ##
65
+ # The root node of the tree.
66
+ attr_accessor :root
67
+
68
+ ##
69
+ # The Hash of all loaded Nodes, keyed by their identifier
70
+ attr_accessor :nodes
71
+
72
+ ##
73
+ # The Hash of all Subscriptions, keyed by their subscription ID
74
+ attr_accessor :subscriptions
75
+
76
+ ##
77
+ # The time at which the manager began running.
78
+ attr_accessor :start_time
79
+
80
+ ##
81
+ # The ZMQ::Handler that manages the IO for the Tree API
82
+ attr_reader :api_handler
83
+
84
+ ##
85
+ # The ZMQ::Handler that manages the IO for the event-publication API.
86
+ attr_reader :event_publisher
87
+
88
+ ##
89
+ # The ZMQ::Loop that will/is acting as the main loop.
90
+ attr_reader :zmq_loop
91
+
92
+ ##
93
+ # Flag for marking when the tree is built successfully the first time
94
+ attr_predicate_accessor :tree_built
95
+
96
+
97
+ #
98
+ # :section: Startup/Shutdown
99
+ #
100
+
101
+ ### Setup sockets and start the event loop.
102
+ def run
103
+ self.log.info "Getting ready to start the manager."
104
+ self.setup_sockets
105
+ self.set_signal_handlers
106
+ self.start_accepting_requests
107
+
108
+ return self # For chaining
109
+ ensure
110
+ self.restore_signal_handlers
111
+ if @zmq_loop
112
+ self.log.debug "Unregistering sockets."
113
+ @zmq_loop.remove( @tree_sock )
114
+ @tree_sock.pollable.close
115
+ @zmq_loop.remove( @event_sock )
116
+ @event_sock.pollable.close
117
+ end
118
+
119
+ self.log.debug "Resetting ZMQ context"
120
+ Arborist.reset_zmq_context
121
+ end
122
+
123
+
124
+ ### Returns true if the Manager is running.
125
+ def running?
126
+ return @zmq_loop && @zmq_loop.running?
127
+ end
128
+
129
+
130
+ ### Start a loop, accepting a request and handling it.
131
+ def start_accepting_requests
132
+ self.log.debug "Starting the main loop"
133
+
134
+ @zmq_loop = ZMQ::Loop.new
135
+
136
+ @api_handler = Arborist::Manager::TreeAPI.new( @tree_sock, self )
137
+ @tree_sock.handler = @api_handler
138
+ @zmq_loop.register( @tree_sock )
139
+
140
+ @event_publisher = Arborist::Manager::EventPublisher.new( @event_sock, self, @zmq_loop )
141
+ @event_sock.handler = @event_publisher
142
+ @zmq_loop.register( @event_sock )
143
+
144
+ self.setup_signal_timer
145
+ self.start_time = Time.now
146
+
147
+ self.log.debug "Manager running."
148
+ @zmq_loop.start
149
+ end
150
+
151
+
152
+ ### Create the ZMQ API socket if necessary.
153
+ def setup_sockets
154
+ self.log.debug "Setting up sockets"
155
+ @tree_sock = self.setup_tree_socket
156
+ @event_sock = self.setup_event_socket
157
+ end
158
+
159
+
160
+ ### Set up the ZMQ REP socket for the Tree API.
161
+ def setup_tree_socket
162
+ sock = Arborist.zmq_context.socket( :REP )
163
+ self.log.debug " binding the tree API socket (%#0x) to %p" %
164
+ [ sock.object_id * 2, Arborist.tree_api_url ]
165
+ sock.linger = 0
166
+ sock.bind( Arborist.tree_api_url )
167
+ return ZMQ::Pollitem.new( sock, ZMQ::POLLIN|ZMQ::POLLOUT )
168
+ end
169
+
170
+
171
+ ### Set up the ZMQ PUB socket for published events.
172
+ def setup_event_socket
173
+ sock = Arborist.zmq_context.socket( :PUB )
174
+ self.log.debug " binding the event socket (%#0x) to %p" %
175
+ [ sock.object_id * 2, Arborist.event_api_url ]
176
+ sock.linger = 0
177
+ sock.bind( Arborist.event_api_url )
178
+ return ZMQ::Pollitem.new( sock, ZMQ::POLLOUT )
179
+ end
180
+
181
+
182
+ ### Restart the manager
183
+ def restart
184
+ raise NotImplementedError
185
+ end
186
+
187
+
188
+ ### Stop the manager.
189
+ def stop
190
+ self.log.info "Stopping the manager."
191
+ self.ignore_signals
192
+ self.cancel_signal_timer
193
+ @zmq_loop.stop if @zmq_loop
194
+ end
195
+
196
+
197
+ #
198
+ # :section: Signal Handling
199
+ # These methods set up some behavior for starting, restarting, and stopping
200
+ # your application when a signal is received. If you don't want signals to
201
+ # be handled, override #set_signal_handlers with an empty method.
202
+ #
203
+
204
+ ### Set up a periodic ZMQ timer to check for queued signals and handle them.
205
+ def setup_signal_timer
206
+ @signal_timer = ZMQ::Timer.new( SIGNAL_INTERVAL, 0, self.method(:process_signal_queue) )
207
+ @zmq_loop.register_timer( @signal_timer )
208
+ end
209
+
210
+
211
+ ### Disable the timer that checks for incoming signals
212
+ def cancel_signal_timer
213
+ if @signal_timer
214
+ @signal_timer.cancel
215
+ @zmq_loop.cancel_timer( @signal_timer )
216
+ end
217
+ end
218
+
219
+
220
+ ### Set up signal handlers for common signals that will shut down, restart, etc.
221
+ def set_signal_handlers
222
+ self.log.debug "Setting up deferred signal handlers."
223
+ QUEUE_SIGS.each do |sig|
224
+ Signal.trap( sig ) { Thread.main[:signal_queue] << sig }
225
+ end
226
+ end
227
+
228
+
229
+ ### Set all signal handlers to ignore.
230
+ def ignore_signals
231
+ self.log.debug "Ignoring signals."
232
+ QUEUE_SIGS.each do |sig|
233
+ Signal.trap( sig, :IGNORE )
234
+ end
235
+ end
236
+
237
+
238
+ ### Set the signal handlers back to their defaults.
239
+ def restore_signal_handlers
240
+ self.log.debug "Restoring default signal handlers."
241
+ QUEUE_SIGS.each do |sig|
242
+ Signal.trap( sig, :DEFAULT )
243
+ end
244
+ end
245
+
246
+ ### Handle any queued signals.
247
+ def process_signal_queue
248
+ # Look for any signals that arrived and handle them
249
+ while sig = Thread.main[:signal_queue].shift
250
+ self.handle_signal( sig )
251
+ end
252
+ end
253
+
254
+
255
+ ### Handle signals.
256
+ def handle_signal( sig )
257
+ self.log.debug "Handling signal %s" % [ sig ]
258
+ case sig
259
+ when :INT, :TERM
260
+ self.on_termination_signal( sig )
261
+
262
+ when :HUP
263
+ self.on_hangup_signal( sig )
264
+
265
+ when :USR1
266
+ self.on_user1_signal( sig )
267
+
268
+ else
269
+ self.log.warn "Unhandled signal %s" % [ sig ]
270
+ end
271
+
272
+ end
273
+
274
+
275
+ ### Handle a TERM signal. Shuts the handler down after handling any current request/s. Also
276
+ ### aliased to #on_interrupt_signal.
277
+ def on_termination_signal( signo )
278
+ self.log.warn "Terminated (%p)" % [ signo ]
279
+ self.stop
280
+ end
281
+ alias_method :on_interrupt_signal, :on_termination_signal
282
+
283
+
284
+ ### Handle a HUP signal. The default is to restart the handler.
285
+ def on_hangup_signal( signo )
286
+ self.log.warn "Hangup (%p)" % [ signo ]
287
+ self.restart
288
+ end
289
+
290
+
291
+ ### Handle a USR1 signal. Writes a message to the log by default.
292
+ def on_user1_signal( signo )
293
+ self.log.info "Checkpoint: User signal."
294
+ end
295
+
296
+
297
+ #
298
+ # :section: Tree API
299
+ #
300
+
301
+ ### Add nodes yielded from the specified +enumerator+ into the manager's
302
+ ### tree.
303
+ def load_tree( enumerator )
304
+ enumerator.each do |node|
305
+ self.add_node( node )
306
+ end
307
+ self.build_tree
308
+ end
309
+
310
+
311
+ ### Build the tree out of all the loaded nodes.
312
+ def build_tree
313
+ self.log.info "Building tree from %d loaded nodes." % [ self.nodes.length ]
314
+ self.nodes.each do |identifier, node|
315
+ next if node.operational?
316
+ self.link_node_to_parent( node )
317
+ end
318
+ self.tree_built = true
319
+ end
320
+
321
+
322
+ ### Link the specified +node+ to its parent. Raises an error if the specified +node+'s
323
+ ### parent is not yet loaded.
324
+ def link_node_to_parent( node )
325
+ parent_id = node.parent || '_'
326
+ parent_node = self.nodes[ parent_id ] or
327
+ raise "no parent '%s' node loaded for %p" % [ parent_id, node ]
328
+
329
+ self.log.debug "adding %p as a child of %p" % [ node, parent_node ]
330
+ parent_node.add_child( node )
331
+ end
332
+
333
+
334
+ ### Add the specified +node+ to the Manager.
335
+ def add_node( node )
336
+ identifier = node.identifier
337
+
338
+ unless self.nodes[identifier].equal?( node )
339
+ self.remove_node( self.nodes[identifier] )
340
+ self.nodes[ identifier ] = node
341
+ end
342
+
343
+ self.log.debug "Linking node %p to its parent" % [ node ]
344
+ self.link_node_to_parent( node ) if self.tree_built?
345
+ end
346
+
347
+
348
+ ### Remove a +node+ from the Manager. The +node+ can either be the Arborist::Node to
349
+ ### remove, or the identifier of a node.
350
+ def remove_node( node )
351
+ node = self.nodes[ node ] unless node.is_a?( Arborist::Node )
352
+ return unless node
353
+
354
+ raise "Can't remove an operational node" if node.operational?
355
+
356
+ self.log.info "Removing node %p" % [ node ]
357
+ node.children.each do |identifier, child_node|
358
+ self.remove_node( child_node )
359
+ end
360
+
361
+ if parent_node = self.nodes[ node.parent || '_' ]
362
+ parent_node.remove_child( node )
363
+ end
364
+
365
+ return self.nodes.delete( node.identifier )
366
+ end
367
+
368
+
369
+ ### Update the node with the specified +identifier+ with the given +new_properties+
370
+ ### and propagate any events generated by the update to the node and its ancestors.
371
+ def update_node( identifier, new_properties )
372
+ unless (( node = self.nodes[identifier] ))
373
+ self.log.warn "Update for non-existent node %p ignored." % [ identifier ]
374
+ return []
375
+ end
376
+
377
+ events = node.update( new_properties )
378
+ self.propagate_events( node, events )
379
+ end
380
+
381
+
382
+ ### Traverse the node tree and fetch the specified +return_values+ from any nodes which
383
+ ### match the given +filter+, skipping downed nodes and all their children
384
+ ### unless +include_down+ is set. If +return_values+ is set to +nil+, then all
385
+ ### values from the node will be returned.
386
+ def fetch_matching_node_states( filter, return_values, include_down=false )
387
+ nodes_iter = if include_down
388
+ self.all_nodes
389
+ else
390
+ self.reachable_nodes
391
+ end
392
+
393
+ states = nodes_iter.
394
+ select {|node| node.matches?(filter) }.
395
+ each_with_object( {} ) do |node, hash|
396
+ hash[ node.identifier ] = node.fetch_values( return_values )
397
+ end
398
+
399
+ return states
400
+ end
401
+
402
+
403
+ ### Return the duration the manager has been running in seconds.
404
+ def uptime
405
+ return 0 unless self.start_time
406
+ return Time.now - self.start_time
407
+ end
408
+
409
+
410
+ ### Return the number of nodes in the manager's tree.
411
+ def nodecount
412
+ return self.nodes.length
413
+ end
414
+
415
+
416
+ ### Return an Array of the identifiers of all nodes in the manager's tree.
417
+ def nodelist
418
+ return self.nodes.keys
419
+ end
420
+
421
+
422
+ #
423
+ # Tree-traversal API
424
+ #
425
+
426
+ ### Yield each node in a depth-first traversal of the manager's tree
427
+ ### to the specified +block+, or return an Enumerator if no block is given.
428
+ def all_nodes( &block )
429
+ iter = self.enumerator_for( self.root )
430
+ return iter.each( &block ) if block
431
+ return iter
432
+ end
433
+
434
+
435
+ ### Yield each node that is not down to the specified +block+, or return
436
+ ### an Enumerator if no block is given.
437
+ def reachable_nodes( &block )
438
+ iter = self.enumerator_for( self.root ) do |node|
439
+ !node.down?
440
+ end
441
+ return iter.each( &block ) if block
442
+ return iter
443
+ end
444
+
445
+
446
+ ### Return an enumerator for the specified +node+.
447
+ def enumerator_for( start_node, &filter )
448
+ return Enumerator.new do |yielder|
449
+ traverse = ->( node ) do
450
+ if !filter || filter.call( node )
451
+ yielder.yield( node )
452
+ node.each( &traverse )
453
+ end
454
+ end
455
+ traverse.call( start_node )
456
+ end
457
+ end
458
+
459
+
460
+
461
+ #
462
+ # Event API
463
+ #
464
+
465
+ ### Create a subscription for the node with the specified +identifier+ and
466
+ ### +event_pattern+, using the given +criteria+ when considering an event.
467
+ def create_subscription( identifier, event_pattern, criteria )
468
+ identifier ||= '_'
469
+
470
+ node = self.nodes[ identifier ] or raise ArgumentError, "no such node %p" % [ identifier ]
471
+ sub = Arborist::Subscription.new( self.event_publisher, event_pattern, criteria )
472
+
473
+ self.log.debug "Registering subscription %p" % [ sub ]
474
+ node.add_subscription( sub )
475
+ self.log.debug " adding '%s' to the subscriptions hash." % [ sub.id ]
476
+ self.subscriptions[ sub.id ] = node
477
+ self.log.debug " subscriptions hash: %#0x" % [ self.subscriptions.object_id ]
478
+
479
+ return sub
480
+ end
481
+
482
+
483
+ ### Remove the subscription with the specified +subscription_identifier+ from the node
484
+ ### it's attached to and from the manager, and return it.
485
+ def remove_subscription( subscription_identifier )
486
+ node = self.subscriptions.delete( subscription_identifier ) or return nil
487
+ return node.remove_subscription( subscription_identifier )
488
+ end
489
+
490
+
491
+ ### Propagate one or more +events+ to the specified +node+ and its ancestors in the tree,
492
+ ### publishing them to matching subscriptions belonging to the nodes along the way.
493
+ def propagate_events( node, *events )
494
+ self.log.debug "Propagating %d events to node %s" % [ events.length, node.identifier ]
495
+ node.publish_events( *events )
496
+
497
+ if node.parent
498
+ parent = self.nodes[ node.parent ] or raise "couldn't find parent %p of node %p!" %
499
+ [ node.parent, node.identifier ]
500
+ self.propagate_events( parent, *events )
501
+ end
502
+ end
503
+
504
+
505
+ require 'arborist/manager/tree_api'
506
+ require 'arborist/manager/event_publisher'
507
+
508
+ end # class Arborist::Manager