arborist 0.2.0.pre20170519125456 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +670 -1
  5. data/History.md +67 -0
  6. data/Manifest.txt +9 -6
  7. data/README.md +1 -3
  8. data/Rakefile +39 -4
  9. data/TODO.md +22 -31
  10. data/lib/arborist.rb +9 -2
  11. data/lib/arborist/cli.rb +67 -85
  12. data/lib/arborist/client.rb +125 -59
  13. data/lib/arborist/command/ack.rb +86 -0
  14. data/lib/arborist/command/reset.rb +48 -0
  15. data/lib/arborist/command/start.rb +11 -1
  16. data/lib/arborist/command/summary.rb +173 -0
  17. data/lib/arborist/command/tree.rb +215 -0
  18. data/lib/arborist/command/watch.rb +22 -22
  19. data/lib/arborist/dependency.rb +24 -4
  20. data/lib/arborist/event.rb +18 -2
  21. data/lib/arborist/event/node.rb +6 -2
  22. data/lib/arborist/event/node_warn.rb +16 -0
  23. data/lib/arborist/manager.rb +179 -48
  24. data/lib/arborist/mixins.rb +11 -0
  25. data/lib/arborist/monitor.rb +29 -17
  26. data/lib/arborist/monitor/connection_batching.rb +293 -0
  27. data/lib/arborist/monitor/socket.rb +101 -167
  28. data/lib/arborist/monitor_runner.rb +101 -24
  29. data/lib/arborist/node.rb +297 -68
  30. data/lib/arborist/node/ack.rb +1 -1
  31. data/lib/arborist/node/host.rb +26 -5
  32. data/lib/arborist/node/resource.rb +14 -5
  33. data/lib/arborist/node/root.rb +12 -3
  34. data/lib/arborist/node/service.rb +29 -26
  35. data/lib/arborist/node_subscription.rb +65 -0
  36. data/lib/arborist/observer.rb +8 -0
  37. data/lib/arborist/observer/action.rb +6 -0
  38. data/lib/arborist/subscription.rb +22 -16
  39. data/lib/arborist/tree_api.rb +7 -2
  40. data/spec/arborist/client_spec.rb +157 -51
  41. data/spec/arborist/dependency_spec.rb +21 -0
  42. data/spec/arborist/event/node_spec.rb +5 -0
  43. data/spec/arborist/event_spec.rb +3 -3
  44. data/spec/arborist/manager_spec.rb +626 -347
  45. data/spec/arborist/mixins_spec.rb +19 -0
  46. data/spec/arborist/monitor/socket_spec.rb +1 -2
  47. data/spec/arborist/monitor_runner_spec.rb +81 -29
  48. data/spec/arborist/monitor_spec.rb +89 -14
  49. data/spec/arborist/node/host_spec.rb +68 -0
  50. data/spec/arborist/node/resource_spec.rb +2 -0
  51. data/spec/arborist/node/root_spec.rb +13 -0
  52. data/spec/arborist/node/service_spec.rb +9 -0
  53. data/spec/arborist/node_spec.rb +673 -111
  54. data/spec/arborist/node_subscription_spec.rb +54 -0
  55. data/spec/arborist/observer/action_spec.rb +6 -0
  56. data/spec/arborist/observer_runner_spec.rb +8 -1
  57. data/spec/arborist/tree_api_spec.rb +111 -8
  58. data/spec/data/monitors/pings.rb +0 -11
  59. data/spec/data/monitors/port_checks.rb +0 -9
  60. data/spec/data/nodes/sidonie.rb +1 -0
  61. data/spec/data/nodes/vhosts.rb +23 -0
  62. data/spec/data/nodes/yevaud.rb +4 -2
  63. data/spec/spec_helper.rb +71 -1
  64. metadata +91 -28
  65. metadata.gz.sig +0 -0
  66. data/Events.md +0 -35
  67. data/Monitors.md +0 -155
  68. data/Nodes.md +0 -70
  69. data/Observers.md +0 -72
  70. data/Protocol.md +0 -276
  71. data/Tutorial.md +0 -8
@@ -221,10 +221,22 @@ class Arborist::Dependency
221
221
  ### Return an English description of why this dependency is not met. If it is
