arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

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 (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/ChangeLog +426 -1
  5. data/Manifest.txt +17 -2
  6. data/Nodes.md +70 -0
  7. data/Protocol.md +68 -9
  8. data/README.md +3 -5
  9. data/Rakefile +4 -1
  10. data/TODO.md +52 -20
  11. data/lib/arborist.rb +19 -6
  12. data/lib/arborist/cli.rb +39 -25
  13. data/lib/arborist/client.rb +97 -4
  14. data/lib/arborist/command/client.rb +2 -1
  15. data/lib/arborist/command/start.rb +51 -5
  16. data/lib/arborist/dependency.rb +286 -0
  17. data/lib/arborist/event.rb +7 -2
  18. data/lib/arborist/event/{node_matching.rb → node.rb} +11 -5
  19. data/lib/arborist/event/node_acked.rb +5 -7
  20. data/lib/arborist/event/node_delta.rb +30 -3
  21. data/lib/arborist/event/node_disabled.rb +16 -0
  22. data/lib/arborist/event/node_down.rb +10 -0
  23. data/lib/arborist/event/node_quieted.rb +11 -0
  24. data/lib/arborist/event/node_unknown.rb +10 -0
  25. data/lib/arborist/event/node_up.rb +10 -0
  26. data/lib/arborist/event/node_update.rb +2 -11
  27. data/lib/arborist/event/sys_node_added.rb +10 -0
  28. data/lib/arborist/event/sys_node_removed.rb +10 -0
  29. data/lib/arborist/exceptions.rb +4 -0
  30. data/lib/arborist/manager.rb +188 -18
  31. data/lib/arborist/manager/event_publisher.rb +1 -1
  32. data/lib/arborist/manager/tree_api.rb +92 -13
  33. data/lib/arborist/mixins.rb +17 -0
  34. data/lib/arborist/monitor.rb +10 -1
  35. data/lib/arborist/monitor/socket.rb +123 -2
  36. data/lib/arborist/monitor_runner.rb +6 -5
  37. data/lib/arborist/node.rb +420 -94
  38. data/lib/arborist/node/ack.rb +72 -0
  39. data/lib/arborist/node/host.rb +43 -8
  40. data/lib/arborist/node/resource.rb +73 -0
  41. data/lib/arborist/node/root.rb +6 -0
  42. data/lib/arborist/node/service.rb +89 -22
  43. data/lib/arborist/observer.rb +1 -1
  44. data/lib/arborist/subscription.rb +11 -6
  45. data/spec/arborist/client_spec.rb +93 -5
  46. data/spec/arborist/dependency_spec.rb +375 -0
  47. data/spec/arborist/event/node_delta_spec.rb +66 -0
  48. data/spec/arborist/event/node_down_spec.rb +84 -0
  49. data/spec/arborist/event/node_spec.rb +59 -0
  50. data/spec/arborist/event/node_update_spec.rb +14 -3
  51. data/spec/arborist/event_spec.rb +3 -3
  52. data/spec/arborist/manager/tree_api_spec.rb +295 -3
  53. data/spec/arborist/manager_spec.rb +240 -57
  54. data/spec/arborist/monitor_spec.rb +26 -3
  55. data/spec/arborist/node/ack_spec.rb +74 -0
  56. data/spec/arborist/node/host_spec.rb +79 -0
  57. data/spec/arborist/node/resource_spec.rb +56 -0
  58. data/spec/arborist/node/service_spec.rb +68 -2
  59. data/spec/arborist/node_spec.rb +288 -11
  60. data/spec/arborist/subscription_spec.rb +23 -14
  61. data/spec/arborist_spec.rb +0 -4
  62. data/spec/data/observers/webservices.rb +10 -2
  63. data/spec/spec_helper.rb +8 -0
  64. metadata +58 -15
  65. metadata.gz.sig +0 -0
  66. data/LICENSE +0 -29
@@ -1,6 +1,7 @@
1
1
  # -*- ruby -*-
2
2
  #encoding: utf-8
3
3
 
4
+ require 'loggability'
4
5
  require 'pluggability'
5
6
  require 'arborist' unless defined?( Arborist )
6
7
 
@@ -9,12 +10,16 @@ require 'arborist' unless defined?( Arborist )
9
10
  # node state changes, when they're updated, and when various other operational
10
11
  # actions take place, e.g., the node tree gets reloaded.
11
12
  class Arborist::Event
12
- extend Pluggability
13
+ extend Pluggability,
14
+ Loggability
13
15
 
14
16
 
15
17
  # Pluggability API -- look for events under the specified prefix
16
18
  plugin_prefixes 'arborist/event'
17
19
 
20
+ # Loggability API -- log to the Arborist logger
21
+ log_to :arborist
22
+
18
23
 
19
24
  ### Create a new event with the specified +payload+ data.
20
25
  def initialize( payload )
@@ -49,7 +54,7 @@ class Arborist::Event
49
54
 
50
55
 
51
56
  ### Return the event as a Hash.
52
- def to_hash
57
+ def to_h
53
58
  return {
54
59
  'type' => self.type,
55
60
  'data' => self.payload
@@ -4,9 +4,8 @@
4
4
  require 'arborist/event' unless defined?( Arborist::Event )
5
5
 
6
6
 
7
- # A mixin which adds common functionality to events which related to an
8
- # Arborist::Node.
9
- module Arborist::Event::NodeMatching
7
+ # A base class for events which are related to an Arborist::Node.
8
+ class Arborist::Event::Node < Arborist::Event
10
9
 
11
10
  ### Strip and save the node argument to the constructor.
12
11
  def initialize( node, payload=nil )
@@ -26,12 +25,19 @@ module Arborist::Event::NodeMatching
26
25
  ### Returns +true+ if the specified +object+ matches this event.
27
26
  def match( object )
28
27
  return super &&
29
- object.respond_to?( :criteria ) && self.node.matches?( object.criteria )
28
+ object.respond_to?( :criteria ) && self.node.matches?( object.criteria ) &&
29
+ ( !object.respond_to?(:negative_criteria) || !self.node.matches?(object.negative_criteria) )
30
+ end
31
+
32
+
33
+ ### Use the node data as this event's payload.
34
+ def payload
35
+ return self.node.to_h
30
36
  end
31
37
 
32
38
 
33
39
  ### Inject the node identifier into the generated hash.
34
- def to_hash
40
+ def to_h
35
41
  return super.merge( identifier: self.node.identifier )
36
42
  end
37
43
 
@@ -2,17 +2,15 @@
2
2
  #encoding: utf-8
3
3
 
4
4
  require 'arborist/event' unless defined?( Arborist::Event )
5
- require 'arborist/event/node_matching'
5
+ require 'arborist/event/node'
6
6
 
7
7
 
8
8
  # An event generated when a node is manually ACKed.
9
- class Arborist::Event::NodeAcked < Arborist::Event
10
- include Arborist::Event::NodeMatching
9
+ class Arborist::Event::NodeAcked < Arborist::Event::Node
11
10
 
12
-
13
- ### Create a new NodeAcked event for the specified +node+ and +ack_info+.
14
- def initialize( node, ack_info )
15
- super
11
+ ### Create a new NodeAcked event for the specified +node+.
12
+ def initialize( node )
13
+ super( node, node.ack.to_h )
16
14
  end
17
15
 
18
16
  end # class Arborist::Event::NodeAcked
@@ -2,12 +2,13 @@
2
2
  #encoding: utf-8
3
3
 
4
4
  require 'arborist/event' unless defined?( Arborist::Event )
5
- require 'arborist/event/node_matching'
5
+ require 'arborist/event/node'
6
+ require 'arborist/mixins'
6
7
 
7
8
 
8
9
  # An event sent when one or more attributes of a node changes.
9
- class Arborist::Event::NodeDelta < Arborist::Event
10
- include Arborist::Event::NodeMatching
10
+ class Arborist::Event::NodeDelta < Arborist::Event::Node
11
+ include Arborist::HashUtilities
11
12
 
12
13
 
13
14
  ### Create a new NodeDelta event for the specified +node+. The +delta+
@@ -17,4 +18,30 @@ class Arborist::Event::NodeDelta < Arborist::Event
17
18
  super # Overridden for the documentation
18
19
  end
19
20
 
21
+
22
+ ### Overridden so delta events only contain the diff of attributes that changed.
23
+ def payload
24
+ return @payload
25
+ end
26
+
27
+
28
+ ### Returns +true+ if the specified +object+ matches this event.
29
+ def match( object )
30
+ return super &&
31
+ object.respond_to?( :criteria ) && self.delta_matches?( object.criteria )
32
+ end
33
+
34
+
35
+ ### Returns +true+ if the 'delta' value of the specified +criteria+ (which
36
+ ### must respond to .all?) matches the delta this event represents.
37
+ def delta_matches?( criteria )
38
+ delta_criteria = criteria['delta'] || {}
39
+ self.log.debug "Matching event against delta criteria: %p" % [ delta_criteria ]
40
+
41
+ return delta_criteria.all? do |key, val|
42
+ self.log.debug " matching %p: %p against %p" % [ key, val, self.payload ]
43
+ hash_matches( self.payload, key, val )
44
+ end.tap {|match| self.log.debug " event delta %s match." % [ match ? "DID" : "did not"] }
45
+ end
46
+
20
47
  end # class Arborist::Event::NodeDelta
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # An event generated when a node is manually disabled
9
+ class Arborist::Event::NodeDisabled < Arborist::Event::Node
10
+
11
+ ### Create a new NodeDisabled event for the specified +node+.
12
+ def initialize( node )
13
+ super( node, node.ack.to_h )
14
+ end
15
+
16
+ end # class Arborist::Event::NodeDisabled
@@ -0,0 +1,10 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # An event generated when a node goes down.
9
+ class Arborist::Event::NodeDown < Arborist::Event::Node
10
+ end # class Arborist::Event::NodeDown
@@ -0,0 +1,11 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # An event generated when a node is quieted by one of its dependencies going
9
+ # down.
10
+ class Arborist::Event::NodeQuieted < Arborist::Event::Node
11
+ end # class Arborist::Event::NodeQuieted
@@ -0,0 +1,10 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # An event generated when a node transitions to an unknown state.
9
+ class Arborist::Event::NodeUnknown < Arborist::Event::Node
10
+ end # class Arborist::Event::NodeUnknown
@@ -0,0 +1,10 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # An event generated when a node comes up.
9
+ class Arborist::Event::NodeUp < Arborist::Event::Node
10
+ end # class Arborist::Event::NodeUp
@@ -1,19 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'arborist/event' unless defined?( Arborist::Event )
4
- require 'arborist/event/node_matching'
4
+ require 'arborist/event/node'
5
5
 
6
6
 
7
7
  # An event sent on every node update, regardless of whether or not the update resulted in
8
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
-
9
+ class Arborist::Event::NodeUpdate < Arborist::Event::Node
19
10
  end # class Arborist::Event::NodeUpdate
@@ -0,0 +1,10 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # A system event generated when a node is added to the tree.
9
+ class Arborist::Event::SysNodeAdded < Arborist::Event::Node
10
+ end # class Arborist::Event::SysNodeAdded
@@ -0,0 +1,10 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/event' unless defined?( Arborist::Event )
5
+ require 'arborist/event/node'
6
+
7
+
8
+ # A system event generated when a node is removed from the tree.
9
+ class Arborist::Event::SysNodeRemoved < Arborist::Event::Node
10
+ end # class Arborist::Event::SysNodeRemoved
@@ -17,5 +17,9 @@ module Arborist
17
17
 
18
18
  class ServerError < RuntimeError; end
19
19
 
20
+ class NodeError < RuntimeError; end
21
+
22
+ class ConfigError < RuntimeError; end
23
+
20
24
  end # module Arborist
21
25
 
@@ -2,6 +2,7 @@
2
2
  #encoding: utf-8
3
3
 
4
4
  require 'pathname'
5
+ require 'tempfile'
5
6
  require 'configurability'
6
7
  require 'loggability'
7
8
  require 'rbczmq'
@@ -26,11 +27,44 @@ class Arborist::Manager
26
27
  # The number of seconds to wait between checks for incoming signals
27
28
  SIGNAL_INTERVAL = 0.5
28
29
 
30
+ # Configurability API -- set config defaults
31
+ CONFIG_DEFAULTS = {
32
+ state_file: nil,
33
+ checkpoint_frequency: 30
34
+ }
35
+
29
36
 
30
- ##
31
37
  # Use the Arborist logger
32
38
  log_to :arborist
33
39
 
40
+ # Configurability API -- use the 'arborist' section
41
+ config_key :arborist
42
+
43
+
44
+ ##
45
+ # The Pathname of the file the manager's node tree state is saved to
46
+ singleton_attr_accessor :state_file
47
+
48
+ ##
49
+ # The number of seconds between automatic state checkpoints
50
+ singleton_attr_accessor :checkpoint_frequency
51
+
52
+
53
+ ### Configurability API -- configure the manager
54
+ def self::configure( config=nil )
55
+ config ||= {}
56
+ config = self.defaults.merge( config[:manager] || {} )
57
+
58
+ self.state_file = config[:state_file] && Pathname( config[:state_file] )
59
+
60
+ interval = config[:checkpoint_frequency].to_i
61
+ if interval && interval.nonzero?
62
+ self.checkpoint_frequency = interval
63
+ else
64
+ self.checkpoint_frequency = nil
65
+ end
66
+ end
67
+
34
68
 
35
69
  #
36
70
  # Instance methods
@@ -54,6 +88,7 @@ class Arborist::Manager
54
88
 
55
89
  @api_handler = nil
56
90
  @event_publisher = nil
91
+ @checkpoint_timer = nil
57
92
  end
58
93
 
59
94
 
@@ -114,8 +149,11 @@ class Arborist::Manager
114
149
  @tree_sock.pollable.close
115
150
  @zmq_loop.remove( @event_sock )
116
151
  @event_sock.pollable.close
152
+ @zmq_loop.cancel_timer( @checkpoint_timer ) if @checkpoint_timer
117
153
  end
118
154
 
155
+ self.save_node_states
156
+
119
157
  self.log.debug "Resetting ZMQ context"
120
158
  Arborist.reset_zmq_context
121
159
  end
@@ -141,6 +179,9 @@ class Arborist::Manager
141
179
  @event_sock.handler = @event_publisher
142
180
  @zmq_loop.register( @event_sock )
143
181
 
182
+ @checkpoint_timer = self.start_state_checkpointing
183
+ @zmq_loop.register_timer( @checkpoint_timer ) if @checkpoint_timer
184
+
144
185
  self.setup_signal_timer
145
186
  self.start_time = Time.now
146
187
 
@@ -194,6 +235,72 @@ class Arborist::Manager
194
235
  end
195
236
 
196
237
 
238
+ #
239
+ # :section: Node state saving/reloading
240
+ #
241
+
242
+ ### Write out the state of all the manager's nodes to the state_file if one is
243
+ ### configured.
244
+ def save_node_states
245
+ path = self.class.state_file or return
246
+ self.log.info "Saving current node state to %s" % [ path ]
247
+ tmpfile = Tempfile.create(
248
+ [path.basename.to_s.sub(path.extname, ''), path.extname],
249
+ path.dirname.to_s,
250
+ encoding: 'binary'
251
+ )
252
+ Marshal.dump( self.nodes, tmpfile )
253
+ tmpfile.close
254
+
255
+ File.rename( tmpfile.path, path.to_s )
256
+
257
+ rescue SystemCallError => err
258
+ self.log.error "%p while saving node state: %s" % [ err.class, err.message ]
259
+
260
+ ensure
261
+ File.unlink( tmpfile.path ) if tmpfile && File.exist?( tmpfile.path )
262
+ end
263
+
264
+
265
+ ### Attempt to restore the state of loaded node from the configured state file. Returns
266
+ ### true if it succeeded, or false if a state file wasn't configured, doesn't
267
+ ### exist, isn't readable, or couldn't be unmarshalled.
268
+ def restore_node_states
269
+ path = self.class.state_file or return false
270
+ return false unless path.readable?
271
+
272
+ self.log.info "Restoring node state from %s" % [ path ]
273
+ nodes = Marshal.load( path.open('r:binary') )
274
+
275
+ nodes.each do |identifier, saved_node|
276
+ self.log.debug "Loaded node: %p" % [ identifier ]
277
+ if (( current_node = self.nodes[ identifier ] ))
278
+ self.log.debug "Restoring state of the %p node." % [ identifier ]
279
+ current_node.restore( saved_node )
280
+ else
281
+ self.log.info "Not restoring state for the %s node: not present in the loaded tree." %
282
+ [ identifier ]
283
+ end
284
+ end
285
+
286
+ return true
287
+ end
288
+
289
+
290
+ ### Start a timer that will save a snapshot of the node tree's state to the state
291
+ ### file on a configured interval if it's configured.
292
+ def start_state_checkpointing
293
+ return nil unless self.class.state_file
294
+ interval = self.class.checkpoint_frequency or return nil
295
+
296
+ self.log.info "Setting up node state checkpoint every %ds" % [ interval ]
297
+ checkpoint_timer = ZMQ::Timer.new( interval, 0 ) do
298
+ self.save_node_states
299
+ end
300
+ return checkpoint_timer
301
+ end
302
+
303
+
197
304
  #
198
305
  # :section: Signal Handling
199
306
  # These methods set up some behavior for starting, restarting, and stopping
@@ -292,6 +399,7 @@ class Arborist::Manager
292
399
  ### Handle a USR1 signal. Writes a message to the log.
293
400
  def on_user1_signal( signo )
294
401
  self.log.info "Checkpoint: User signal."
402
+ self.save_node_states
295
403
  end
296
404
 
297
405
 
@@ -312,17 +420,27 @@ class Arborist::Manager
312
420
  ### Build the tree out of all the loaded nodes.
313
421
  def build_tree
314
422
  self.log.info "Building tree from %d loaded nodes." % [ self.nodes.length ]
315
- self.nodes.each do |identifier, node|
423
+
424
+ # Build primary tree structure
425
+ self.nodes.each_value do |node|
316
426
  next if node.operational?
317
427
  self.link_node_to_parent( node )
318
428
  end
319
429
  self.tree_built = true
430
+
431
+ # Set up secondary dependencies
432
+ self.nodes.each_value do |node|
433
+ node.register_secondary_dependencies( self )
434
+ end
435
+
436
+ self.restore_node_states
320
437
  end
321
438
 
322
439
 
323
440
  ### Link the specified +node+ to its parent. Raises an error if the specified +node+'s
324
441
  ### parent is not yet loaded.
325
442
  def link_node_to_parent( node )
443
+ self.log.debug "Linking node %p to its parent" % [ node ]
326
444
  parent_id = node.parent || '_'
327
445
  parent_node = self.nodes[ parent_id ] or
328
446
  raise "no parent '%s' node loaded for %p" % [ parent_id, node ]
@@ -336,13 +454,24 @@ class Arborist::Manager
336
454
  def add_node( node )
337
455
  identifier = node.identifier
338
456
 
339
- unless self.nodes[identifier].equal?( node )
457
+ unless self.nodes[ identifier ].equal?( node )
340
458
  self.remove_node( self.nodes[identifier] )
341
459
  self.nodes[ identifier ] = node
342
460
  end
343
461
 
344
- self.log.debug "Linking node %p to its parent" % [ node ]
345
- self.link_node_to_parent( node ) if self.tree_built?
462
+ if self.tree_built?
463
+ self.link_node( node )
464
+ node.handle_event( Arborist::Event.create(:sys_node_added, node) )
465
+ end
466
+ end
467
+
468
+
469
+ ### Link the node to other nodes in the tree.
470
+ def link_node( node )
471
+ raise "Tree is not built yet" unless self.tree_built?
472
+
473
+ self.link_node_to_parent( node )
474
+ node.register_secondary_dependencies( self )
346
475
  end
347
476
 
348
477
 
@@ -355,6 +484,7 @@ class Arborist::Manager
355
484
  raise "Can't remove an operational node" if node.operational?
356
485
 
357
486
  self.log.info "Removing node %p" % [ node ]
487
+ node.handle_event( Arborist::Event.create(:sys_node_removed, node) )
358
488
  node.children.each do |identifier, child_node|
359
489
  self.remove_node( child_node )
360
490
  end
@@ -384,7 +514,7 @@ class Arborist::Manager
384
514
  ### match the given +filter+, skipping downed nodes and all their children
385
515
  ### unless +include_down+ is set. If +return_values+ is set to +nil+, then all
386
516
  ### values from the node will be returned.
387
- def fetch_matching_node_states( filter, return_values, include_down=false )
517
+ def fetch_matching_node_states( filter, return_values, include_down=false, negative_filter={} )
388
518
  nodes_iter = if include_down
389
519
  self.all_nodes
390
520
  else
@@ -393,6 +523,7 @@ class Arborist::Manager
393
523
 
394
524
  states = nodes_iter.
395
525
  select {|node| node.matches?(filter) }.
526
+ reject {|node| !negative_filter.empty? && node.matches?(negative_filter) }.
396
527
  each_with_object( {} ) do |node, hash|
397
528
  hash[ node.identifier ] = node.fetch_values( return_values )
398
529
  end
@@ -437,14 +568,14 @@ class Arborist::Manager
437
568
  ### an Enumerator if no block is given.
438
569
  def reachable_nodes( &block )
439
570
  iter = self.enumerator_for( self.root ) do |node|
440
- !(node.down? || node.disabled?)
571
+ !(node.down? || node.disabled? || node.quieted?)
441
572
  end
442
573
  return iter.each( &block ) if block
443
574
  return iter
444
575
  end
445
576
 
446
577
 
447
- ### Return an enumerator for the specified +node+.
578
+ ### Return an enumerator for the specified +start_node+.
448
579
  def enumerator_for( start_node, &filter )
449
580
  return Enumerator.new do |yielder|
450
581
  traverse = ->( node ) do
@@ -458,24 +589,64 @@ class Arborist::Manager
458
589
  end
459
590
 
460
591
 
592
+ ### Return a +depth+ limited enumerator for the specified +start_node+.
593
+ def depth_limited_enumerator_for( start_node, depth, &filter )
594
+ return Enumerator.new do |yielder|
595
+ traverse = ->( node, current_depth ) do
596
+ self.log.debug "Enumerating nodes from %s at depth: %p" %
597
+ [ node.identifier, current_depth ]
598
+
599
+ if !filter || filter.call( node )
600
+ yielder.yield( node )
601
+ node.each do |child|
602
+ traverse[ child, current_depth - 1 ]
603
+ end if current_depth > 0
604
+ end
605
+ end
606
+ traverse.call( start_node, depth )
607
+ end
608
+ end
609
+
610
+
611
+ ### Return an Array of all nodes below the specified +node+.
612
+ def descendants_for( node )
613
+ return self.enumerator_for( node ).to_a
614
+ end
615
+
616
+
617
+ ### Return the Array of all nodes above the specified +node+.
618
+ def ancestors_for( node )
619
+ parent_id = node.parent or return []
620
+ parent = self.nodes[ parent_id ]
621
+ return [ parent ] + self.ancestors_for( parent )
622
+ end
623
+
461
624
 
462
625
  #
463
626
  # Event API
464
627
  #
465
628
 
466
- ### Create a subscription for the node with the specified +identifier+ and
467
- ### +event_pattern+, using the given +criteria+ when considering an event.
468
- def create_subscription( identifier, event_pattern, criteria )
629
+ ### Add the specified +subscription+ to the node corresponding with the given +identifier+.
630
+ def subscribe( identifier, subscription )
469
631
  identifier ||= '_'
470
-
471
632
  node = self.nodes[ identifier ] or raise ArgumentError, "no such node %p" % [ identifier ]
472
- sub = Arborist::Subscription.new( self.event_publisher, event_pattern, criteria )
473
633
 
474
- self.log.debug "Registering subscription %p" % [ sub ]
475
- node.add_subscription( sub )
476
- self.log.debug " adding '%s' to the subscriptions hash." % [ sub.id ]
477
- self.subscriptions[ sub.id ] = node
634
+ self.log.debug "Registering subscription %p" % [ subscription ]
635
+ node.add_subscription( subscription )
636
+ self.log.debug " adding '%s' to the subscriptions hash." % [ subscription.id ]
637
+ self.subscriptions[ subscription.id ] = node
478
638
  self.log.debug " subscriptions hash: %#0x" % [ self.subscriptions.object_id ]
639
+ end
640
+
641
+
642
+ ### Create a subscription that publishes to the Manager's event publisher for
643
+ ### the node with the specified +identifier+ and +event_pattern+, using the
644
+ ### given +criteria+ when considering an event.
645
+ def create_subscription( identifier, event_pattern, criteria )
646
+ sub = Arborist::Subscription.new( event_pattern, criteria ) do |*args|
647
+ self.event_publisher.publish( *args )
648
+ end
649
+ self.subscribe( identifier, sub )
479
650
 
480
651
  return sub
481
652
  end
@@ -492,7 +663,6 @@ class Arborist::Manager
492
663
  ### Propagate one or more +events+ to the specified +node+ and its ancestors in the tree,
493
664
  ### publishing them to matching subscriptions belonging to the nodes along the way.
494
665
  def propagate_events( node, *events )
495
- self.log.info "Propagating %d events to node %s" % [ events.length, node.identifier ]
496
666
  node.publish_events( *events )
497
667
 
498
668
  if node.parent