arborist 0.1.0 → 0.2.0.pre20170519125456

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.
@@ -23,6 +23,7 @@ module Arborist::CLI::Client
23
23
  end
24
24
 
25
25
  client = Arborist::Client.new
26
+ Pry.config.prompt_name = "arborist %s> " % [ Arborist.tree_api_url ]
26
27
  Pry.pry( client )
27
28
  end
28
29
  end
@@ -5,6 +5,7 @@ require 'msgpack'
5
5
 
6
6
  require 'arborist/cli' unless defined?( Arborist::CLI )
7
7
  require 'arborist/client'
8
+ require 'arborist/event_api'
8
9
 
9
10
  # Command to watch events in an Arborist manager.
10
11
  module Arborist::CLI::Watch
@@ -30,12 +31,10 @@ module Arborist::CLI::Watch
30
31
  last_runid = nil
31
32
  prompt.say "Watching for events on manager at %s" % [ client.event_api_url ]
32
33
  loop do
33
- msgsubid = sock.recv
34
- raise "Partial write?!" unless sock.rcvmore?
35
- raw_event = sock.recv
36
- event = MessagePack.unpack( raw_event )
34
+ msg = sock.receive
35
+ subid, event = Arborist::EventAPI.decode( msg )
37
36
 
38
- case msgsubid
37
+ case subid
39
38
  when 'sys.heartbeat'
40
39
  this_runid = event['run_id']
41
40
 
@@ -0,0 +1,35 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'msgpack'
5
+ require 'loggability'
6
+ require 'cztop'
7
+ require 'arborist' unless defined?( Arborist )
8
+
9
+
10
+ module Arborist::EventAPI
11
+ extend Loggability
12
+
13
+
14
+ # Loggability API -- log to arborist's logger
15
+ log_to :arborist
16
+
17
+
18
+ ### Encode an event with the specified +identifier+ and +payload+ as a
19
+ ### CZTop::Message and return it.
20
+ def self::encode( identifier, payload )
21
+ encoded_payload = MessagePack.pack( payload )
22
+ return CZTop::Message.new([ identifier, encoded_payload ])
23
+ end
24
+
25
+
26
+ ### Decode and return the identifier and payload from the specified +msg+ (a CZTop::Message).
27
+ def self::decode( msg )
28
+ identifier, encoded_payload = msg.to_a
29
+ payload = MessagePack.unpack( encoded_payload )
30
+ return identifier, payload
31
+ end
32
+
33
+
34
+ end # class Arborist::Manager::EventPublisher
35
+
@@ -7,10 +7,10 @@ module Arborist
7
7
 
8
8
  class ClientError < RuntimeError; end
9
9
 
10
- class RequestError < ClientError
10
+ class MessageError < ClientError
11
11
 
12
12
  def initialize( reason )
13
- super( "Invalid request (#{reason})" )
13
+ super( "Invalid message (#{reason})" )
14
14
  end
15
15
 
16
16
  end
@@ -6,11 +6,15 @@ require 'pathname'
6
6
  require 'tempfile'
7
7
  require 'configurability'
8
8
  require 'loggability'
9
- require 'rbczmq'
9
+ require 'cztop'
10
+ require 'cztop/reactor'
11
+ require 'cztop/reactor/signal_handling'
10
12
 
11
13
  require 'arborist' unless defined?( Arborist )
12
14
  require 'arborist/node'
13
15
  require 'arborist/mixins'
16
+ require 'arborist/tree_api'
17
+ require 'arborist/event_api'
14
18
 
15
19
 
16
20
  # The main Arborist process -- responsible for coordinating all other activity.
@@ -18,73 +22,57 @@ class Arborist::Manager
18
22
  extend Configurability,
19
23
  Loggability,
20
24
  Arborist::MethodUtilities
25
+ include CZTop::Reactor::SignalHandling
26
+
21
27
 
22
28
  # Signals the manager responds to
23
29
  QUEUE_SIGS = [
24
30
  :INT, :TERM, :HUP, :USR1,
25
31
  # :TODO: :QUIT, :WINCH, :USR2, :TTIN, :TTOU
26
- ]
27
-
28
- # The number of seconds to wait between checks for incoming signals
29
- SIGNAL_INTERVAL = 0.5
30
-
31
- # Configurability API -- set config defaults
32
- CONFIG_DEFAULTS = {
33
- state_file: nil,
34
- checkpoint_frequency: 30000,
35
- heartbeat_frequency: 1000,
36
- linger: 5000
37
- }
32
+ ] & Signal.list.keys.map( &:to_sym )
38
33
 
