arborist 0.0.1.pre20160829140603 → 0.0.1.pre20161005112841

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