arborist 0.3.0 → 0.4.0

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.
data/History.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v0.4.0 [2018-11-21] Mahlon E. Smith <mahlon@martini.nu>
2
+
3
+ Enhancements:
4
+
5
+ - Add an 'informational' predicate for node events.
6
+ - Introduce basic flap heuristics for node state changes.
7
+
8
+
9
+ Fixes:
10
+
11
+ - Clean up excess ruby warnings.
12
+
13
+
14
+
1
15
  ## v0.3.0 [2018-08-29] Michael Granger <ged@FaerieMUD.org>
2
16
 
3
17
  Enhancements:
data/Rakefile CHANGED
@@ -38,7 +38,7 @@ hoespec = Hoe.spec 'arborist' do |spec|
38
38
  spec.developer 'Michael Granger', 'ged@FaerieMUD.org'
39
39
  spec.developer 'Mahlon E. Smith', 'mahlon@martini.nu'
40
40
 
41
- spec.dependency 'schedulability', '~> 0.1'
41
+ spec.dependency 'schedulability', '~> 0.4'
42
42
  spec.dependency 'loggability', '~> 0.12'
43
43
  spec.dependency 'configurability', '~> 3.0'
44
44
  spec.dependency 'pluggability', '~> 0.4'
@@ -14,10 +14,10 @@ module Arborist
14
14
  Configurability
15
15
 
16
16
  # Package version
17
- VERSION = '0.3.0'
17
+ VERSION = '0.4.0'
18
18
 
19
19
  # Version control revision
20
- REVISION = %q$Revision: db70f921e653 $
20
+ REVISION = %q$Revision$
21
21
 
22
22
 
23
23
  # The name of the environment variable which can be used to set the config path
@@ -242,7 +242,7 @@ class Arborist::Client
242
242
  ### Modify operational attributes of a node.
243
243
  def modify( *args )
244
244
  request = self.make_modify_request( *args )
245
- response = self.send_tree_api_request( request )
245
+ self.send_tree_api_request( request )
246
246
  return true
247
247
  end
248
248
 
@@ -263,7 +263,7 @@ class Arborist::Client
263
263
  ### +time+ of now.
264
264
  def acknowledge( *args )
265
265
  request = self.make_acknowledge_request( *args )
266
- response = self.send_tree_api_request( request )
266
+ self.send_tree_api_request( request )
267
267
  return true
268
268
  end
269
269
  alias_method :ack, :acknowledge
@@ -286,7 +286,7 @@ class Arborist::Client
286
286
  ### Clear the acknowledgement for a node.
287
287
  def clear_acknowledgement( *args )
288
288
  request = self.make_unack_request( *args )
289
- response = self.send_tree_api_request( request )
289
+ self.send_tree_api_request( request )
290
290
  return true
291
291
  end
292
292
  alias_method :unack, :clear_acknowledgement
@@ -22,7 +22,7 @@ module Arborist::CLI::Summary
22
22
 
23
23
  desc 'Summarize known problems'
24
24
 
25
- command :summary do |cmd|
25
+ command [ :summary, :status ] do |cmd|
26
26
 
27
27
  cmd.flag [:s, :sort],
28
28
  type: String,
@@ -45,6 +45,14 @@ class Arborist::Event
45
45
  end
46
46
 
47
47
 
48
+ ### Returns +true+ if the event contains node information other than about a
49
+ ### change in its state.
50
+ def informational?
51
+ return false
52
+ end
53
+ alias_method :is_informational?, :informational?
54
+
55
+
48
56
  ### Match operator -- returns +true+ if the other object matches this event.
49
57
  def match( object )
50
58
  rval = object.respond_to?( :event_type ) &&
@@ -41,9 +41,10 @@ class Arborist::Event::Node < Arborist::Event
41
41
  ### Inject useful node metadata into the generated hash.
42
42
  def to_h