222
222
  ### met, returns +nil+.
223
223
  def down_reason
224
- ids = self.down_identifiers
225
- subdeps = self.down_subdeps
224
+ parts = [
225
+ self.down_primary_reasons,
226
+ self.down_secondary_reasons
227
+ ].compact
228
+
229
+ return nil if parts.empty?
230
+
231
+ return parts.join( '; ' )
232
+ end
226
233
 
227
- return nil if ids.empty? && subdeps.empty?
234
+
235
+ ### Return an English description of why any first tier dependencies are not met,
236
+ ### or +nil+ if there are none.
237
+ def down_primary_reasons
238
+ ids = self.down_identifiers
239
+ return nil if ids.empty?
228
240
 
229
241
  msg = nil
230
242
  case self.behavior
@@ -245,8 +257,16 @@ class Arborist::Dependency
245
257
  else
246
258
  raise "Don't know how to build a description of down behavior for %p" % [ self.behavior ]
247
259
  end
260
+ end
261
+
262
+
263
+ ### Return an English description of why any subdependencies are not met,
264
+ ### or +nil+ if there are none.
265
+ def down_secondary_reasons
266
+ subdeps = self.down_subdeps
267
+ return nil if subdeps.empty?
248
268
 
249
- return msg
269
+ return subdeps.map( &:down_reason ).join( ' and ' )
250
270
  end
251
271
 
252
272
 
@@ -58,11 +58,27 @@ class Arborist::Event
58
58
  ### Return the event as a Hash.
59
59
  def to_h
60
60
  return {
61
- 'type' => self.type,
62
- 'data' => self.payload
61
+ type: self.type,
62
+ data: self.payload
63
63
  }
64
64
  end
65
65
 
66
+
67
+ ### Return a string representation of the object suitable for debugging.
68
+ def inspect
69
+ return "#<%p:%#016x %s>" % [
70
+ self.class,
71
+ self.object_id * 2,
72
+ self.inspect_details,
73
+ ]
74
+ end
75
+
76
+
77
+ ### Return the detail portion of the #inspect string appropriate for this event type.
78
+ def inspect_details
79
+ return self.payload.inspect
80
+ end
81
+
66
82
  end # class Arborist::Event
67
83
 
68
84
 
@@ -38,9 +38,13 @@ class Arborist::Event::Node < Arborist::Event
38
38
  end
39
39
 
40
40
 
41
- ### Inject the node identifier into the generated hash.
41
+ ### Inject useful node metadata into the generated hash.
42
42
  def to_h
43
- return super.merge( identifier: self.node.identifier )
43
+ return super.merge(
44
+ identifier: self.node.identifier,
45
+ parent: self.node.parent,
46
+ nodetype: self.node.type
47
+ )
44
48
  end
45
49
 
46
50
  end # module Arborist::Event::NodeMatching
@@ -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 monitor adds the first warning to a node.
9
+ class Arborist::Event::NodeWarn < Arborist::Event::Node
10
+
11
+ ### Create a new NodeWarn event for the specified +node+.
12
+ def initialize( node )
13
+ super( node, node.warnings.to_h )
14
+ end
15
+
16
+ end # class Arborist::Event::NodeWarn
@@ -22,7 +22,8 @@ class Arborist::Manager
22
22
  extend Configurability,
23
23
  Loggability,
24
24
  Arborist::MethodUtilities
25
- include CZTop::Reactor::SignalHandling
25
+ include CZTop::Reactor::SignalHandling,
26
+ Arborist::HashUtilities
26
27
 
27
28
 
28
29
  # Signals the manager responds to
@@ -31,6 +32,22 @@ class Arborist::Manager
31
32
  # :TODO: :QUIT, :WINCH, :USR2, :TTIN, :TTOU
32
33
  ] & Signal.list.keys.map( &:to_sym )
33
34
 
35
+ # Array of actions supported by the Tree API
36
+ VALID_TREEAPI_ACTIONS = %w[
37
+ ack
38
+ deps
39
+ fetch
40
+ graft
41
+ modify
42
+ prune
43
+ search
44
+ status
45
+ subscribe
46
+ unack
47
+ unsubscribe
48
+ update
49
+ ]
50
+
34
51
 