39
34
 
40
35
  # Use the Arborist logger
41
36
  log_to :arborist
42
37
 
43
38
  # Configurability API -- use the 'arborist' section
44
- config_key :arborist
45
-
46
-
47
- ##
48
- # The Pathname of the file the manager's node tree state is saved to
49
- singleton_attr_accessor :state_file
50
-
51
- ##
52
- # The number of milliseconds between automatic state checkpoints
53
- singleton_attr_accessor :checkpoint_frequency
54
-
55
- ##
56
- # The number of milliseconds between heartbeat events
57
- singleton_attr_accessor :heartbeat_frequency
58
-
59
- ##
60
- # The maximum amount of time to wait for pending events to be delivered during
61
- # shutdown, in milliseconds.
62
- singleton_attr_accessor :linger
39
+ configurability( 'arborist.manager' ) do
63
40
 
41
+ ##
42
+ # The Pathname of the file the manager's node tree state is saved to
43
+ setting :state_file, default: nil do |value|
44
+ value && Pathname( value )
45
+ end
64
46
 
65
- ### Configurability API -- configure the manager
66
- def self::configure( config=nil )
67
- config = self.defaults.merge( config || {} )
68
- self.log.debug "Config is: %p" % [ config ]
69
-
70
- self.state_file = config[:state_file] && Pathname( config[:state_file] )
71
- self.linger = config[:linger].to_i
72
- self.log.info "Linger configured to %p" % [ self.linger ]
47
+ ##
48
+ # The number of seconds between automatic state checkpoints
49
+ setting :checkpoint_frequency, default: 30.0 do |value|
50
+ if value
51
+ value = value.to_f
52
+ value = nil unless value > 0
53
+ end
54
+ value
55
+ end
73
56
 
74
- self.heartbeat_frequency = config[:heartbeat_frequency].to_i ||
75
- CONFIG_DEFAULTS[:heartbeat_frequency]
76
- raise Arborist::ConfigError, "heartbeat frequency must be a positive non-zero integer" if
77
- self.heartbeat_frequency <= 0
57
+ ##
58
+ # The number of seconds between heartbeat events
59
+ setting :heartbeat_frequency, default: 1.0 do |value|
60
+ raise Arborist::ConfigError, "heartbeat must be positive and non-zero" if
61
+ !value || value <= 0
62
+ Float( value )
63
+ end
78
64
 
79
- interval = config[:checkpoint_frequency].to_i
80
- if interval && interval.nonzero?
81
- self.checkpoint_frequency = interval
82
- else
83
- self.checkpoint_frequency = nil
65
+ ##
66
+ # The maximum amount of time to wait for pending events to be delivered during
67
+ # shutdown, in seconds.
68
+ setting :linger, default: 5.0 do |value|
69
+ value && value.to_f
84
70
  end
71
+
85
72
  end
86
73
 
87
74
 
75
+
88
76
  #
89
77
  # Instance methods
90
78
  #
@@ -98,31 +86,19 @@ class Arborist::Manager
98
86
  @subscriptions = {}
99
87
  @tree_built = false
100
88
 
101
- @tree_sock = @event_sock = nil
102
- @signal_timer = nil
103
89
  @start_time = nil
104
90
 
105
91
  @checkpoint_timer = nil
106
- @linger = self.class.linger || Arborist::Manager::CONFIG_DEFAULTS[ :linger ]
92
+ @linger = self.class.linger
107
93
  self.log.info "Linger set to %p" % [ @linger ]
108
94
 
109
- @zmq_loop = ZMQ::Loop.new
110
- # @zmq_loop.verbose = true
111
- @tree_sock = self.setup_tree_socket
112
- @event_sock = self.setup_event_socket
113
-
114
- @api_handler = Arborist::Manager::TreeAPI.new( @tree_sock, self )
115
- @tree_sock.handler = @api_handler
116
- @zmq_loop.register( @tree_sock )
117
-
118
- @event_publisher = Arborist::Manager::EventPublisher.new( @event_sock, self, @zmq_loop )
119
- @event_sock.handler = @event_publisher
120
- @zmq_loop.register( @event_sock )
121
-
122
- @heartbeat_timer = self.make_heartbeat_timer
123
- @checkpoint_timer = self.make_checkpoint_timer
95
+ @reactor = CZTop::Reactor.new
96
+ @tree_socket = nil
97
+ @event_socket = nil
98
+ @event_queue = []
124
99
 