43
43
  return super.merge(
44
- identifier: self.node.identifier,
45
- parent: self.node.parent,
46
- nodetype: self.node.type
44
+ identifier: self.node.identifier,
45
+ parent: self.node.parent,
46
+ nodetype: self.node.type,
47
+ flapping: self.node.flapping?
47
48
  )
48
49
  end
49
50
 
@@ -25,6 +25,13 @@ class Arborist::Event::NodeDelta < Arborist::Event::Node
25
25
  end
26
26
 
27
27
 
28
+ ### Returns +true+ if the event contains node information other than about a
29
+ ### change in its state.
30
+ def informational?
31
+ return true
32
+ end
33
+
34
+
28
35
  ### Returns +true+ if the specified +object+ matches this event.
29
36
  def match( object )
30
37
  rval = super &&
@@ -7,4 +7,11 @@ require 'arborist/event/node'
7
7
  # An event sent on every node update, regardless of whether or not the update resulted in
8
8
  # any changes
9
9
  class Arborist::Event::NodeUpdate < Arborist::Event::Node
10
+
11
+ ### Returns +true+ if the event contains node information other than about a
12
+ ### change in its state.
13
+ def informational?
14
+ return true
15
+ end
16
+
10
17
  end # class Arborist::Event::NodeUpdate
@@ -110,7 +110,7 @@ module Arborist::Monitor::Socket
110
110
  sock = conn_hash[:conn]
111
111
  # Why getpeername? Testing socket success without read()ing, I think?
112
112
  # FreeBSD source?
113
- res = sock.getpeername
113
+ sock.getpeername
114
114
  return {
115
115
  tcp_socket_connect: { duration: duration }
116
116
  }
@@ -169,8 +169,8 @@ class Arborist::MonitorRunner
169
169
  err.message
170
170
  ]
171
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 }
172
+ nodes.keys.each_with_object({}) do |id, node_results|
173
+ node_results[id] = { error: errmsg }
174
174
  end
175
175
  end
176
176
 
@@ -230,8 +230,6 @@ class Arborist::MonitorRunner
230
230
 
231
231
  ### Register a timer for the specified +monitor+.
232
232
  def add_timer_for( monitor )
233
- interval = monitor.interval
234
-
235
233
  if monitor.splay.nonzero?
236
234
  self.add_splay_timer_for( monitor )
237
235
  else
@@ -7,8 +7,10 @@ require 'time'
7
7
  require 'pathname'
8
8
  require 'state_machines'
9
9
 
10
+ require 'configurability'
10
11
  require 'loggability'
11
12
  require 'pluggability'
13
+
12
14
  require 'arborist' unless defined?( Arborist )
13
15
  require 'arborist/mixins'
14
16
  require 'arborist/exceptions'
@@ -23,6 +25,7 @@ class Arborist::Node
23
25
  Arborist::HashUtilities
24
26
  extend Loggability,
25
27
  Pluggability,
28
+ Configurability,
26
29
  Arborist::MethodUtilities
27
30
 
28
31
 
@@ -46,6 +49,7 @@ class Arborist::Node
46
49
  last_contacted
47
50
  ack
48
51
  errors
52
+ warnings
49
53
  quieted_reasons
50
54
  config
51
55
  ]
@@ -69,6 +73,17 @@ class Arborist::Node
69
73
  plugin_prefixes 'arborist/node'
70
74
 
71
75
 
76
+ # Configurability API
77
+ configurability( 'arborist.node' ) do
78
+
79
+ # How many status entries to keep in a ring buffer
80
+ setting :status_history_size, default: 0
81
+
82
+ # How many status transitions in the history constitutes flapping
83
+ setting :flap_threshold, default: 0
84
+ end
85
+
86
+
72
87
  ##
73
88
  # :method: unknown?
74
89
  # Returns +true+ if the node is in an 'unknown' state.
@@ -166,6 +181,9 @@ class Arborist::Node
166
181
  after_transition any => any, do: :update_status_changed
