arborist 0.0.1.pre20160829140603 → 0.0.1.pre20161005112841

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.
@@ -19,7 +19,6 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
19
19
  ### Create the TreeAPI handler that will read requests from the specified +pollable+
20
20
  ### and call into the +manager+ to respond to them.
21
21
  def initialize( pollable, manager )
22
- self.log.debug "Setting up a %p" % [ self.class ]
23
22
  @pollitem = pollable
24
23
  @enabled = true
25
24
  @manager = manager
@@ -41,8 +40,6 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
41
40
 
42
41
  ### Handle the specified +raw_request+ and return a response.
43
42
  def handle_request( raw_request )
44
- self.log.debug "Handling request: %p" % [ raw_request ]
45
-
46
43
  raise "Manager is shutting down" unless self.enabled?
47
44
 
48
45
  header, body = self.parse_request( raw_request )
@@ -68,13 +65,11 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
68
65
  ### Attempt to dispatch a request given its +header+ and +body+, and return the
69
66
  ### serialized response.
70
67
  def dispatch_request( header, body )
71
- self.log.debug "Dispatching request %p -> %p" % [ header, body ]
72
68
  handler = self.lookup_request_action( header ) or
73
69
  raise Arborist::RequestError, "No such action '%s'" % [ header['action'] ]
74
70
 
75
71
  response = handler.call( header, body )
76
72
 
77
- self.log.debug "Returning response: %p" % [ response ]
78
73
  return response
79
74
  end
80
75
 
@@ -96,7 +91,6 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
96
91
  msg = [
97
92
  { category: category, reason: reason, success: false, version: 1 }
98
93
  ]
99
- self.log.debug "Returning error response: %p" % [ msg ]
100
94
  return MessagePack.pack( msg )
101
95
  end
102
96
 
@@ -107,7 +101,6 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
107
101
  { success: true, version: 1 },
108
102
  body
109
103
  ]
110
- self.log.debug "Returning successful response: %p" % [ msg ]
111
104
  return MessagePack.pack( msg )
112
105
  end
113
106
 
@@ -120,8 +113,6 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
120
113
  raise Arborist::RequestError, err.message
121
114
  end
122
115
 
123
- self.log.debug "Parsed request: %p" % [ tuple ]
124
-
125
116
  raise Arborist::RequestError, 'not a tuple' unless tuple.is_a?( Array )
126
117
  raise Arborist::RequestError, 'incorrect length' if tuple.length.zero? || tuple.length > 2
127
118
 
@@ -158,7 +149,13 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
158
149
  self.log.debug "SUBSCRIBE: %p" % [ header ]
159
150
  event_type = header[ 'event_type' ]
160
151
  node_identifier = header[ 'identifier' ]
161
- subscription = @manager.create_subscription( node_identifier, event_type, body )
152
+
153
+ body = [ body ] unless body.is_a?( Array )
154
+ positive = body.shift
155
+ negative = body.shift || {}
156
+
157
+ subscription = @manager.
158
+ create_subscription( node_identifier, event_type, positive, negative )
162
159
 
163
160
  return successful_response([ subscription.id ])
164
161
  end
@@ -225,6 +222,10 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
225
222
  def handle_update_request( header, body )
226
223
  self.log.debug "UPDATE: %p" % [ header ]
227
224
 
225
+ unless body.respond_to?( :each )
226
+ return error_response( 'client', 'Malformed update: body does not respond to #each' )
227
+ end
228
+
228
229
  body.each do |identifier, properties|
229
230
  @manager.update_node( identifier, properties )
230
231
  end
@@ -301,14 +301,14 @@ class Arborist::Monitor
301
301
  ### Specify that the monitor should include the specified +criteria+ when searching
302
302
  ### for nodes it will run against.
303
303
  def match( criteria )
304
- @positive_criteria.merge!( criteria )
304
+ self.positive_criteria.merge!( criteria )
305
305
  end
306
306
 
307
307
 
308
308
  ### Specify that the monitor should exclude nodes which match the specified
309
309
  ### +criteria+ when searching for nodes it will run against.
310
310
  def exclude( criteria )
311
- @negative_criteria.merge!( criteria )
311
+ self.negative_criteria.merge!( criteria )
312
312
  end
313
313
 
314
314
 
@@ -1,6 +1,7 @@
1
1
  # -*- ruby -*-
2
2
  #encoding: utf-8
3
3
 
4
+ require 'time'
4
5
  require 'loggability'
5
6
  require 'timeout'
6
7
  require 'socket'
@@ -118,10 +119,9 @@ module Arborist::Monitor::Socket
118
119
  # Now wait for connections to complete
