arborist 0.0.1.pre20160106113421

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