167
182
 
168
183
  after_transition do: :add_status_to_update_delta
184
+
185
+ after_transition do: :record_status_history
186
+ after_failure do: :record_status_history
169
187
  end
170
188
 
171
189
 
@@ -308,11 +326,15 @@ class Arborist::Node
308
326
  @source = nil
309
327
  @children = {}
310
328
  @dependencies = Arborist::Dependency.new( :all )
329
+ @flap_threshold = nil
330
+ @flapping = false
331
+ @status_history_size = nil
311
332
 
312
333
  # Primary state
313
334
  @status = 'unknown'
314
335
  @status_changed = Time.at( 0 )
315
336
  @status_last_changed = Time.at( 0 )
337
+ @status_history = []
316
338
 
317
339
  # Attributes that govern state
318
340
  @errors = {}
@@ -407,6 +429,14 @@ class Arborist::Node
407
429
  # type of dependency it came from (either :primary or :secondary).
408
430
  attr_reader :quieted_reasons
409
431
 
432
+ ##
433
+ # An array of statuses, retained after an update.
434
+ attr_reader :status_history
435
+
436
+ ##
437
+ # The current flapping state of this node.
438
+ attr_predicate_accessor :flapping
439
+
410
440
 
411
441
  ### Set the source of the node to +source+, which should be a valid URI.
412
442
  def source=( source )
@@ -501,6 +531,22 @@ class Arborist::Node
501
531
  end
502
532
 
503
533
 
534
+ ### Get or set the number of entries to store for the status
535
+ ### history.
536
+ def status_history_size( new_size=nil )
537
+ @status_history_size = new_size if new_size
538
+ return @status_history_size || Arborist::Node.status_history_size || 0
539
+ end
540
+
541
+
542
+ ### Get or set the number of transitions in the status
543
+ ### history to determine if a node is considering 'flapping'.
544
+ def flap_threshold( new_count=nil )
545
+ @flap_threshold = new_count if new_count
546
+ return @flap_threshold || Arborist::Node.flap_threshold || 0
547
+ end
548
+
549
+
504
550
  #
505
551
  # :section: Manager API
506
552
  # Methods used by the manager to manage its nodes.
@@ -813,8 +859,8 @@ class Arborist::Node
813
859
  self.log.debug "No handler for a %s event!" % [ event.type ]
814
860
  end
815
861
 
816
- self.log.debug ">>> Pending change events before: %p" % [ self.pending_change_events ]
817
-
862
+ # Don't transition on informational events
863
+ return if event.informational?
818
864
  super # to state-machine
819
865
 
820
866
  results = self.pending_change_events.clone
@@ -1054,14 +1100,17 @@ class Arborist::Node
1054
1100
  ### used to overlay selective bits of the saved node tree to the equivalent nodes loaded
1055
1101
  ### from node definitions.
1056
1102
  def restore( old_node )
1057
- @status = old_node.status
1058
- @properties = old_node.properties.dup
1059
- @ack = old_node.ack.dup if old_node.ack
1060
- @last_contacted = old_node.last_contacted
1061
- @status_changed = old_node.status_changed
1062
- @errors = old_node.errors
1063
- @warnings = old_node.warnings
1064
- @quieted_reasons = old_node.quieted_reasons
1103
+ @status = old_node.status
1104
+ @properties = old_node.properties.dup
1105
+ @ack = old_node.ack.dup if old_node.ack
1106
+ @last_contacted = old_node.last_contacted
1107
+ @status_changed = old_node.status_changed
1108
+ @status_history = old_node.status_history
1109
+ @status_history_size = old_node.status_history_size
1110
+ @flapping = old_node.flapping?
1111
+ @errors = old_node.errors
1112
+ @warnings = old_node.warnings
1113
+ @quieted_reasons = old_node.quieted_reasons
1065
1114
  @status_last_changed = old_node.status_last_changed
