arborist 0.1.0 → 0.2.0.pre20170519125456

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