125
- Thread.main[:signal_queue] = []
100
+ @heartbeat_timer = nil
101
+ @checkpoint_timer = nil
126
102
  end
127
103
 
128
104
 
@@ -151,16 +127,20 @@ class Arborist::Manager
151
127
  attr_accessor :start_time
152
128
 
153
129
  ##
154
- # The ZMQ::Handler that manages the IO for the Tree API
155
- attr_reader :api_handler
130
+ # The CZTop::Reactor that runs the event loop
131
+ attr_reader :reactor
132
+
133
+ ##
134
+ # The ZeroMQ socket REP socket that handles Tree API requests
135
+ attr_accessor :tree_socket
156
136
 
157
137
  ##
158
- # The ZMQ::Handler that manages the IO for the event-publication API.
159
- attr_reader :event_publisher
138
+ # The ZeroMQ PUB socket that publishes events for the Event API
139
+ attr_accessor :event_socket
160
140
 
161
141
  ##
162
- # The ZMQ::Loop that will/is acting as the main loop.
163
- attr_reader :zmq_loop
142
+ # The queue of pending Event API events
143
+ attr_reader :event_queue
164
144
 
165
145
  ##
166
146
  # Flag for marking when the tree is built successfully the first time
@@ -172,15 +152,12 @@ class Arborist::Manager
172
152
  attr_reader :linger
173
153
 
174
154
  ##
175
- # The ZMQ::Timer that processes signals
176
- attr_reader :signal_timer
177
-
178
- ##
179
- # The ZMQ::Timer that periodically checkpoints the manager's state (if it's configured to do so)
155
+ # The Timers::Timer that periodically checkpoints the manager's state (if it's
156
+ # configured to do so)
180
157
  attr_reader :checkpoint_timer
181
158
 
182
159
  ##
183
- # The ZMQ::Timer that periodically publishes a heartbeat event
160
+ # The Timers::Timer that periodically publishes a heartbeat event
184
161
  attr_reader :heartbeat_timer
185
162
 
186
163
 
@@ -191,40 +168,52 @@ class Arborist::Manager
191
168
  ### Setup sockets and start the event loop.
192
169
  def run
193
170
  self.log.info "Getting ready to start the manager."
171
+ self.setup_sockets
194
172
  self.publish_system_event( 'startup', start_time: Time.now.to_s, version: Arborist::VERSION )
195
173
  self.register_timers
196
- self.set_signal_handlers
197
- self.start_accepting_requests
198
-
199
- return self # For chaining
200
- ensure
201
- self.restore_signal_handlers
202
- if self.zmq_loop
203
- self.log.debug "Unregistering sockets."
204
- self.zmq_loop.remove( @tree_sock )
205
- @tree_sock.pollable.close
206
- self.zmq_loop.remove( @event_sock )
207
- @event_sock.pollable.close
208
- self.zmq_loop.cancel_timer( @checkpoint_timer ) if @checkpoint_timer
174
+ self.with_signal_handler( reactor, *QUEUE_SIGS ) do
175
+ self.start_accepting_requests
209
176
  end
210
-
177
+ ensure
178
+ self.shutdown_sockets
211
179
  self.save_node_states
180
+ end
212
181
 
213
- self.log.debug "Resetting ZMQ context"
214
- Arborist.reset_zmq_context
182
+
183
+ ### Create the sockets used by the manager and bind them to the appropriate
184
+ ### endpoints.
185
+ def setup_sockets
186
+ self.setup_tree_socket
187
+ self.setup_event_socket
188
+ end
189
+
190
+
191
+ ### Shut down the sockets used by the manager.
192
+ def shutdown_sockets
193
+ self.shutdown_tree_socket
194
+ self.shutdown_event_socket
215
195
  end
216
196
 
217
197
 
218
198
  ### Returns true if the Manager is running.
219
199
  def running?
220
- return self.zmq_loop && self.zmq_loop.running?
200
+ return self.reactor &&
201
+ self.event_socket &&
202
+ self.reactor.registered?( self.event_socket )
221
203
  end
222
204
 
223
205
 
224
206
  ### Register the Manager's timers.
225
207
  def register_timers
226
- self.zmq_loop.register_timer( self.heartbeat_timer )
227
- self.zmq_loop.register_timer( self.checkpoint_timer ) if self.checkpoint_timer
208
+ self.register_checkpoint_timer
209
+ self.register_heartbeat_timer
210
+ end
211
+
212
+
213
+ ### Register the Manager's timers.
214
+ def cancel_timers
215
+ self.cancel_heartbeat_timer
216
+ self.cancel_checkpoint_timer
228
217
  end