1066
1115
 
1067
1116
  # Only merge in downed dependencies.
@@ -1088,6 +1137,9 @@ class Arborist::Node
1088
1137
  last_contacted: self.last_contacted ? self.last_contacted.iso8601 : nil,
1089
1138
  status_changed: self.status_changed ? self.status_changed.iso8601 : nil,
1090
1139
  status_last_changed: self.status_last_changed ? self.status_last_changed.iso8601 : nil,
1140
+ status_history: self.status_history,
1141
+ status_history_size: self.status_history_size,
1142
+ flapping: self.flapping?,
1091
1143
  errors: self.errors,
1092
1144
  warnings: self.warnings,
1093
1145
  dependencies: self.dependencies.to_h,
@@ -1117,29 +1169,32 @@ class Arborist::Node
1117
1169
  ### previously-marshalled node.
1118
1170
  def marshal_load( hash )
1119
1171
  self.log.debug "Restoring from serialized hash: %p" % [ hash ]
1120
- @identifier = hash[:identifier]
1121
- @properties = hash[:properties]
1172
+ @identifier = hash[:identifier]
1173
+ @properties = hash[:properties]
1122
1174
 
1123
- @parent = hash[:parent]
1124
- @description = hash[:description]
1125
- @tags = Set.new( hash[:tags] )
1126
- @config = hash[:config]
1127
- @children = {}
1175
+ @parent = hash[:parent]
1176
+ @description = hash[:description]
1177
+ @tags = Set.new( hash[:tags] )
1178
+ @config = hash[:config]
1179
+ @children = {}
1128
1180
 
1129
- @status = hash[:status]
1130
- @status_changed = Time.parse( hash[:status_changed] )
1181
+ @status = hash[:status]
1182
+ @status_changed = Time.parse( hash[:status_changed] )
1131
1183
  @status_last_changed = Time.parse( hash[:status_last_changed] )
1132
- @ack = Arborist::Node::Ack.from_hash( hash[:ack] ) if hash[:ack]
1133
-
1134
- @errors = hash[:errors]
1135
- @warnings = hash[:warnings]
1136
- @properties = hash[:properties] || {}
1137
- @last_contacted = Time.parse( hash[:last_contacted] )
1138
- @quieted_reasons = hash[:quieted_reasons] || {}
1184
+ @status_history = hash[:status_history]
1185
+ @status_history_size = hash[:status_history_size]
1186
+ @flapping = hash[:flapping]
1187
+ @ack = Arborist::Node::Ack.from_hash( hash[:ack] ) if hash[:ack]
1188
+
1189
+ @errors = hash[:errors]
1190
+ @warnings = hash[:warnings]
1191
+ @properties = hash[:properties] || {}
1192
+ @last_contacted = Time.parse( hash[:last_contacted] )
1193
+ @quieted_reasons = hash[:quieted_reasons] || {}
1139
1194
  self.log.debug "Deps are: %p" % [ hash[:dependencies] ]
1140
- @dependencies = hash[:dependencies]
1195
+ @dependencies = hash[:dependencies]
1141
1196
 
1142
- @update_delta = Hash.new do |h,k|
1197
+ @update_delta = Hash.new do |h,k|
1143
1198
  h[ k ] = Hash.new( &h.default_proc )
1144
1199
  end
1145
1200
 
@@ -1164,6 +1219,23 @@ class Arborist::Node
1164
1219
  protected
1165
1220
  #########
1166
1221
 
1222
+ ### Detects if this node is considered 'flapping', returning +true+ if so.
1223
+ def check_flapping
1224
+ return false if self.flap_threshold.zero?
1225
+
1226
+ runs = self.status_history.each_cons( 2 ).count do |status_1, status_2|
1227
+ status_1 != status_2
1228
+ end
1229
+
1230
+ self.log.debug "Observed %d changes in %d samples." % [
1231
+ runs,
1232
+ self.status_history.size
1233
+ ]
1234
+
1235
+ return runs >= self.flap_threshold
1236
+ end
1237
+
1238
+
1167
1239
  ### Ack the node with the specified +ack_data+, which should contain