119
120
  wait_seconds = timeout_at - Time.now
120
121
  until connections.empty? || wait_seconds <= 0
121
- self.log.debug "Waiting on %d connections for %0.3ds..." %
122
+ self.log.debug "Waiting on %d connections for %0.3fs..." %
122
123
  [ connections.values.length, wait_seconds ]
123
124
 
124
- # :FIXME: Race condition: errors if timeout_at - Time.now is 0
125
125
  _, ready, _ = IO.select( nil, connections.keys, nil, wait_seconds )
126
126
 
127
127
  now = Time.now
@@ -129,14 +129,18 @@ module Arborist::Monitor::Socket
129
129
  identifier, sockaddr = *connections.delete( sock )
130
130
 
131
131
  begin
132
- sock.connect_nonblock( sockaddr )
133
- rescue Errno::EISCONN
132
+ res = sock.getpeername
133
+ self.log.debug "connected to %s" % [ identifier ]
134
134
  results[ identifier ] = {
135
- tcp_socket_connect: { time: now.to_s, duration: now - start }
135
+ tcp_socket_connect: { time: now.iso8601, duration: now - start }
136
136
  }
137
137
  rescue SocketError, SystemCallError => err
138
- self.log.debug "%p during connection: %s" % [ err.class, err.message ]
139
- results[ identifier ] = { error: err.message }
138
+ begin
139
+ sock.read( 1 )
140
+ rescue => err
141
+ self.log.debug "read: %p: %s" % [ err.class, err.message ]
142
+ results[ identifier ] = { error: err.message }
143
+ end
140
144
  ensure
141
145
  sock.close
142
146
  end
@@ -257,7 +261,7 @@ module Arborist::Monitor::Socket
257
261
  if ready.nil?
258
262
  now = Time.now
259
263
  results[ identifier ] = {
260
- udp_socket_connect: { time: now.to_s, duration: now - start }
264
+ udp_socket_connect: { time: now.iso8601, duration: now - start }
261
265
  }
262
266
  self.log.debug " connection successful"
263
267
  else
data/lib/arborist/node.rb CHANGED
@@ -33,6 +33,21 @@ class Arborist::Node
33
33
  # Regex to match a valid identifier
34
34
  VALID_IDENTIFIER = /^\w[\w\-]*$/
35
35
 
36
+ # The attributes of a node which are used in the operation of the system
37
+ OPERATIONAL_ATTRIBUTES = %i[
38
+ type
39
+ status
40
+ tags
41
+ parent
42
+ description
43
+ dependencies
44
+ status_changed
45
+ last_contacted
46
+ ack
47
+ error
48
+ quieted_reasons
49
+ config
50
+ ]
36
51
 
37
52
  autoload :Root, 'arborist/node/root'
38
53
  autoload :Ack, 'arborist/node/ack'
@@ -98,15 +113,14 @@ class Arborist::Node
98
113
  :quieted
99
114
 
100
115
  event :update do
116
+ transition [:up, :unknown] => :disabled, if: :ack_set?
101
117
  transition [:down, :unknown, :acked] => :up, if: :last_contact_successful?
102
118
  transition [:up, :unknown] => :down, unless: :last_contact_successful?
103
119
  transition :down => :acked, if: :ack_set?
104
- transition [:unknown, :up] => :disabled, if: :ack_set?
105
120
  transition :disabled => :unknown, unless: :ack_set?
106
121
  end
107
122
 
108
123
  event :handle_event do
109
- transition :unknown => :acked, if: :ack_and_error_set?
110
124
  transition any - [:disabled, :quieted, :acked] => :quieted, if: :has_quieted_reason?
111
125
  transition :quieted => :unknown, unless: :has_quieted_reason?
112
126
  end
@@ -541,7 +555,15 @@ class Arborist::Node
541
555
 
542
556
 
543
557
  ### Returns +true+ if the specified search +criteria+ all match this node.
544
- def matches?( criteria )
558
+ def matches?( criteria, if_empty: true )
559
+
560
+ # Omit 'delta' criteria from matches; delta matching is done separately.
561
+ criteria = criteria.dup
562
+ criteria.delete( 'delta' )
563
+
564
+ self.log.debug "Node matching %p (%p if empty)" % [ criteria, if_empty ]
565
+ return if_empty if criteria.empty?
566
+
545
567
  self.log.debug "Matching %p against criteria: %p" % [ self, criteria ]
546
568
  return criteria.all? do |key, val|
547
569
  self.match_criteria?( key, val )
@@ -552,8 +574,6 @@ class Arborist::Node
552
574
  ### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