229
218
 
230
219
 
@@ -232,33 +221,13 @@ class Arborist::Manager
232
221
  def start_accepting_requests
233
222
  self.log.debug "Starting the main loop"
234
223
 
235
- self.setup_signal_timer
236
224
  self.start_time = Time.now
237
225
 
238
- self.log.debug "Manager running."
239
- return self.zmq_loop.start
240
- end
241
-
226
+ self.reactor.register( self.tree_socket, :read, &self.method(:on_tree_socket_event) )
227
+ self.reactor.register( self.event_socket, :write, &self.method(:on_event_socket_event) )
242
228
 
243
- ### Set up the ZMQ REP socket for the Tree API.
244
- def setup_tree_socket
245
- sock = Arborist.zmq_context.socket( :REP )
246
- self.log.debug " binding the tree API socket (%#0x) to %p" %
247
- [ sock.object_id * 2, Arborist.tree_api_url ]
248
- sock.linger = 0
249
- sock.bind( Arborist.tree_api_url )
250
- return ZMQ::Pollitem.new( sock, ZMQ::POLLIN|ZMQ::POLLOUT )
251
- end
252
-
253
-
254
- ### Set up the ZMQ PUB socket for published events.
255
- def setup_event_socket
256
- sock = Arborist.zmq_context.socket( :PUB )
257
- self.log.debug " binding the event socket (%#0x) to %p" %
258
- [ sock.object_id * 2, Arborist.event_api_url ]
259
- sock.linger = self.linger
260
- sock.bind( Arborist.event_api_url )
261
- return ZMQ::Pollitem.new( sock, ZMQ::POLLOUT )
229
+ self.log.debug "Manager running."
230
+ return self.reactor.start_polling( ignore_interrupts: true )
262
231
  end
263
232
 
264
233
 
@@ -271,13 +240,7 @@ class Arborist::Manager
271
240
  ### Stop the manager.
272
241
  def stop
273
242
  self.log.info "Stopping the manager."
274
- self.ignore_signals
275
- self.cancel_signal_timer
276
-
277
- @api_handler.shutdown
278
- @event_publisher.shutdown
279
-
280
- self.zmq_loop.stop
243
+ self.reactor.stop_polling
281
244
  end
282
245
 
283
246
 
@@ -333,90 +296,60 @@ class Arborist::Manager
333
296
  end
334
297
 
335
298
 
336
- ### Make a ZMQ::Timer that will publish a heartbeat event at a configurable interval.
337
- def make_heartbeat_timer
338
- interval = self.class.heartbeat_frequency || CONFIG_DEFAULTS[ :heartbeat_frequency ]
299
+ ### Register a periodic timer that will publish a heartbeat event at a
300
+ ### configurable interval.
301
+ def register_heartbeat_timer
302
+ interval = self.class.heartbeat_frequency
339
303
 
340
- self.log.info "Setting up to heartbeat every %dms" % [ interval ]
341
- heartbeat_timer = ZMQ::Timer.new( (interval/1000.0), 0 ) do
304
+ self.log.info "Setting up to heartbeat every %ds" % [ interval ]
305
+ @heartbeat_timer = self.reactor.add_periodic_timer( interval ) do
342
306
  self.publish_heartbeat_event
343
307
  end
344
- return heartbeat_timer
345
308
  end
346
309
 
347
310
 
348
- ### Make a ZMQ::Timer that will save a snapshot of the node tree's state to the state
349
- ### file on a configured interval if it's configured.
350
- def make_checkpoint_timer
351
- return nil unless self.class.state_file
352
- interval = self.class.checkpoint_frequency or return nil
353
-
354
- self.log.info "Setting up node state checkpoint every %dms" % [ interval ]
355
- checkpoint_timer = ZMQ::Timer.new( (interval/1000.0), 0 ) do
356
- self.save_node_states
357
- end
358
- return checkpoint_timer
311
+ ### Cancel the timer that publishes heartbeat events.
312
+ def cancel_heartbeat_timer
313
+ self.reactor.remove_timer( self.heartbeat_timer )
359
314
  end
360
315
 
361
316
 
362
- #
363
- # :section: Signal Handling
364
- # These methods set up some behavior for starting, restarting, and stopping
365
- # your application when a signal is received. If you don't want signals to
366
- # be handled, override #set_signal_handlers with an empty method.
367
- #
368
-
369
- ### Set up a periodic ZMQ timer to check for queued signals and handle them.
370
- def setup_signal_timer
371
- @signal_timer = ZMQ::Timer.new( SIGNAL_INTERVAL, 0, self.method(:process_signal_queue) )
372
- self.zmq_loop.register_timer( @signal_timer )
317
+ ### Resume the timer that publishes heartbeat events.
318
+ def resume_heartbeat_timer
319
+ self.reactor.resume_timer( self.heartbeat_timer )
373
320
  end
