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
@@ -1,6 +1,8 @@
1
1
  # -*- ruby -*-
2
2
  #encoding: utf-8
3
3
 
4
+ require 'set'
5
+
4
6
  require 'cztop'
5
7
  require 'cztop/reactor'
6
8
  require 'cztop/reactor/signal_handling'
@@ -21,17 +23,21 @@ class Arborist::MonitorRunner
21
23
  # :TODO: :QUIT, :WINCH, :USR2, :TTIN, :TTOU
22
24
  ] & Signal.list.keys.map( &:to_sym )
23
25
 
26
+ # Number of seconds between thread cleanup
27
+ THREAD_CLEANUP_INTERVAL = 5 # seconds
28
+
24
29
 
25
30
  log_to :arborist
26
31
 
27
32
 
28
33
  ### Create a new Arborist::MonitorRunner
29
34
  def initialize
30
- @monitors = []
31
- @handler = nil
32
- @reactor = CZTop::Reactor.new
33
- @client = Arborist::Client.new
34
- @request_queue = {}
35
+ @monitors = []
36
+ @handler = nil
37
+ @reactor = CZTop::Reactor.new
38
+ @client = Arborist::Client.new
39
+ @runner_threads = {}
40
+ @request_queue = {}
35
41
  end
36
42
 
37
43
 
@@ -60,6 +66,10 @@ class Arborist::MonitorRunner
60
66
  # The Arborist::Client that will provide the message packing and unpacking
61
67
  attr_reader :client
62
68
 
69
+ ##
70
+ # A hash of monitor object -> thread used to contain and track running monitor threads.
71
+ attr_reader :runner_threads
72
+
63
73
 
64
74
  ### Load monitors from the specified +enumerator+.
65
75
  def load_monitors( enumerator )
@@ -69,6 +79,12 @@ class Arborist::MonitorRunner
69
79
 
70
80
  ### Run the specified +monitors+
71
81
  def run
82
+ self.monitors.each do |mon|
83
+ self.add_timer_for( mon )
84
+ end
85
+
86
+ self.add_thread_cleanup_timer
87
+
72
88
  self.with_signal_handler( self.reactor, *QUEUE_SIGS ) do
73
89
  self.reactor.register( self.client.tree_api, :write, &self.method(:handle_io_event) )
74
90
  self.reactor.start_polling
@@ -108,45 +124,77 @@ class Arborist::MonitorRunner
108
124
  end
109
125
 
110
126
 
111
- ### Run the specified +monitor+ and update nodes with the results.
127
+ ### Update nodes with the results of a monitor's run.
112
128
  def run_monitor( monitor )
113
129
  positive = monitor.positive_criteria
114
130
  negative = monitor.negative_criteria
115
- include_down = monitor.include_down?
131
+ exclude_down = monitor.exclude_down?
116
132
  props = monitor.node_properties
117
133
 
118
- self.fetch( positive, include_down, props, negative ) do |nodes|
119
- results = monitor.run( nodes )
120
- monitor_key = monitor.key
121
-
122
- results.each do |ident, properties|
123
- properties['_monitor_key'] = monitor_key
134
+ self.search( positive, exclude_down, props, negative ) do |nodes|
135
+ self.log.info "Running %p monitor for %d node(s)" % [
136
+ monitor.description,
137
+ nodes.length
138
+ ]
139
+
140
+ unless nodes.empty?
141
+ self.runner_threads[ monitor ] = Thread.new do
142
+ Thread.current[:monitor_desc] = monitor.description
143
+ results = self.run_monitor_safely( monitor, nodes )
144
+
145
+ self.log.debug " updating with results: %p" % [ results ]
146
+ self.update( results, monitor.key ) do
147
+ self.log.debug "Updated %d via the '%s' monitor" %
148
+ [ results.length, monitor.description ]
149
+ end
150
+ end
151
+ self.log.debug "THREAD: Started %p for %p" % [ self.runner_threads[monitor], monitor ]
152
+ self.log.debug "THREAD: Runner threads have: %p" % [ self.runner_threads.to_a ]
124
153
  end
154
+ end
155
+ end
156
+
125
157
 
126
- self.update( results ) do
127
- self.log.debug "Updated %d via the '%s' monitor" %
128
- [ results.length, monitor.description ]
158
+ ### Exec +monitor+ against the provided +nodes+ hash, treating
159
+ ### runtime exceptions as an error condition. Returns an update
160
+ ### hash, keyed by node identifier.
161
+ ###
162
+ def run_monitor_safely( monitor, nodes )
163
+ results = begin
164
+ monitor.run( nodes )
165
+ rescue => err
166
+ errmsg = "Exception while running %p monitor: %s: %s" % [
167
+ monitor.description,
168
+ err.class.name,
169
+ err.message
170
+ ]
171
+ self.log.error "%s\n%s" % [ errmsg, err.backtrace.join("\n ") ]
172
+ nodes.keys.each_with_object({}) do |id, results|
173
+ results[id] = { error: errmsg }
129
174
  end