553
575
  def match_criteria?( key, val )
554
576
  return case key
555
- when 'delta'
556
- true
557
577
  when 'status'
558
578
  self.status == val
559
579
  when 'type'
@@ -574,6 +594,7 @@ class Arborist::Node
574
594
  def fetch_values( value_spec=nil )
575
595
  state = self.properties.merge( self.operational_values )
576
596
  state = stringify_keys( state )
597
+ state = make_serializable( state )
577
598
 
578
599
  if value_spec
579
600
  self.log.debug "Eliminating all values except: %p (from keys: %p)" %
@@ -588,12 +609,9 @@ class Arborist::Node
588
609
  ### Return a Hash of the operational values that are included with the node's
589
610
  ### monitor state.
590
611
  def operational_values
591
- values = {
592
- type: self.type,
593
- status: self.status,
594
- tags: self.tags
595
- }
596
- values[:ack] = self.ack.to_h if self.ack
612
+ values = OPERATIONAL_ATTRIBUTES.each_with_object( {} ) do |key, hash|
613
+ hash[ key ] = self.send( key )
614
+ end
597
615
 
598
616
  return values
599
617
  end
@@ -644,11 +662,19 @@ class Arborist::Node
644
662
  def handle_event( event )
645
663
  self.log.debug "Handling %p" % [ event ]
646
664
  handler_name = "handle_%s_event" % [ event.type.gsub('.', '_') ]
665
+
647
666
  if self.respond_to?( handler_name )
648
667
  self.log.debug "Handling a %s event." % [ event.type ]
649
668
  self.method( handler_name ).call( event )
669
+ else
670
+ self.log.debug "No handler for a %s event!" % [ event.type ]
650
671
  end
672
+
651
673
  super # to state-machine
674
+
675
+ self.publish_events( event, *self.pending_update_events )
676
+ ensure
677
+ self.pending_update_events.clear
652
678
  end
653
679
 
654
680
 
@@ -688,6 +714,21 @@ class Arborist::Node
688
714
  end
689
715
 
690
716
 
717
+ def handle_node_disabled_event( event )
718
+ self.log.debug "Got a node.disabled event: %p" % [ event ]
719
+ self.dependencies.mark_down( event.node.identifier )
720
+
721
+ if self.dependencies_down?
722
+ self.quieted_reasons[ :secondary ] = "Secondary dependencies not met: %s" %
723
+ [ self.dependencies.down_reason ]
724
+ end
725
+
726
+ if event.node.identifier == self.parent
727
+ self.quieted_reasons[ :primary ] = "Parent disabled: %s" % [ self.parent ]
728
+ end
729
+ end
730
+
731
+
691
732
  ### Handle a 'node.quieted' event received via broadcast.
692
733
  def handle_node_quieted_event( event )
693
734
  self.log.debug "Got a node.quieted event: %p" % [ event ]
@@ -947,12 +988,6 @@ class Arborist::Node
947
988
  end
948
989
 
949
990
 
950
- ### Returns +true+ if the node has been acked and also has an error set.
951
- def ack_and_error_set?
952
- return self.error && self.ack_set?
953
- end
954
-
955
-
956
991
  #
957
992
  # :section: State Callbacks
958
993
  #
@@ -1056,4 +1091,26 @@ class Arborist::Node
1056
1091
  end
1057
1092
 
1058
1093
 
1094
+ ### Turn any non-msgpack-able objects in the values of a copy of +hash+ to
1095
+ ### values that can be serialized and return the copy.
1096
+ def make_serializable( hash )
1097
+ new_hash = hash.dup
1098
+ new_hash.keys.each do |key|
1099
+ val = new_hash[ key ]
1100
+ case val
1101
+ when Hash
1102
+ new_hash[ key ] = make_serializable( val )
1103
+
1104
+ when Arborist::Dependency,
1105
+ Arborist::Node::Ack
1106
+ new_hash[ key ] = val.to_h
1107
+
1108
+ when Time
1109
+ new_hash[ key ] = val.iso8601
1110
+ end
1111
+ end
1112
+
1113
+ return new_hash
1114
+ end
1115
+
1059
1116
  end # class Arborist::Node
@@ -107,8 +107,8 @@ class Arborist::Observer
107
107
  ### on::
108
108
  ### the identifier of the node to subscribe on, defaults to the root node
109
109
  ## which receives all node events.
110
- def subscribe( to: nil, where: {}, on: nil )
111
- @subscriptions << { criteria: where, identifier: on, event_type: to }
110
+ def subscribe( to: nil, where: {}, exclude: {}, on: nil )
111
+ @subscriptions << { criteria: where, exclude: exclude, identifier: on, event_type: to }
112
112
  end