374
321
 
375
322
 
376
- ### Disable the timer that checks for incoming signals
377
- def cancel_signal_timer
378
- if self.signal_timer
379
- self.signal_timer.cancel
380
- self.zmq_loop.cancel_timer( self.signal_timer )
381
- end
382
- end
383
-
323
+ ### Register a periodic timer that will save a snapshot of the node tree's state to the state
324
+ ### file on a configured interval if one is configured.
325
+ def register_checkpoint_timer
326
+ return nil unless self.class.state_file
327
+ interval = self.class.checkpoint_frequency or return nil
384
328
 
385
- ### Set up signal handlers for common signals that will shut down, restart, etc.
386
- def set_signal_handlers
387
- self.log.debug "Setting up deferred signal handlers."
388
- QUEUE_SIGS.each do |sig|
389
- Signal.trap( sig ) { Thread.main[:signal_queue] << sig }
329
+ self.log.info "Setting up node state checkpoint every %0.3fs" % [ interval ]
330
+ @checkpoint_timer = self.reactor.add_periodic_timer( interval ) do
331
+ self.save_node_states
390
332
  end
391
333
  end
392
334
 
393
335
 
394
- ### Set all signal handlers to ignore.
395
- def ignore_signals
396
- self.log.debug "Ignoring signals."
397
- QUEUE_SIGS.each do |sig|
398
- Signal.trap( sig, :IGNORE )
399
- end
336
+ ### Cancel the timer that saves tree snapshots.
337
+ def cancel_checkpoint_timer
338
+ self.reactor.remove_timer( self.checkpoint_timer )
400
339
  end
401
340
 
402
341
 
403
- ### Set the signal handlers back to their defaults.
404
- def restore_signal_handlers
405
- self.log.debug "Restoring default signal handlers."
406
- QUEUE_SIGS.each do |sig|
407
- Signal.trap( sig, :DEFAULT )
408
- end
342
+ ### Resume the timer that saves tree snapshots.
343
+ def resume_checkpoint_timer
344
+ self.reactor.resume_timer( self.checkpoint_timer )
409
345
  end
410
346
 
411
347
 
412
- ### Handle any queued signals.
413
- def process_signal_queue
414
- # Look for any signals that arrived and handle them
415
- while sig = Thread.main[:signal_queue].shift
416
- self.handle_signal( sig )
417
- end
418
- end
419
-
348
+ #
349
+ # :section: Signal Handling
350
+ # These methods set up some behavior for starting, restarting, and stopping
351
+ # the manager when a signal is received.
352
+ #
420
353
 
421
354
  ### Handle signals.
422
355
  def handle_signal( sig )
@@ -461,12 +394,6 @@ class Arborist::Manager
461
394
  end
462
395
 
463
396
 
464
- ### Simulate the receipt of the specified +signal+ (probably only useful
465
- ### in testing).
466
- def simulate_signal( signal )
467
- Thread.main[:signal_queue] << signal.to_sym
468
- end
469
-
470
397
 
471
398
  #
472
399
  # :section: Tree API
@@ -617,10 +544,243 @@ class Arborist::Manager
617
544
 
618
545
 
619
546
  #
620
- # Tree-traversal API
547
+ # Tree network API
621
548
  #
622
549
 
623
550
 
