arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

Sign up to get free protection for your applications and to get access to all the features.
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