35
52
  # Use the Arborist logger
36
53
  log_to :arborist
@@ -169,7 +186,6 @@ class Arborist::Manager
169
186
  def run
170
187
  self.log.info "Getting ready to start the manager."
171
188
  self.setup_sockets
172
- self.publish_system_event( 'startup', start_time: Time.now.to_s, version: Arborist::VERSION )
173
189
  self.register_timers
174
190
  self.with_signal_handler( reactor, *QUEUE_SIGS ) do
175
191
  self.start_accepting_requests
@@ -244,6 +260,17 @@ class Arborist::Manager
244
260
  end
245
261
 
246
262
 
263
+ ### Return a human-readable representation of the Manager suitable for debugging.
264
+ def inspect
265
+ return "#<%p:%#x {runid: %s} %d nodes>" % [
266
+ self.class,
267
+ self.object_id * 2,
268
+ self.run_id,
269
+ self.nodes.length,
270
+ ]
271
+ end
272
+
273
+
247
274
  #
248
275
  # :section: Node state saving/reloading
249
276
  #
@@ -251,6 +278,7 @@ class Arborist::Manager
251
278
  ### Write out the state of all the manager's nodes to the state_file if one is
252
279
  ### configured.
253
280
  def save_node_states
281
+ start_time = Time.now
254
282
  path = self.class.state_file or return
255
283
  self.log.info "Saving current node state to %s" % [ path ]