551
+ ### Set up the ZeroMQ REP socket for the Tree API.
552
+ def setup_tree_socket
553
+ @tree_socket = CZTop::Socket::REP.new
554
+ self.log.debug " binding the tree API socket (%#0x) to %p" %
555
+ [ @tree_socket.object_id * 2, Arborist.tree_api_url ]
556
+ @tree_socket.options.linger = 0
557
+ @tree_socket.bind( Arborist.tree_api_url )
558
+ end
559
+
560
+
561
+ ### Tear down the ZeroMQ REP socket.
562
+ def shutdown_tree_socket
563
+ @tree_socket.unbind( @tree_socket.last_endpoint )
564
+ @tree_socket = nil
565
+ end
566
+
567
+
568
+ ### ZMQ::Handler API -- Read and handle an incoming request.
569
+ def on_tree_socket_event( event )
570
+ if event.readable?
571
+ request = event.socket.receive
572
+ msg = self.handle_tree_request( request )
573
+ event.socket << msg
574
+ else
575
+ raise "Unsupported event %p on tree API socket!" % [ event ]
576
+ end
577
+ end
578
+
579
+
580
+ ### Handle the specified +raw_request+ and return a response.
581
+ def handle_tree_request( raw_request )
582
+ raise "Manager is shutting down" unless self.running?
583
+
584
+ header, body = Arborist::TreeAPI.decode( raw_request )
585
+ raise Arborist::MessageError, "missing required header 'action'" unless
586
+ header.key?( 'action' )
587
+ handler = self.lookup_tree_request_action( header ) or
588
+ raise Arborist::MessageError, "No such action '%s'" % [ header['action'] ]
589
+
590
+ return handler.call( header, body )
591
+
592
+ rescue => err
593
+ self.log.error "%p: %s" % [ err.class, err.message ]
594
+ err.backtrace.each {|frame| self.log.debug " #{frame}" }
595
+
596
+ errtype = case err
597
+ when Arborist::MessageError,
598
+ Arborist::ConfigError,
599
+ Arborist::NodeError
600
+ 'client'
601
+ else
602
+ 'server'
603
+ end
604
+
605
+ return Arborist::TreeAPI.error_response( errtype, err.message )
606
+ end
607
+
608
+
609
+ ### Given a request +header+, return a #call-able object that can handle the response.
610
+ def lookup_tree_request_action( header )
611
+ raise Arborist::MessageError, "unsupported version %d" % [ header['version'] ] unless
612
+ header['version'] == 1
613
+
614
+ handler_name = "handle_%s_request" % [ header['action'] ]
615
+ return nil unless self.respond_to?( handler_name )
616
+
617
+ return self.method( handler_name )
618
+ end
619
+
620
+
621
+ ### Return a response to the `status` action.
622
+ def handle_status_request( header, body )
623
+ self.log.debug "STATUS: %p" % [ header ]
624
+ return Arborist::TreeAPI.successful_response(
625
+ server_version: Arborist::VERSION,
626
+ state: self.running? ? 'running' : 'not running',
627
+ uptime: self.uptime,
628
+ nodecount: self.nodecount
629
+ )
630
+ end
631
+
632
+
633
+ ### Return a response to the `subscribe` action.
634
+ def handle_subscribe_request( header, body )
635
+ self.log.debug "SUBSCRIBE: %p" % [ header ]
636
+ event_type = header[ 'event_type' ]
637
+ node_identifier = header[ 'identifier' ]
638
+
639
+ body = [ body ] unless body.is_a?( Array )
640
+ positive = body.shift
641
+ negative = body.shift || {}
642
+
643
+ subscription = self.create_subscription( node_identifier, event_type, positive, negative )
644
+ self.log.info "Subscription to %s events at or under %s: %p" %
645
+ [ event_type, node_identifier || 'the root node', subscription ]
646
+
647
+ return Arborist::TreeAPI.successful_response( id: subscription.id )
648
+ end
649
+
650
+
651
+ ### Return a response to the `unsubscribe` action.
652
+ def handle_unsubscribe_request( header, body )
653
+ self.log.debug "UNSUBSCRIBE: %p" % [ header ]
654
+ subscription_id = header[ 'subscription_id' ] or
655
+ return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for UNSUBSCRIBE.' )
656
+ subscription = self.remove_subscription( subscription_id ) or
657
+ return Arborist::TreeAPI.successful_response( nil )
658
+
659
+ self.log.info "Destroyed subscription: %p" % [ subscription ]
660
+ return Arborist::TreeAPI.successful_response(
661
+ event_type: subscription.event_type,
662
+ criteria: subscription.criteria
663
+ )
664
+ end
665
+
666
+
667
+ ### Return a repsonse to the `list` action.
668
+ def handle_list_request( header, body )
669
+ self.log.debug "LIST: %p" % [ header ]
670
+ from = header['from'] || '_'
671
+ depth = header['depth']
672
+
673
+ start_node = self.nodes[ from ]
674
+ self.log.debug " Listing nodes under %p" % [ start_node ]
675
+ iter = if depth
676
+ self.log.debug " depth limited to %d" % [ depth ]
677
+ self.depth_limited_enumerator_for( start_node, depth )
678
+ else
679
+ self.log.debug " no depth limit"
680
+ self.enumerator_for( start_node )
681
+ end
682
+ data = iter.map( &:to_h )
683
+ self.log.debug " got data for %d nodes" % [ data.length ]
684
+
685
+ return Arborist::TreeAPI.successful_response( data )
686
+ end
687
+
688
+
689
+ ### Return a response to the 'fetch' action.
690
+ def handle_fetch_request( header, body )
691
+ self.log.debug "FETCH: %p" % [ header ]
692
+
693
+ include_down = header['include_down']
694
+ values = if header.key?( 'return' )
695
+ header['return'] || []
696
+ else
697
+ nil
698
+ end
699
+
700
+ body = [ body ] unless body.is_a?( Array )
701
+ positive = body.shift
702
+ negative = body.shift || {}
703
+ states = self.fetch_matching_node_states( positive, values, include_down, negative )
704
+
705
+ return Arborist::TreeAPI.successful_response( states )
706
+ end
707
+
708
+
709
+ ### Update nodes using the data from the update request's +body+.
710
+ def handle_update_request( header, body )
711
+ self.log.debug "UPDATE: %p" % [ header ]
712
+
713
+ unless body.respond_to?( :each )
714
+ return Arborist::TreeAPI.error_response( 'client', 'Malformed update: body does not respond to #each' )
715
+ end
716
+
717
+ body.each do |identifier, properties|
718
+ self.update_node( identifier, properties )
719
+ end
720
+
721
+ return Arborist::TreeAPI.successful_response( nil )
722
+ end
723
+
724
+
725
+ ### Remove a node and its children.
726
+ def handle_prune_request( header, body )
727
+ self.log.debug "PRUNE: %p" % [ header ]
728
+
729
+ identifier = header[ 'identifier' ] or
730
+ return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for PRUNE.' )
731
+ node = self.remove_node( identifier )
732
+
733
+ return Arborist::TreeAPI.successful_response( node ? node.to_h : nil )
734
+ end
735
+
736
+
737
+ ### Add a node
738
+ def handle_graft_request( header, body )
739
+ self.log.debug "GRAFT: %p" % [ header ]
740
+
741
+ identifier = header[ 'identifier' ] or
742
+ return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for GRAFT.' )
743
+ type = header[ 'type' ] or
744
+ return Arborist::TreeAPI.error_response( 'client', 'No type specified for GRAFT.' )
745
+ parent = header[ 'parent' ] || '_'
746
+ parent_node = self.nodes[ parent ] or
747
+ return Arborist::TreeAPI.error_response( 'client', 'No parent node found for %s.' % [parent] )
748
+
749
+ self.log.debug "Grafting a new %s node under %p" % [ type, parent_node ]
750
+
751
+ # If the parent has a factory method for the node type, use it, otherwise
752
+ # use the Pluggability factory
753
+ node = if parent_node.respond_to?( type )
754
+ parent_node.method( type ).call( identifier, body )
755
+ else
756
+ body.merge!( parent: parent )
757
+ Arborist::Node.create( type, identifier, body )
758
+ end
759
+
760
+ self.add_node( node )
761
+
762
+ return Arborist::TreeAPI.successful_response( node ? {identifier: node.identifier} : nil )
763
+ end
764
+
765
+
766
+ ### Modify a node's operational attributes
767
+ def handle_modify_request( header, body )
768
+ self.log.debug "MODIFY: %p" % [ header ]
769
+
770
+ identifier = header[ 'identifier' ] or
771
+ return Arborist::TreeAPI.error_response( 'client', 'No identifier specified for MODIFY.' )
772
+ return Arborist::TreeAPI.error_response( 'client', "Unable to MODIFY root node." ) if identifier == '_'
773
+ node = self.nodes[ identifier ] or
774
+ return Arborist::TreeAPI.error_response( 'client', "No such node %p" % [identifier] )
775
+
776
+ self.log.debug "Modifying operational attributes of the %s node: %p" % [ identifier, body ]
777
+
778
+ node.modify( body )
779
+
780
+ return Arborist::TreeAPI.successful_response( nil )
781
+ end
782
+
783
+
624
784
  ### Return the current root node.