1168
1240
  def ack=( ack_data )
1169
1241
  if ack_data
@@ -1356,6 +1428,30 @@ class Arborist::Node
1356
1428
  end
1357
1429
 
1358
1430
 
1431
+ ### Add a flap change to the delta event.
1432
+ def add_flap_to_update_delta( from, to )
1433
+ return if from == to
1434
+ self.update_delta[ 'flapping' ] = [ from, to ]
1435
+ end
1436
+
1437
+
1438
+ ### Retain the status in the node's history.
1439
+ ###
1440
+ def record_status_history
1441
+ retain = self.status_history_size
1442
+ return if retain.zero?
1443
+
1444
+ pre_state = self.flapping?
1445
+ self.status_history << self.status
1446
+ self.flapping = self.check_flapping
1447
+
1448
+ current_size = self.status_history.size
1449
+ self.status_history.slice!( 0, current_size - retain ) if current_size >= retain
1450
+
1451
+ self.add_flap_to_update_delta( pre_state, self.flapping? )
1452
+ end
1453
+
1454
+
1359
1455
  #######
1360
1456
  private
1361
1457
  #######
@@ -37,7 +37,7 @@ class Arborist::Node::Root < Arborist::Node
37
37
  def initialize( * )
38
38
  super( '_' ) do
39
39
  description "The root node."
40
- source = URI( __FILE__ )
40
+ self.source = URI( __FILE__ )
41
41
  end
42
42
 
43
43
  @status = 'up'
@@ -19,12 +19,13 @@ class Arborist::Observer::Action
19
19
  ### Create a new Action that will call the specified +block+ +during+ the given schedule,
20
20
  ### but only +after+ the specified number of events have arrived +within+ the given
21
21
  ### time threshold.
22
- def initialize( within: 0, after: 1, during: nil, &block )
22
+ def initialize( within: 0, after: 1, during: nil, ignore_flapping: false, &block )
23
23
  raise ArgumentError, "Action requires a block" unless block
24
24
 
25
25
  @block = block
26
26
  @time_threshold = within
27
27
  @schedule = Schedulability::Schedule.parse( during ) if during
28
+ @ignore_flapping = ignore_flapping
28
29
 
29
30
  if within.zero?
30
31
  @count_threshold = after
@@ -58,6 +59,11 @@ class Arborist::Observer::Action
58
59
  # The schedule that applies to this action.
59
60
  attr_reader :schedule
60
61
 
62
+ ##
63
+ # Take no action if the node the event belongs to is in a flapping
64
+ # state.
65
+ attr_reader :ignore_flapping
66
+
61
67
  ##
62
68
  # The Hash of recent events, keyed by their arrival time.
63
69
  attr_reader :event_history
@@ -66,7 +72,7 @@ class Arborist::Observer::Action
66
72
  ### Call the action for the specified +event+.
67
73
  def handle_event( event )
68
74
  self.record_event( event )
69
- self.call_block( event ) if self.should_run?
75
+ self.call_block( event ) if self.should_run? && ! self.flapping?( event )
70
76
  end
71
77
 
72
78
 
@@ -107,6 +113,13 @@ class Arborist::Observer::Action
107
113
  end
108
114
 
109
115
 
116
+ ### Returns +true+ if this observer respects the flapping state of
117
+ ### a node, and the generated event is attached to a flapping node.
118
+ def flapping?( event )
119
+ return self.ignore_flapping && event[ 'flapping' ]
120
+ end
121
+
122
+
110
123
  ### Returns +true+ if the time between the first and last event in the #event_history is
111
124
  ### less than the #time_threshold.
112
125
  def time_threshold_exceeded?