arborist 0.3.0 → 0.4.0

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