625
785
  def root_node
626
786
  return self.nodes[ '_' ]
@@ -696,16 +856,66 @@ class Arborist::Manager
696
856
  # Event API
697
857
  #
698
858
 
699
- ### Add the specified +subscription+ to the node corresponding with the given +identifier+.
700
- def subscribe( identifier, subscription )
701
- identifier ||= '_'
702
- node = self.nodes[ identifier ] or raise ArgumentError, "no such node %p" % [ identifier ]
859
+ ### Set up the ZMQ PUB socket for published events.
860
+ def setup_event_socket
861
+ @event_socket = CZTop::Socket::PUB.new
862
+ self.log.debug " binding the event socket (%#0x) to %p" %
863
+ [ @event_socket.object_id * 2, Arborist.event_api_url ]
864
+ @event_socket.options.linger = ( self.linger * 1000 ).ceil
865
+ @event_socket.bind( Arborist.event_api_url )
866
+ end
703
867
 
704
- self.log.debug "Registering subscription %p" % [ subscription ]
705
- node.add_subscription( subscription )
706
- self.log.debug " adding '%s' to the subscriptions hash." % [ subscription.id ]
707
- self.subscriptions[ subscription.id ] = node
708
- self.log.debug " subscriptions hash: %#0x" % [ self.subscriptions.object_id ]
868
+
869
+ ### Stop accepting events to be published
870
+ def shutdown_event_socket
871
+ start = Time.now
872
+ timeout = start + (self.linger.to_f / 2.0)
873
+
874
+ self.log.warn "Waiting to empty the event queue..."
875
+ until self.event_queue.empty?
876
+ sleep 0.1
877
+ break if Time.now > timeout
878
+ end
879
+ self.log.warn " ... waited %0.1f seconds" % [ Time.now - start ]
880
+
881
+ @event_socket.options.linger = 0
882
+ @event_socket.unbind( @event_socket.last_endpoint )
883
+ @event_socket = nil
884
+ end
885
+
886
+
887
+ ### Publish the specified +event+.
888
+ def publish( identifier, event )
889
+ self.event_queue << Arborist::EventAPI.encode( identifier, event.to_h )
890
+ self.register_event_socket if self.running?
891
+ end
892
+
893
+
894
+ ### Register the publisher with the reactor if it's not already.
895
+ def register_event_socket
896
+ self.reactor.enable_events( self.event_socket, :write ) unless
897
+ self.reactor.event_enabled?( self.event_socket, :write )
898
+ end
899
+
900
+
901
+ ### Unregister the event publisher socket from the reactor if it's registered.
902
+ def unregister_event_socket
903
+ self.reactor.disable_events( self.event_socket, :write ) if
904
+ self.reactor.event_enabled?( self.event_socket, :write )
905
+ end
906
+
907
+
908
+ ### IO event handler for the event socket.
909
+ def on_event_socket_event( event )
910
+ if event.writable?
911
+ if (( msg = self.event_queue.shift ))
912
+ event.socket << msg
913
+ end
914
+ else
915
+ raise "Unhandled event %p on the event socket" % [ event ]
916
+ end
917
+
918
+ self.unregister_event_socket if self.event_queue.empty?
709
919
  end