130
175
  end
176
+
177
+ return results
131
178
  end
132
179
 
133
180
 
134
- ### Create a fetch request using the runner's client, then queue the request up
181
+ ### Create a search request using the runner's client, then queue the request up
135
182
  ### with the specified +block+ as the callback.
136
- def fetch( criteria, include_down, properties, negative={}, &block )
137
- fetch = self.client.make_fetch_request( criteria,
138
- include_down: include_down,
183
+ def search( criteria, exclude_down, properties, negative={}, &block )
184
+ search = self.client.make_search_request( criteria,
185
+ exclude_down: exclude_down,
139
186
  properties: properties,
140
187
  exclude: negative
141
188
  )
142
- self.queue_request( fetch, &block )
189
+ self.queue_request( search, &block )
143
190
  end
144
191
 
145
192
 
146
193
  ### Create an update request using the runner's client, then queue the request up
147
194
  ### with the specified +block+ as the callback.
148
- def update( nodemap, &block )
149
- update = self.client.make_update_request( nodemap )
195
+ def update( nodemap, monitor_key, &block )
196
+ return if nodemap.empty?
197
+ update = self.client.make_update_request( nodemap, monitor_key: monitor_key )
150
198
  self.queue_request( update, &block )
151
199
  end
152
200
 
@@ -198,7 +246,9 @@ class Arborist::MonitorRunner
198
246
  self.log.info "Creating timer for %p" % [ monitor ]
199
247
 
200
248
  return self.reactor.add_periodic_timer( interval ) do
201
- self.run_monitor( monitor )
249
+ unless self.runner_threads.key?( monitor )
250
+ self.run_monitor( monitor )
251
+ end
202
252
  end
203
253
  end
204
254
 
@@ -215,6 +265,33 @@ class Arborist::MonitorRunner
215
265
  end
216
266
 
217
267
 
268
+ ### Set up a timer to clean up monitor threads.
269
+ def add_thread_cleanup_timer
270
+ self.log.debug "Starting thread cleanup timer for %p." % [ self.runner_threads ]
271
+ self.reactor.add_periodic_timer( THREAD_CLEANUP_INTERVAL ) do
272
+ self.cleanup_monitor_threads
273
+ end
274
+ end
275
+
276
+
277
+ ### :TODO: Handle the thread-interrupt stuff?
278
+
279
+ ### Clean up any monitor runner threads that are dead.
280
+ def cleanup_monitor_threads
281
+ self.runner_threads.values.reject( &:alive? ).each do |thr|
282
+ monitor = self.runner_threads.key( thr )
283
+ self.runner_threads.delete( monitor )
284
+
285
+ begin
286
+ thr.join
287
+ rescue => err
288
+ self.log.error "%p while running %s: %s" %
289
+ [ err.class, thr[:monitor_desc], err.message ]
290
+ end
291
+ end
292
+ end
293
+
294
+
218
295
  #
219
296
  # :section: Signal Handling
220
297
  # These methods set up some behavior for starting, restarting, and stopping
@@ -42,6 +42,7 @@ class Arborist::Node
42
42
  description
43
43
  dependencies
44
44
  status_changed
45
+ status_last_changed
45
46
  last_contacted
46
47
  ack
47
48
  errors
@@ -116,16 +117,26 @@ class Arborist::Node
116
117
  state :unknown,
117
118
  :up,
118
119
  :down,
120
+ :warn,
119
121
  :acked,
120
122
  :disabled,
121
123
  :quieted
122
124
 
123
125
  event :update do
124
- transition [:up, :unknown] => :disabled, if: :ack_set?
125
- transition [:down, :unknown, :acked] => :up, unless: :has_errors?
126
- transition [:up, :unknown, :acked] => :down, if: :has_errors?
127
- transition :down => :acked, if: :ack_set?
128
- transition :disabled => :unknown, unless: :ack_set?
126
+ transition [:down, :warn, :unknown, :acked] => :up, unless: :has_errors_or_warnings?
127
+ transition [:up, :warn, :unknown] => :down, if: :has_errors?
128
+ transition [:up, :down, :unknown] => :warn, if: :has_only_warnings?
129
+ end
130
+
131
+ event :acknowledge do
132
+ transition any - [:down, :acked] => :disabled
133
+ transition [:down, :acked] => :acked
134
+ end
135
+
136
+ event :unacknowledge do
137
+ transition [:acked, :disabled] => :warn, if: :has_warnings?
138
+ transition [:acked, :disabled] => :down, if: :has_errors?
139
+ transition [:acked, :disabled] => :unknown
129
140
  end
130
141
 
131
142
  event :handle_event do
@@ -133,11 +144,19 @@ class Arborist::Node
133
144
  transition :quieted => :unknown, unless: :has_quieted_reason?
134
145
  end
135
146
 
147
+ event :reparent do
148
+ transition any - [:disabled, :quieted, :acked] => :unknown
149
+ transition :quieted => :unknown, unless: :has_quieted_reason?
150
+ end
151
+
152
+ before_transition [:acked, :disabled] => any, do: :save_previous_ack
153
+
136
154
  after_transition any => :acked, do: :on_ack
137
155
  after_transition :acked => :up, do: :on_ack_cleared
138
156
  after_transition :down => :up, do: :on_node_up
139
- after_transition [:unknown, :up] => :down, do: :on_node_down
140
- after_transition [:unknown, :up] => :disabled, do: :on_node_disabled
157
+ after_transition :up => :warn, do: :on_node_warn
158
+ after_transition [:unknown, :warn, :up] => :down, do: :on_node_down
159
+ after_transition [:unknown, :warn, :up] => :disabled, do: :on_node_disabled
141
160
  after_transition any => :quieted, do: :on_node_quieted
142
161
  after_transition :disabled => :unknown, do: :on_node_enabled
143
162
  after_transition :quieted => :unknown, do: :on_node_unquieted
@@ -180,7 +199,7 @@ class Arborist::Node
180
199
  ### them.
181
200
  def self::add_loaded_instance( new_instance )
182
201
  instances = Thread.current[ LOADED_INSTANCE_KEY ] or return
183
- self.log.debug "Adding new instance %p to node tree" % [ new_instance ]
202
+ # self.log.debug "Adding new instance %p to node tree" % [ new_instance ]
184
203
  instances << new_instance
185
204
  end
186
205
 
@@ -195,14 +214,16 @@ class Arborist::Node
195
214
 
196
215
 
197
216
  ### Get/set the node type instances of the class live under. If no parent_type is set, it
198
- ### is a top-level node type.
199
- def self::parent_types( *types )
217
+ ### is a top-level node type. If a +block+ is given, it can be used to pre-process the
218
+ ### arguments into the (identifier, attributes, block) arguments used to create
219
+ ### the node instances.
220
+ def self::parent_types( *types, &block )
200
221
  @parent_types ||= []
201
222
 
202
223
  types.each do |new_type|
203
224
  subclass = Arborist::Node.get_subclass( new_type )
204
225
  @parent_types << subclass
205
- subclass.add_subnode_factory_method( self )
226
+ subclass.add_subnode_factory_method( self, &block )
206
227
  end
207
228
 
208
229
  return @parent_types
@@ -218,11 +239,23 @@ class Arborist::Node
218
239
 
219
240
  ### Add a factory method that can be used to create subnodes of the specified +subnode_type+
220
241
  ### on instances of the receiving class.
221
- def self::add_subnode_factory_method( subnode_type )
242
+ def self::add_subnode_factory_method( subnode_type, &dsl_block )
222
243
  if subnode_type.name
223
244
  name = subnode_type.plugin_name
224
- body = lambda do |identifier, attributes={}, &block|
225
- return Arborist::Node.create( name, identifier, self, attributes, &block )
245
+ # self.log.debug "Adding factory constructor for %s nodes to %p" % [ name, self ]
246
+ body = lambda do |*args, &constructor_block|
247
+ if dsl_block
248
+ # self.log.debug "Using DSL block to split args: %p" % [ dsl_block ]
249
+ identifier, attributes = dsl_block.call( *args )
250
+ else
251
+ # self.log.debug "Splitting args the default way: %p" % [ args ]
252
+ identifier, attributes = *args
253
+ end
254
+ attributes ||= {}
255
+ # self.log.debug "Identifier: %p, attributes: %p, self: %p" %
256
+ # [ identifier, attributes, self ]
257
+
258
+ return Arborist::Node.create( name, identifier, self, attributes, &constructor_block )
226
259
  end
227
260
 
228
261
  define_method( name, &body )
@@ -279,10 +312,13 @@ class Arborist::Node
279
312
  # Primary state
280
313
  @status = 'unknown'
281
314
  @status_changed = Time.at( 0 )
315
+ @status_last_changed = Time.at( 0 )
282
316
 
283
317
  # Attributes that govern state
284
318
  @errors = {}
319
+ @warnings = {}
285
320
  @ack = nil
321
+ @previous_ack = nil
286
322
  @last_contacted = Time.at( 0 )
287
323
  @quieted_reasons = {}
288
324
 
@@ -290,10 +326,9 @@ class Arborist::Node
290
326
  @update_delta = Hash.new do |h,k|
291
327
  h[ k ] = Hash.new( &h.default_proc )
292
328
  end
293
- @pending_update_events = []
329
+ @pending_change_events = []
294
330
  @subscriptions = {}
295
331
 
296
- self.log.debug "Setting node attributes to: %p" % [ attributes ]
297
332
  self.modify( attributes )
298
333
  self.instance_eval( &block ) if block
299
334
  end
@@ -327,22 +362,36 @@ class Arborist::Node
327
362
  # The Time the node's status last changed.
328
363
  attr_accessor :status_changed
329
364
 
365
+ ##
366
+ # The previous Time the node's status changed, for duration
367
+ # calculations between states.
368
+ attr_accessor :status_last_changed
369
+
330
370
  ##
331
371
  # The Hash of last errors encountered by a monitor attempting to update this
332
372
  # node, keyed by the monitor's `key`.
333
373
  attr_accessor :errors
334
374
 
375
+ ##
376
+ # The Hash of last warnings encountered by a monitor attempting to update this
377
+ # node, keyed by the monitor's `key`.
378
+ attr_accessor :warnings
379
+
335
380
  ##
336
381
  # The acknowledgement currently in effect. Should be an instance of Arborist::Node::ACK
337
382
  attr_accessor :ack
338
383
 
384
+ ##
385
+ # The acknowledgement previously in effect (if any).
386
+ attr_accessor :previous_ack
387
+
339
388
  ##
340
389
  # The Hash of changes tracked during an #update.
341
390
  attr_reader :update_delta
342
391
 
343
392
  ##
344
393
  # The Array of events generated by the current update event
345
- attr_reader :pending_update_events
394
+ attr_reader :pending_change_events
346
395
 
347
396
  ##
348
397
  # The Hash of Subscription objects observing this node and its children, keyed by
@@ -353,7 +402,6 @@ class Arborist::Node
353
402
  # The node's secondary dependencies, expressed as an Arborist::Node::Sexp
354
403
  attr_accessor :dependencies
355
404
 
356
-
357
405
  ##
358
406
  # The reasons this node was quieted. This is a Hash of text descriptions keyed by the
359
407
  # type of dependency it came from (either :primary or :secondary).
@@ -368,13 +416,14 @@ class Arborist::Node
368
416
 
369
417
  ### Set one or more node +attributes+. This should be overridden by subclasses which
370
418
  ### wish to allow their operational attributes to be set/updated via the Tree API
371
- ### (+modify+ and +graft+). Supported attributes are: +parent+, +description+, and
372
- ### +tags+.
419
+ ### (+modify+ and +graft+). Supported attributes are: +parent+, +description+,
420
+ ### +tags+, and +config+.
373
421
  def modify( attributes )
374
422
  attributes = stringify_keys( attributes )
375
423
 
376
424
  self.parent( attributes['parent'] )
377
425
  self.description( attributes['description'] )
426
+ self.config( attributes['config'] )
378
427
 
379
428
  if attributes['tags']
380
429
  @tags.clear
@@ -447,7 +496,7 @@ class Arborist::Node
447
496
  ### Get or set the node's configuration hash. This can be used to pass per-node
448
497
  ### information to systems using the tree (e.g., monitors, subscribers).
449
498
  def config( new_config=nil )
450
- @config = stringify_keys( new_config ) if new_config
499
+ @config.merge!( stringify_keys( new_config ) ) if new_config
451
500
  return @config
452
501
  end
453
502
 
@@ -483,45 +532,108 @@ class Arborist::Node
483
532
  end
484
533
 
485
534
 
535
+ ### Return the Set of identifier of nodes that are secondary dependencies of this node.
536
+ def node_subscribers
537
+ self.log.debug "Finding node subscribers among %d subscriptions" % [ self.subscriptions.length ]
538
+ return self.subscriptions.each_with_object( Set.new ) do |(identifier, sub), set|
539
+ if sub.respond_to?( :node_identifier )
540
+ set.add( sub.node_identifier )
541
+ else
542
+ self.log.debug "Skipping %p: not a node subscription" % [ sub ]
543
+ end
544
+ end
545
+ end
546
+
547
+
486
548
  ### Update specified +properties+ for the node.
487
- def update( new_properties )
549
+ def update( new_properties, monitor_key='_' )
550
+ self.last_contacted = Time.now
551
+ self.update_properties( new_properties, monitor_key )
552
+
553
+ # Super to the state machine event method
554
+ super
555
+
556
+ events = self.pending_change_events.clone
557
+ events << self.make_update_event
558
+ events << self.make_delta_event unless self.update_delta.empty?
559
+
560
+ results = self.broadcast_events( *events )
561
+ self.log.debug ">>> Results from broadcast: %p" % [ results ]
562
+ events.concat( results )
563
+
564
+ return events
565
+ ensure
566
+ self.clear_transition_temp_vars
567
+ end
568
+
569
+
570
+ ### Update the node's properties with those in +new_properties+ (a String-keyed Hash)
571
+ def update_properties( new_properties, monitor_key )
572
+ monitor_key ||= '_'
488
573
  new_properties = stringify_keys( new_properties )
489
- monitor_key = new_properties[ '_monitor_key' ] || '_'
490
574
 
491
575
  self.log.debug "Updated via a %s monitor: %p" % [ monitor_key, new_properties ]
492
- self.last_contacted = Time.now
576
+ self.update_errors( monitor_key, new_properties.delete('error') )
577
+ self.update_warnings( monitor_key, new_properties.delete('warning') )
578
+
579
+ self.properties.merge!( new_properties, &self.method(:merge_and_record_delta) )
580
+ compact_hash( self.properties )
581
+ end
582
+
493
583
 
494
- if new_properties.key?( 'ack' )
495
- self.ack = new_properties.delete( 'ack' )
496
- elsif new_properties['error']
497
- self.errors[ monitor_key ] = new_properties.delete( 'error' )
584
+ ### Update the errors hash for the specified +monitor_key+ to +value+.
585
+ def update_errors( monitor_key, value=nil )
586
+ if value
587
+ self.errors[ monitor_key ] = value
498
588
  else
499
589
  self.errors.delete( monitor_key )
500
590
  end
591
+ end
501
592
 
502
- self.properties.merge!( new_properties, &self.method(:merge_and_record_delta) )
503
- compact_hash( self.properties )
504
593
 
505
- # Super to the state machine event method
506
- super
507
-
508
- return self.collect_events
594
+ ### Update the warnings hash for the specified +monitor_key+ to +value+.
595
+ def update_warnings( monitor_key, value=nil )
596
+ if value
597
+ self.warnings[ monitor_key ] = value
598
+ else
599
+ self.warnings.delete( monitor_key )
600
+ end
509
601
  end
510
602
 
511
603
 
512
- ### Collect the events generated by updates and return them after broadcasting
513
- ### them to any child nodes.
514
- def collect_events
515
- events = self.pending_update_events.clone
516
- events << self.make_update_event
604
+ ### Acknowledge any current or future abnormal status for this node.
605
+ def acknowledge( **args )
606
+ super()
607
+
608
+ self.ack = args
609
+
610
+ events = self.pending_change_events.clone
517
611
  events << self.make_delta_event unless self.update_delta.empty?
612
+ results = self.broadcast_events( *events )
613
+ self.log.debug ">>> Results from broadcast: %p" % [ results ]
614
+ events.concat( results )
518
615
 
519
- self.broadcast_events( *events )
616
+ return events
617
+ ensure
618
+ self.clear_transition_temp_vars
619
+ end
620
+
621
+
622
+ ### Clear any current acknowledgement.
623
+ def unacknowledge
624
+ super()
625
+
626
+ self.ack = nil
627
+
628
+ events = self.pending_change_events.clone
629
+ events << self.make_delta_event unless self.update_delta.empty?
630
+ results = self.broadcast_events( *events )
631
+ self.log.debug ">>> Results from broadcast: %p" % [ results ]
632
+ events.concat( results )
520
633
 
521
634
  return events
522
635
  ensure
523
- self.update_delta.clear
524
- self.pending_update_events.clear
636
+ self.clear_transition_temp_vars
525
637
  end
526
638
 
527
639
 
@@ -554,6 +666,14 @@ class Arborist::Node
554
666
  end
555
667
 
556
668
 
669
+ ### Clear out the state used during a transition to track changes.
670
+ def clear_transition_temp_vars
671
+ self.previous_ack = nil
672
+ self.update_delta.clear
673
+ self.pending_change_events.clear
674
+ end
675
+
676
+
557
677
  ### Return the node's state in an Arborist::Event of type 'node.update'.
558
678
  def make_update_event
559
679
  return Arborist::Event.create( 'node_update', self )
@@ -593,17 +713,18 @@ class Arborist::Node
593
713
 
594
714
  ### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
595
715
  def match_criteria?( key, val )
716
+ array_val = Array( val )
596
717
  return case key
597
718
  when 'status'
598
- self.status == val
719
+ array_val.include?( self.status )
599
720
  when 'type'
600
- self.log.debug "Checking node type %p against %p" % [ self.type, val ]
601
- self.type == val
721
+ array_val.include?( self.type )
602
722
  when 'parent'
603
- self.parent == val
723
+ array_val.include?( self.parent )
604
724
  when 'tag' then @tags.include?( val.to_s )
605
- when 'tags' then Array(val).all? {|tag| @tags.include?(tag) }
606
- when 'identifier' then @identifier == val
725
+ when 'tags' then array_val.all? {|tag| @tags.include?(tag) }
726
+ when 'identifier'
727
+ array_val.include?( self.identifier )
607
728
  when 'config'
608
729
  val.all? {|ikey, ival| hash_matches(@config, ikey, ival) }
609
730
  else
@@ -650,9 +771,7 @@ class Arborist::Node
650
771
  raise Arborist::ConfigError, "Can't depend on descendant node %p." % [ identifier ]
651
772
  end
652
773
 
653
- sub = Arborist::Subscription.new do |_, event|
654
- self.handle_event( event )
655
- end
774
+ sub = Arborist::NodeSubscription.new( self )
656
775
  manager.subscribe( identifier, sub )
657
776
  end
658
777
  end
@@ -670,12 +789,14 @@ class Arborist::Node
670
789
  ### Send an event to this node's immediate children.
671
790
  def broadcast_events( *events )
672
791
  events.flatten!
673
- self.children.each do |identifier, child|
792
+ results = self.children.flat_map do |identifier, child|
674
793
  self.log.debug "Broadcasting %d events to %p" % [ events.length, identifier ]
675
- events.each do |event|
794
+ events.flat_map do |event|
676
795
  child.handle_event( event )
677
796
  end
678
797
  end
798
+
799
+ return results
679
800
  end
680
801
 
681
802
 
@@ -692,10 +813,33 @@ class Arborist::Node
692
813
  self.log.debug "No handler for a %s event!" % [ event.type ]
693
814
  end
694
815
 
816
+ self.log.debug ">>> Pending change events before: %p" % [ self.pending_change_events ]
817
+
695
818
  super # to state-machine
696
819
 
697
- self.publish_events( event, *self.pending_update_events )
698
- self.collect_events
820
+ results = self.pending_change_events.clone
821
+ self.log.debug ">>> Pending change events after: %p" % [ results ]
822
+ results << self.make_delta_event unless self.update_delta.empty?
823
+
824
+ child_results = self.broadcast_events( *results )
825
+ results.concat( child_results )
826
+
827
+ self.publish_events( *results )
828
+
829
+ return results
830
+ ensure
831
+ self.clear_transition_temp_vars
832
+ end
833
+
834
+
835
+ ### Move a node from +old_parent+ to +new_parent+.
836
+ def reparent( old_parent, new_parent )
837
+ old_parent.remove_child( self )
838
+ self.parent( new_parent.identifier )
839
+ new_parent.add_child( self )
840
+
841
+ self.quieted_reasons.delete( :primary )
842
+ super
699
843
  end
700
844
 
701
845
 
@@ -769,7 +913,7 @@ class Arborist::Node
769
913
 
770
914
  ### Handle a 'node.up' event received via broadcast.
771
915
  def handle_node_up_event( event )
772
- self.log.debug "Got a node.up event: %p" % [ event ]
916
+ self.log.debug "Got a node.%s event: %p" % [ event.type, event ]
773
917
 
774
918
  self.dependencies.mark_up( event.node.identifier )
775
919
  self.quieted_reasons.delete( :secondary ) if self.dependencies_up?
@@ -782,6 +926,7 @@ class Arborist::Node
782
926
  self.quieted_reasons.delete( :primary )
783
927
  end
784
928
  end
929
+ alias_method :handle_node_warn_event, :handle_node_up_event
785
930
 
786
931
 
787
932
 
@@ -810,6 +955,7 @@ class Arborist::Node
810
955
  ### Returns +true+ if the node's status indicates it shouldn't be
811
956
  ### included by default when traversing nodes.
812
957
  def unreachable?
958
+ self.log.debug "Testing for reachability; status is: %p" % [ self.status ]
813
959
  return UNREACHABLE_STATES.include?( self.status )
814
960
  end
815
961
 
@@ -860,7 +1006,7 @@ class Arborist::Node
860
1006
  ### Return a string describing the node's status.
861
1007
  def status_description
862
1008
  case self.status
863
- when 'up', 'down'
1009
+ when 'up', 'down', 'warn'
864
1010
  return "%s as of %s" % [ self.status.upcase, self.last_contacted ]
865
1011
  when 'acked'
866
1012
  return "ACKed %s" % [ self.acked_description ]
@@ -869,8 +1015,10 @@ class Arborist::Node
869
1015
  when 'quieted'
870
1016
  reasons = self.quieted_reasons.values.join( ',' )
871
1017
  return "quieted: %s" % [ reasons ]
1018
+ when 'unknown'
1019
+ return "in an 'unknown' state"
872
1020
  else
873
- return "in an unknown state"
1021
+ return "in an unhandled state"
874
1022
  end
875
1023
  end
876
1024
 
@@ -902,7 +1050,9 @@ class Arborist::Node
902
1050
  # :section: Serialization API
903
1051
  #
904
1052
 
905
- ### Restore any saved state from the +old_node+ loaded from the state file.
1053
+ ### Restore any saved state from the +old_node+ loaded from the state file. This is
1054
+ ### used to overlay selective bits of the saved node tree to the equivalent nodes loaded
1055
+ ### from node definitions.
906
1056
  def restore( old_node )
907
1057
  @status = old_node.status
908
1058
  @properties = old_node.properties.dup
@@ -910,7 +1060,9 @@ class Arborist::Node
910
1060
  @last_contacted = old_node.last_contacted
911
1061
  @status_changed = old_node.status_changed
912
1062
  @errors = old_node.errors
1063
+ @warnings = old_node.warnings
913
1064
  @quieted_reasons = old_node.quieted_reasons
1065
+ @status_last_changed = old_node.status_last_changed
914
1066
 
915
1067
  # Only merge in downed dependencies.
916
1068
  old_node.dependencies.each_downed do |identifier, time|
@@ -919,8 +1071,10 @@ class Arborist::Node
919
1071
  end
920
1072
 
921
1073
 
922
- ### Return a Hash of the node's state.
923
- def to_h( deep: false )
1074
+ ### Return a Hash of the node's state. If +depth+ is greater than 0, that many
1075
+ ### levels of child nodes are included in the node's `:children` value. Setting
1076
+ ### +depth+ to a negative number will return all of the node's children.
1077
+ def to_h( depth: 0 )
924
1078
  hash = {
925
1079
  identifier: self.identifier,
926
1080
  type: self.class.name.to_s.sub( /.+::/, '' ).downcase,
@@ -933,15 +1087,20 @@ class Arborist::Node
933
1087
  ack: self.ack ? self.ack.to_h : nil,
934
1088
  last_contacted: self.last_contacted ? self.last_contacted.iso8601 : nil,
935
1089
  status_changed: self.status_changed ? self.status_changed.iso8601 : nil,
1090
+ status_last_changed: self.status_last_changed ? self.status_last_changed.iso8601 : nil,
936
1091
  errors: self.errors,
1092
+ warnings: self.warnings,
937
1093
  dependencies: self.dependencies.to_h,
938
1094
  quieted_reasons: self.quieted_reasons,
939
1095
  }
940
1096
 
941
- if deep
1097
+ if depth.nonzero?
1098
+ # self.log.debug "including children for depth %p" % [ depth ]
942
1099
  hash[ :children ] = self.children.each_with_object( {} ) do |(ident, node), h|
943
- h[ ident ] = node.to_h( deep )
1100
+ h[ ident ] = node.to_h( depth: depth - 1 )
944
1101
  end
1102
+ else
1103
+ hash[ :children ] = {}
945
1104
  end
946
1105
 
947
1106
  return hash
@@ -969,9 +1128,11 @@ class Arborist::Node
969
1128
 
970
1129
  @status = hash[:status]
971
1130
  @status_changed = Time.parse( hash[:status_changed] )
1131
+ @status_last_changed = Time.parse( hash[:status_last_changed] )
972
1132
  @ack = Arborist::Node::Ack.from_hash( hash[:ack] ) if hash[:ack]
973
1133
 
974
1134
  @errors = hash[:errors]
1135
+ @warnings = hash[:warnings]
975
1136
  @properties = hash[:properties] || {}
976
1137
  @last_contacted = Time.parse( hash[:last_contacted] )
977
1138
  @quieted_reasons = hash[:quieted_reasons] || {}
@@ -982,7 +1143,7 @@ class Arborist::Node
982
1143
  h[ k ] = Hash.new( &h.default_proc )
983
1144
  end
984
1145
 
985
- @pending_update_events = []
1146
+ @pending_change_events = []
986
1147
  @subscriptions = {}
987
1148
 
988
1149
  end
@@ -1012,6 +1173,26 @@ class Arborist::Node
1012
1173
  self.log.info "Node %s ACK cleared explicitly" % [ self.identifier ]
1013
1174
  @ack = nil
1014
1175
  end
1176
+
1177
+ self.add_previous_ack_to_update_delta
1178
+ end
1179
+
1180
+
1181
+ ### Save off the current acknowledgement so it can be used after transitions
1182
+ ### which unset it.
1183
+ def save_previous_ack
1184
+ self.log.debug "Saving previous ack: %p" % [ self.ack ]
1185
+ self.previous_ack = self.ack
1186
+ end
1187
+
1188
+
1189
+ ### Add the previous and current acknowledgement to the delta if either of them
1190
+ ### are set.
1191
+ def add_previous_ack_to_update_delta
1192
+ unless self.ack == self.previous_ack
1193
+ self.log.debug "Adding previous ack to the update delta: %p" % [ self.previous_ack ]
1194
+ self.update_delta[ 'ack' ] = [ self.previous_ack&.to_h, self.ack&.to_h ]
1195
+ end
1015
1196
  end
1016
1197
 
1017
1198
 
@@ -1023,8 +1204,7 @@ class Arborist::Node
1023
1204
  end
1024
1205
 
1025
1206
 
1026
- ### State machine guard predicate -- Returns +true+ if the last time the node
1027
- ### was monitored resulted in an update.
1207
+ ### State machine guard predicate -- returns +true+ if the node has errors.
1028
1208
  def has_errors?
1029
1209
  has_errors = ! self.errors.empty?
1030
1210
  self.log.debug "Checking to see if last contact cleared remaining errors (it %s)" %
@@ -1034,6 +1214,36 @@ class Arborist::Node
1034
1214
  end
1035
1215
 
1036
1216
 
1217
+ ### State machine guard predicate -- Returns +true+ if the node has errors
1218
+ ### and does not have an ACK status set.
1219
+ def has_unacked_errors?
1220
+ return self.has_errors? && !self.ack_set?
1221
+ end
1222
+
1223
+
1224
+ ### State machine guard predicate -- returns +true+ if the node has warnings.
1225
+ def has_warnings?
1226
+ has_warnings = ! self.warnings.empty?
1227
+ self.log.debug "Checking to see if last contact cleared remaining warnings (it %s)" %
1228
+ [ has_warnings ? "did not" : "did" ]
1229
+ self.log.debug "Warnings are: %p" % [ self.warnings ]
1230
+ return has_warnings
1231
+ end
1232
+
1233
+
1234
+ ### State machine guard predicate -- returns +true+ if the node has warnings or errors.
1235
+ def has_errors_or_warnings?
1236
+ return self.has_errors? || self.has_warnings?
1237
+ end
1238
+
1239
+
1240
+ ### State machine guard predicate -- returns +true+ if the node has warnings but
1241
+ ### no errors.
1242
+ def has_only_warnings?
1243
+ return self.has_warnings? && ! self.has_errors?
1244
+ end
1245
+
1246
+
1037
1247
  ### Return a string describing the errors that are set on the node.
1038
1248
  def errors_description
1039
1249
  return "No errors" if self.errors.empty?
@@ -1042,6 +1252,16 @@ class Arborist::Node
1042
1252
  end.join( '; ' )
1043
1253
  end
1044
1254
 
1255
+
1256
+ ### Return a string describing the warnings that are set on the node.
1257
+ def warnings_description
1258
+ return "No warnings" if self.warnings.empty?
1259
+ return self.warnings.map do |key, msg|
1260
+ "%s: %s" % [ key, msg ]
1261
+ end.join( '; ' )
1262
+ end
1263
+
1264
+
1045
1265
  #
1046
1266
  # :section: State Callbacks
1047
1267
  #
@@ -1055,6 +1275,7 @@ class Arborist::Node
1055
1275
 
1056
1276
  ### Update the last status change time.
1057
1277
  def update_status_changed( transition )
1278
+ self.status_last_changed = self.status_changed
1058
1279
  self.status_changed = Time.now
1059
1280
  end
1060
1281
 
@@ -1063,7 +1284,7 @@ class Arborist::Node
1063
1284
  def make_transition_event( transition )
1064
1285
  node_type = "node_%s" % [ transition.to ]
1065
1286
  self.log.debug "Making a %s event for %p" % [ node_type, transition ]
1066
- self.pending_update_events << Arborist::Event.create( node_type, self )
1287
+ self.pending_change_events << Arborist::Event.create( node_type, self )
1067
1288
  end
1068
1289
 
1069
1290
 
@@ -1075,8 +1296,8 @@ class Arborist::Node
1075
1296
 
1076
1297
  ### Callback for when an acknowledgement is cleared.
1077
1298
  def on_ack_cleared( transition )
1078
- self.log.warn "ACK cleared for %s" % [ self.identifier ]
1079
1299
  self.ack = nil
1300
+ self.log.warn "ACK cleared for %s" % [ self.identifier ]
1080
1301
  end
1081
1302
 
1082
1303
 
@@ -1094,6 +1315,13 @@ class Arborist::Node
1094
1315
  end
1095
1316
 
1096
1317
 
1318
+ ### Callback for when a node goes from up to warn
1319
+ def on_node_warn( transition )
1320
+ self.log.error "%s is %s" % [ self.identifier, self.status_description ]
1321
+ self.update_delta[ 'warnings' ] = [ nil, self.warnings_description ]
1322
+ end
1323
+
1324
+
1097
1325
  ### Callback for when a node goes from up to disabled
1098
1326
  def on_node_disabled( transition )
1099
1327
  self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
@@ -1115,6 +1343,7 @@ class Arborist::Node
1115
1343
  ### Callback for when a node goes from disabled to unknown
1116
1344
  def on_node_enabled( transition )
1117
1345
  self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
1346
+ self.ack = nil
1118
1347
  end
1119
1348
 
1120
1349