113
113
 
114
114
 
@@ -20,11 +20,14 @@ class Arborist::Subscription
20
20
 
21
21
  ### Instantiate a new Subscription object given an +event+ pattern
22
22
  ### and event +criteria+.
23
- def initialize( event_type=nil, criteria={}, &callback )
23
+ def initialize( event_type=nil, criteria={}, negative_criteria={}, &callback )
24
24
  raise LocalJumpError, "requires a callback block" unless callback
25
+
25
26
  @callback = callback
26
27
  @event_type = event_type
27
28
  @criteria = stringify_keys( criteria )
29
+ @negative_criteria = stringify_keys( negative_criteria )
30
+
28
31
  @id = self.generate_id
29
32
  end
30
33
 
@@ -42,9 +45,19 @@ class Arborist::Subscription
42
45
  # The Arborist event pattern that this subscription handles.
43
46
  attr_reader :event_type
44
47
 
45
- # Node selection attributes to match
48
+ # Node selection attributes to require
46
49
  attr_reader :criteria
47
50
 
51
+ # Node selection attributes to exclude
52
+ attr_reader :negative_criteria
53
+
54
+
55
+ ### Add the given +criteria+ hash to the #negative_criteria.
56
+ def exclude( criteria )
57
+ criteria = stringify_keys( criteria )
58
+ self.negative_criteria.merge!( criteria )
59
+ end
60
+
48
61
 
49
62
  ### Create an identifier for this subscription object.
50
63
  def generate_id
@@ -65,21 +78,24 @@ class Arborist::Subscription
65
78
 
66
79
  ### Returns +true+ if the receiver is interested in publishing the specified +event+.
67
80
  def interested_in?( event )
68
- self.log.debug "Testing %p against type = %p and criteria = %p" %
69
- [ event, self.event_type, self.criteria ]
70
- return event.match( self )
81
+ self.log.debug "Testing %p against type = %p and criteria = %p but not %p" %
82
+ [ event, self.event_type, self.criteria, self.negative_criteria ]
83
+ rval = event.match( self )
84
+ self.log.debug " event %s match." % [ rval ? "did" : "did NOT" ]
85
+ return rval
71
86
  end
72
87
  alias_method :is_interested_in?, :interested_in?
73
88
 
74
89
 
75
90
  ### Return a String representation of the object suitable for debugging.
76
91
  def inspect