710
920
 
711
921
 
@@ -720,7 +930,20 @@ class Arborist::Manager
720
930
  eventname = eventname.to_s
721
931
  eventname = 'sys.' + eventname unless eventname.start_with?( 'sys.' )
722
932
  self.log.debug "Publishing %s event: %p." % [ eventname, data ]
723
- self.event_publisher.publish( eventname, data )
933
+ self.publish( eventname, data )
934
+ end
935
+
936
+
937
+ ### Add the specified +subscription+ to the node corresponding with the given +identifier+.
938
+ def subscribe( identifier, subscription )
939
+ identifier ||= '_'
940
+ node = self.nodes[ identifier ] or raise ArgumentError, "no such node %p" % [ identifier ]
941
+
942
+ self.log.debug "Registering subscription %p" % [ subscription ]
943
+ node.add_subscription( subscription )
944
+ self.log.debug " adding '%s' to the subscriptions hash." % [ subscription.id ]
945
+ self.subscriptions[ subscription.id ] = node
946
+ self.log.debug " subscriptions hash: %#0x" % [ self.subscriptions.object_id ]
724
947
  end
725
948
 
726
949
 
@@ -729,7 +952,7 @@ class Arborist::Manager
729
952
  ### given +criteria+ when considering an event.
730
953
  def create_subscription( identifier, event_pattern, criteria, negative_criteria={} )
731
954
  sub = Arborist::Subscription.new( event_pattern, criteria, negative_criteria ) do |*args|
732
- self.event_publisher.publish( *args )
955
+ self.publish( *args )
733
956
  end
734
957
  self.subscribe( identifier, sub )
735
958
 
@@ -763,7 +986,4 @@ class Arborist::Manager
763
986
  end
764
987
 
765
988
 
766
- require 'arborist/manager/tree_api'
767
- require 'arborist/manager/event_publisher'
768
-
769
989
  end # class Arborist::Manager