256
284
  tmpfile = Tempfile.create(
@@ -262,6 +290,7 @@ class Arborist::Manager
262
290
  tmpfile.close
263
291
 
264
292
  File.rename( tmpfile.path, path.to_s )
293
+ self.log.debug "Saved state file in %0.1f seconds." % [ Time.now - start_time ]
265
294
 
266
295
  rescue SystemCallError => err
267
296
  self.log.error "%p while saving node state: %s" % [ err.class, err.message ]
@@ -323,8 +352,15 @@ class Arborist::Manager
323
352
  ### Register a periodic timer that will save a snapshot of the node tree's state to the state
324
353
  ### file on a configured interval if one is configured.
325
354
  def register_checkpoint_timer
326
- return nil unless self.class.state_file
327
- interval = self.class.checkpoint_frequency or return nil
355
+ unless self.class.state_file
356
+ self.log.info "No state file configured; skipping checkpoint timer setup."
357
+ return nil
358
+ end
359
+ interval = self.class.checkpoint_frequency
360
+ unless interval && interval.nonzero?
361
+ self.log.info "Checkpoint frequency is %p; skipping checkpoint timer setup." % [ interval ]
362
+ return nil
363
+ end
328
364
 
329
365
  self.log.info "Setting up node state checkpoint every %0.3fs" % [ interval ]
330
366
  @checkpoint_timer = self.reactor.add_periodic_timer( interval ) do
@@ -446,10 +482,8 @@ class Arborist::Manager
446
482
  def add_node( node )
447
483
  identifier = node.identifier
448
484
 
449
- unless self.nodes[ identifier ].equal?( node )
450
- self.remove_node( self.nodes[identifier] )
451
- self.nodes[ identifier ] = node
452
- end
485
+ raise Arborist::NodeError, "Node %p already present." % [ identifier ] if self.nodes[ identifier ]
486
+ self.nodes[ identifier ] = node
453
487
 
454
488
  if self.tree_built?
455
489
  self.link_node( node )
@@ -491,26 +525,26 @@ class Arborist::Manager
491
525
 
492
526
  ### Update the node with the specified +identifier+ with the given +new_properties+
493
527
  ### and propagate any events generated by the update to the node and its ancestors.
494
- def update_node( identifier, new_properties )
528
+ def update_node( identifier, new_properties, monitor_key='_' )
495
529
  unless (( node = self.nodes[identifier] ))
496
530
  self.log.warn "Update for non-existent node %p ignored." % [ identifier ]
497
531
  return []
498
532
  end
499
533
 
500
- events = node.update( new_properties )
534
+ events = node.update( new_properties, monitor_key )
501
535
  self.propagate_events( node, events )
502
536
  end
503
537
 
504
538
 
505
- ### Traverse the node tree and fetch the specified +return_values+ from any nodes which
539
+ ### Traverse the node tree and return the specified +return_values+ from any nodes which
506
540
  ### match the given +filter+, skipping downed nodes and all their children
507
- ### unless +include_down+ is set. If +return_values+ is set to +nil+, then all
541
+ ### if +exclude_down+ is set. If +return_values+ is set to +nil+, then all
508
542
  ### values from the node will be returned.
509
- def fetch_matching_node_states( filter, return_values, include_down=false, negative_filter={} )
510
- nodes_iter = if include_down
511
- self.all_nodes
512
- else
543
+ def find_matching_node_states( filter, return_values, exclude_down=false, negative_filter={} )
544
+ nodes_iter = if exclude_down
513
545
  self.reachable_nodes
546
+ else
547
+ self.all_nodes
514
548
  end
515
549
 
516
550
  states = nodes_iter.
@@ -551,7 +585,7 @@ class Arborist::Manager
551
585
  ### Set up the ZeroMQ REP socket for the Tree API.
552
586
  def setup_tree_socket
553
587
  @tree_socket = CZTop::Socket::REP.new
554
- self.log.debug " binding the tree API socket (%#0x) to %p" %
588
+ self.log.info " binding the tree API socket (%#0x) to %p" %
555
589
  [ @tree_socket.object_id * 2, Arborist.tree_api_url ]
556
590
  @tree_socket.options.linger = 0
557
591
  @tree_socket.bind( Arborist.tree_api_url )
@@ -569,7 +603,7 @@ class Arborist::Manager
569
603
  def on_tree_socket_event( event )
570
604
  if event.readable?
571
605
  request = event.socket.receive
572
- msg = self.handle_tree_request( request )
606
+ msg = self.dispatch_request( request )
573
607
  event.socket << msg
574
608
  else
575
609
  raise "Unsupported event %p on tree API socket!" % [ event ]
@@ -578,14 +612,11 @@ class Arborist::Manager
578
612
 
579
613
 
580
614
  ### Handle the specified +raw_request+ and return a response.
581
- def handle_tree_request( raw_request )
615
+ def dispatch_request( raw_request )
582
616
  raise "Manager is shutting down" unless self.running?
583
617
 
584
618
  header, body = Arborist::TreeAPI.decode( raw_request )
585
- raise Arborist::MessageError, "missing required header 'action'" unless
586
- header.key?( 'action' )
587
- handler = self.lookup_tree_request_action( header ) or
588
- raise Arborist::MessageError, "No such action '%s'" % [ header['action'] ]
619
+ handler = self.lookup_tree_request_action( header )
589
620
 
590
621
  return handler.call( header, body )
591
622
 
@@ -611,16 +642,19 @@ class Arborist::Manager
611
642
  raise Arborist::MessageError, "unsupported version %d" % [ header['version'] ] unless
612
643
  header['version'] == 1
613
644
 
614
- handler_name = "handle_%s_request" % [ header['action'] ]
615
- return nil unless self.respond_to?( handler_name )
645
+ action = header['action'] or
646
+ raise Arborist::MessageError, "missing required header 'action'"
647
+ raise Arborist::MessageError, "No such action '%s'" % [ action ] unless
648
+ VALID_TREEAPI_ACTIONS.include?( action )
616
649
 
650
+ handler_name = "handle_%s_request" % [ action ]
617
651
  return self.method( handler_name )
618
652
  end
619
653
 
620
654
 
621
655
  ### Return a response to the `status` action.
622
656
  def handle_status_request( header, body )
623
- self.log.debug "STATUS: %p" % [ header ]
657
+ self.log.info "STATUS: %p" % [ header ]
624
658
  return Arborist::TreeAPI.successful_response(
625
659
  server_version: Arborist::VERSION,
626
660
  state: self.running? ? 'running' : 'not running',
@@ -632,7 +666,7 @@ class Arborist::Manager
632
666
 
633
667
  ### Return a response to the `subscribe` action.
634
668
  def handle_subscribe_request( header, body )
635
- self.log.debug "SUBSCRIBE: %p" % [ header ]
669
+ self.log.info "SUBSCRIBE: %p" % [ header ]
636
670
  event_type = header[ 'event_type' ]
637
671
  node_identifier = header[ 'identifier' ]
638
672
 
@@ -642,7 +676,7 @@ class Arborist::Manager
642
676
 
643
677
  subscription = self.create_subscription( node_identifier, event_type, positive, negative )
644
678
  self.log.info "Subscription to %s events at or under %s: %p" %
645
- [ event_type, node_identifier || 'the root node', subscription ]
679
+ [ event_type || 'all', node_identifier || 'the root node', subscription ]
646
680
 
647
681
  return Arborist::TreeAPI.successful_response( id: subscription.id )
648
682
  end
@@ -650,7 +684,7 @@ class Arborist::Manager
650
684
 
651
685
  ### Return a response to the `unsubscribe` action.
652
686
  def handle_unsubscribe_request( header, body )
653
- self.log.debug "UNSUBSCRIBE: %p" % [ header ]
687
+ self.log.info "UNSUBSCRIBE: %p" % [ header ]
654
688
  subscription_id = header[ 'subscription_id' ] or
655
689
  return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for UNSUBSCRIBE.' )
656
690
  subscription = self.remove_subscription( subscription_id ) or
@@ -664,20 +698,25 @@ class Arborist::Manager
664
698
  end
665
699
 
666
700
 
667
- ### Return a repsonse to the `list` action.
668
- def handle_list_request( header, body )
669
- self.log.debug "LIST: %p" % [ header ]
701
+ ### Return a repsonse to the `fetch` action.
702
+ def handle_fetch_request( header, body )
703
+ self.log.info "FETCH: %p" % [ header ]
670
704
  from = header['from'] || '_'
671
705
  depth = header['depth']
706
+ tree = header['tree']
672
707
 
673
- start_node = self.nodes[ from ]
708
+ start_node = self.nodes[ from ] or
709
+ return Arborist::TreeAPI.error_response( 'client', "No such node %s." % [from] )
674
710
  self.log.debug " Listing nodes under %p" % [ start_node ]
675
- iter = if depth
711
+
712
+ if tree
713
+ iter = [ start_node.to_h(depth: (depth || -1)) ]
714
+ elsif depth
676
715
  self.log.debug " depth limited to %d" % [ depth ]
677
- self.depth_limited_enumerator_for( start_node, depth )
716
+ iter = self.depth_limited_enumerator_for( start_node, depth )
678
717
  else
679
718
  self.log.debug " no depth limit"
680
- self.enumerator_for( start_node )
719
+ iter = self.enumerator_for( start_node )
681
720
  end
682
721
  data = iter.map( &:to_h )
683
722
  self.log.debug " got data for %d nodes" % [ data.length ]
@@ -686,11 +725,45 @@ class Arborist::Manager
686
725
  end
687
726
 
688
727
 
689
- ### Return a response to the 'fetch' action.
690
- def handle_fetch_request( header, body )
691
- self.log.debug "FETCH: %p" % [ header ]
728
+ ### Return a response to the `deps` action.
729
+ def handle_deps_request( header, body )
730
+ self.log.info "DEPS: %p" % [ header ]
731
+ from = header['from'] || '_'
732
+
733
+ deps = self.merge_dependencies_from( from )
734
+ deps.delete( from )
692
735
 
693
- include_down = header['include_down']
736
+ return Arborist::TreeAPI.successful_response({ deps: deps.to_a })
737
+
738
+ rescue Arborist::ClientError => err
739
+ return Arborist::TreeAPI.error_response( 'client', err.message )
740
+ end
741
+
742
+
743
+ ### Recurse into the children and secondary dependencies of the +from+ node and
744
+ ### merge the identifiers of the traversed nodes into the +deps_set+.
745
+ def merge_dependencies_from( from, deps_set=Set.new )
746
+ return deps_set unless deps_set.add?( from )
747
+
748
+ start_node = self.nodes[ from ] or
749
+ raise Arborist::ClientError "No such node %s." % [ from ]
750
+
751
+ self.enumerator_for( start_node ).each do |subnode|
752
+ deps_set.add( subnode.identifier )
753
+ subnode.node_subscribers.each do |subdep|
754
+ self.merge_dependencies_from( subdep, deps_set )
755
+ end
756
+ end
757
+
758
+ return deps_set
759
+ end
760
+
761
+
762
+ ### Return a response to the 'search' action.
763
+ def handle_search_request( header, body )
764
+ self.log.info "SEARCH: %p" % [ header ]
765
+
766
+ exclude_down = header['exclude_down']
694
767
  values = if header.key?( 'return' )
695
768
  header['return'] || []
696
769
  else
@@ -700,7 +773,7 @@ class Arborist::Manager
700
773
  body = [ body ] unless body.is_a?( Array )
701
774
  positive = body.shift
702
775
  negative = body.shift || {}
703
- states = self.fetch_matching_node_states( positive, values, include_down, negative )
776
+ states = self.find_matching_node_states( positive, values, exclude_down, negative )
704
777
 
705
778
  return Arborist::TreeAPI.successful_response( states )
706
779
  end
@@ -708,14 +781,15 @@ class Arborist::Manager
708
781
 
709
782
  ### Update nodes using the data from the update request's +body+.
710
783
  def handle_update_request( header, body )
711
- self.log.debug "UPDATE: %p" % [ header ]
784
+ self.log.info "UPDATE: %p" % [ header ]
712
785
 
713
786
  unless body.respond_to?( :each )
714
787
  return Arborist::TreeAPI.error_response( 'client', 'Malformed update: body does not respond to #each' )
715
788
  end
716
789
 
790
+ monitor_key = header['monitor_key']
717
791
  body.each do |identifier, properties|
718
- self.update_node( identifier, properties )
792
+ self.update_node( identifier, properties, monitor_key )
719
793
  end
720
794
 
721
795
  return Arborist::TreeAPI.successful_response( nil )
@@ -724,7 +798,7 @@ class Arborist::Manager
724
798
 
725
799
  ### Remove a node and its children.
726
800
  def handle_prune_request( header, body )
727
- self.log.debug "PRUNE: %p" % [ header ]
801
+ self.log.info "PRUNE: %p" % [ header ]
728
802
 
729
803
  identifier = header[ 'identifier' ] or
730
804
  return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for PRUNE.' )
@@ -736,10 +810,15 @@ class Arborist::Manager
736
810
 
737
811
  ### Add a node
738
812
  def handle_graft_request( header, body )
739
- self.log.debug "GRAFT: %p" % [ header ]
813
+ self.log.info "GRAFT: %p" % [ header ]
740
814
 
741
815
  identifier = header[ 'identifier' ] or
742
816
  return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for GRAFT.' )
817
+
818
+ if self.nodes[ identifier ]
819
+ return Arborist::TreeAPI.error_response( 'client', "Node %p already exists." % [identifier] )
820
+ end
821
+
743
822
  type = header[ 'type' ] or
744
823
  return Arborist::TreeAPI.error_response( 'client', 'No type specified for GRAFT.' )
745
824
  parent = header[ 'parent' ] || '_'
@@ -765,7 +844,7 @@ class Arborist::Manager
765
844
 
766
845
  ### Modify a node's operational attributes
767
846
  def handle_modify_request( header, body )
768
- self.log.debug "MODIFY: %p" % [ header ]
847
+ self.log.info "MODIFY: %p" % [ header ]
769
848
 
770
849
  identifier = header[ 'identifier' ] or
771
850
  return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for MODIFY.' )
@@ -775,12 +854,56 @@ class Arborist::Manager
775
854
 
776
855
  self.log.debug "Modifying operational attributes of the %s node: %p" % [ identifier, body ]
777
856
 
857
+ if new_parent_identifier = body.delete( 'parent' )
858
+ old_parent = self.nodes[ node.parent ]
859
+ new_parent = self.nodes[ new_parent_identifier ] or
860
+ return Arborist::TreeAPI.error_response( 'client', "No such parent node: %p" % [new_parent_identifier] )
861
+ node.reparent( old_parent, new_parent )
862
+ end
863
+
778
864
  node.modify( body )
779
865
 
780
866
  return Arborist::TreeAPI.successful_response( nil )
781
867
  end
782
868
 
783
869
 
870
+ ### Acknowledge a node
871
+ def handle_ack_request( header, body )
872
+ self.log.info "ACK: %p" % [ header ]
873
+
874
+ identifier = header[ 'identifier' ] or
875
+ return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for ACK.' )
876
+ node = self.nodes[ identifier ] or
877
+ return Arborist::TreeAPI.error_response( 'client', "No such node %p" % [identifier] )
878
+
879
+ self.log.debug "Acking the %s node: %p" % [ identifier, body ]
880
+
881
+ body = symbolify_keys( body )
882
+ events = node.acknowledge( **body )
883
+ self.propagate_events( node, events )
884
+
885
+ return Arborist::TreeAPI.successful_response( nil )
886
+ end
887
+
888
+
889
+ ### Un-acknowledge a node
890
+ def handle_unack_request( header, body )
891
+ self.log.info "UNACK: %p" % [ header ]
892
+
893
+ identifier = header[ 'identifier' ] or
894
+ return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for UNACK.' )
895
+ node = self.nodes[ identifier ] or
896
+ return Arborist::TreeAPI.error_response( 'client', "No such node %p" % [identifier] )
897
+
898
+ self.log.debug "Unacking the %s node: %p" % [ identifier, body ]
899
+
900
+ events = node.unacknowledge
901
+ self.propagate_events( node, events )
902
+
903
+ return Arborist::TreeAPI.successful_response( nil )
904
+ end
905
+
906
+
784
907
  ### Return the current root node.
785
908
  def root_node
786
909
  return self.nodes[ '_' ]
@@ -859,7 +982,7 @@ class Arborist::Manager
859
982
  ### Set up the ZMQ PUB socket for published events.
860
983
  def setup_event_socket
861
984
  @event_socket = CZTop::Socket::PUB.new
862
- self.log.debug " binding the event socket (%#0x) to %p" %
985
+ self.log.info " binding the event socket (%#0x) to %p" %
863
986
  [ @event_socket.object_id * 2, Arborist.event_api_url ]
864
987
  @event_socket.options.linger = ( self.linger * 1000 ).ceil
865
988
  @event_socket.bind( Arborist.event_api_url )
@@ -893,6 +1016,7 @@ class Arborist::Manager
893
1016
 
894
1017
  ### Register the publisher with the reactor if it's not already.
895
1018
  def register_event_socket
1019
+ self.log.debug "Registering event socket for write events."
896
1020
  self.reactor.enable_events( self.event_socket, :write ) unless
897
1021
  self.reactor.event_enabled?( self.event_socket, :write )
898
1022
  end
@@ -900,6 +1024,7 @@ class Arborist::Manager
900
1024
 
901
1025
  ### Unregister the event publisher socket from the reactor if it's registered.
902
1026
  def unregister_event_socket
1027
+ self.log.debug "Unregistering event socket for write events."
903
1028
  self.reactor.disable_events( self.event_socket, :write ) if
904
1029
  self.reactor.event_enabled?( self.event_socket, :write )
905
1030
  end
@@ -909,6 +1034,7 @@ class Arborist::Manager
909
1034
  def on_event_socket_event( event )
910
1035
  if event.writable?
911
1036
  if (( msg = self.event_queue.shift ))
1037
+ # self.log.debug "Publishing event %p" % [ msg ]
912
1038
  event.socket << msg
913
1039
  end
914
1040
  else
@@ -921,7 +1047,12 @@ class Arborist::Manager
921
1047
 
922
1048
  ### Publish a system event that observers can watch for to detect restarts.
923
1049
  def publish_heartbeat_event
924
- self.publish_system_event( 'heartbeat', run_id: self.run_id )
1050
+ return unless self.start_time
1051
+ self.publish_system_event( 'heartbeat',
1052
+ run_id: self.run_id,
1053
+ start_time: self.start_time.iso8601,
1054
+ version: Arborist::VERSION
1055
+ )
925
1056
  end
926
1057
 
927
1058