77
- return "#<%p:%#x [%s] for %s events matching: %p -> %p>" % [
92
+ return "#<%p:%#x [%s] for %s events matching: %p %s-> %p>" % [
78
93
  self.class,
79
94
  self.object_id * 2,
80
95
  self.id,
81
96
  self.event_type,
82
97
  self.criteria,
98
+ self.negative_criteria.empty? ? '' : "(but not #{self.negative_criteria.inspect}",
83
99
  self.callback,
84
100
  ]
85
101
  end
@@ -321,7 +321,7 @@ describe Arborist::Dependency do
321
321
 
322
322
  it "can describe the reason it's down" do
323
323
  dep.mark_down( 'node2' )
324
- expect( dep.down_reason ).to match( /node2 is down as of/i )
324
+ expect( dep.down_reason ).to match( /node2 is unavailable as of/i )
325
325
  end
326
326
 
327
327
 
@@ -329,7 +329,7 @@ describe Arborist::Dependency do
329
329
  dep.mark_down( 'node1' )
330
330
  dep.mark_down( 'node2' )
331
331
  # :FIXME: Does order matter in the 'all' case? This assumes no.
332
- expect( dep.down_reason ).to match( /node(1|2) \(and 1 other\) are down as of/i )
332
+ expect( dep.down_reason ).to match( /node(1|2) \(and 1 other\) are unavailable as of/i )
333
333
  end
334
334
 
335
335
  end
@@ -364,7 +364,8 @@ describe Arborist::Dependency do
364
364
  dep.mark_down( 'node2' )
365
365
  dep.mark_down( 'node1' )
366
366
 
367
- expect( dep.down_reason ).to match( /are all down as of/i ).and( include('node1', 'node2') )
367
+ expect( dep.down_reason ).to match( /are all unavailable as of/i ).
368
+ and( include('node1', 'node2') )
368
369
  end
369
370
 
370
371
  end
@@ -59,6 +59,14 @@ describe Arborist::Event::NodeDelta do
59
59
  end
60
60
 
61
61
 
62
+ it "doesn't match a subscription with matching negative criteria" do
63
+ sub = Arborist::Subscription.new( 'node.delta', 'type' => node.type ) {}
64
+ sub.exclude( 'delta' => {status: ['up', 'down']} )
65
+ event = described_class.new( node, 'status' => ['up', 'down'] )
66
+
67
+ expect( event ).to_not match( sub )
68
+ end
69
+
62
70
  end
63
71
 
64
72
 
@@ -45,7 +45,6 @@ describe Arborist::Event::Node do
45
45
 
46
46
 
47
47
  it "matches subscriptions which have non-matching negative criteria" do
48
- pending "Adding negative criteria to subscriptions"
49
48
  negative_criteria = {
50
49
  tag: 'nope'
51
50
  }
@@ -393,17 +393,22 @@ describe Arborist::Manager::TreeAPI, :testing_manager do
393
393
  expect( hdr ).to include( 'success' => true )
394
394
  end
395
395
 
396
+ it "fails with a client error if the body is invalid" do
397
+ msg = pack_message( :update, nil )
398
+ sock.send( msg )
399
+ resmsg = sock.recv
400
+
401
+ hdr, body = unpack_message( resmsg )
402
+ expect( hdr ).to include( 'success' => false )
403
+ expect( hdr['reason'] ).to match( /respond to #each/ )
404
+ end
396
405
  end
397
406
 
398
407
 
399
408
  describe "subscribe" do
400
409
 
401
410
  it "adds a subscription for all event types to the root node by default" do
402
- criteria = {
403
- type: 'host'
404
- }
405
-
406
- msg = pack_message( :subscribe, criteria )
411
+ msg = pack_message( :subscribe, [{}, {}] )
407
412
 
408
413
  resmsg = nil
409
414
  expect {
@@ -422,11 +427,7 @@ describe Arborist::Manager::TreeAPI, :testing_manager do
422
427
 
423
428
 
424
429
  it "adds a subscription to the specified node if an identifier is specified" do
425
- criteria = {
426
- type: 'host'
427
- }
428
-
429
- msg = pack_message( :subscribe, {identifier: 'sidonie'}, criteria )
430
+ msg = pack_message( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
430
431
 
431
432
  resmsg = nil
432
433
  expect {
@@ -444,12 +445,49 @@ describe Arborist::Manager::TreeAPI, :testing_manager do
444
445
  end
445
446
 
446
447
 
447
- it "adds a subscription for node types matching a pattern if one is specified" do
448
- criteria = {
449
- type: 'host'
450
- }
448
+ it "adds a subscription for particular event types if one is specified" do
449
+ msg = pack_message( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
450
+
451
+ resmsg = nil
452
+ expect {
453
+ sock.send( msg )
454
+ resmsg = sock.recv
455
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
456
+ change { manager.root.subscriptions.length }.by( 1 )
457
+ )
458
+ hdr, body = unpack_message( resmsg )
459
+ node = manager.subscriptions[ body.first ]
460
+ sub = node.subscriptions[ body.first ]
461
+
462
+ expect( sub.event_type ).to eq( 'node.acked' )
463
+ end
464
+
465
+
466
+ it "adds a subscription for events which match a pattern if one is specified" do
467
+ criteria = { type: 'host' }
468
+
469
+ msg = pack_message( :subscribe, [criteria, {}] )
470
+
471
+ resmsg = nil
472
+ expect {
473
+ sock.send( msg )
474
+ resmsg = sock.recv
475
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
476
+ change { manager.root.subscriptions.length }.by( 1 )
477
+ )
478
+ hdr, body = unpack_message( resmsg )
479
+ node = manager.subscriptions[ body.first ]
480
+ sub = node.subscriptions[ body.first ]
481
+
482
+ expect( sub.event_type ).to be_nil
483
+ expect( sub.criteria ).to eq({ 'type' => 'host' })
484
+ end
485
+
486
+
487
+ it "adds a subscription for events which don't match a pattern if an exclusion pattern is given" do
488
+ criteria = { type: 'host' }
451
489
 
452
- msg = pack_message( :subscribe, {event_type: 'node.ack'}, criteria )
490
+ msg = pack_message( :subscribe, [{}, criteria] )
453
491
 
454
492
  resmsg = nil
455
493
  expect {
@@ -462,7 +500,8 @@ describe Arborist::Manager::TreeAPI, :testing_manager do
462
500
  node = manager.subscriptions[ body.first ]
463
501
  sub = node.subscriptions[ body.first ]
464
502
 
465
- expect( sub.event_type ).to eq( 'node.ack' )
503
+ expect( sub.event_type ).to be_nil
504
+ expect( sub.negative_criteria ).to eq({ 'type' => 'host' })
466
505
  end
467
506
 
468
507
  end