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.
- checksums.yaml +5 -5
- data/ChangeLog +46 -2
- data/Manifest.txt +4 -4
- data/Rakefile +6 -4
- data/TODO.md +16 -0
- data/lib/arborist.rb +12 -51
- data/lib/arborist/client.rb +23 -46
- data/lib/arborist/command/client.rb +1 -0
- data/lib/arborist/command/watch.rb +4 -5
- data/lib/arborist/event_api.rb +35 -0
- data/lib/arborist/exceptions.rb +2 -2
- data/lib/arborist/manager.rb +432 -212
- data/lib/arborist/mixins.rb +9 -9
- data/lib/arborist/monitor_runner.rb +174 -137
- data/lib/arborist/node.rb +11 -4
- data/lib/arborist/observer/summarize.rb +1 -1
- data/lib/arborist/observer_runner.rb +163 -126
- data/lib/arborist/tree_api.rb +113 -0
- data/spec/arborist/client_spec.rb +63 -64
- data/spec/arborist/event_api_spec.rb +23 -0
- data/spec/arborist/manager_spec.rb +842 -66
- data/spec/arborist/monitor_runner_spec.rb +45 -85
- data/spec/arborist/observer_runner_spec.rb +86 -23
- data/spec/arborist/tree_api_spec.rb +30 -0
- data/spec/arborist_spec.rb +0 -5
- data/spec/spec_helper.rb +0 -13
- metadata +47 -45
- checksums.yaml.gz.sig +0 -3
- data.tar.gz.sig +0 -0
- data/lib/arborist/manager/event_publisher.rb +0 -126
- data/lib/arborist/manager/tree_api.rb +0 -302
- data/spec/arborist/manager/event_publisher_spec.rb +0 -65
- data/spec/arborist/manager/tree_api_spec.rb +0 -791
- metadata.gz.sig +0 -0
@@ -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
|
-
|
34
|
-
|
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
|
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
|
+
|
data/lib/arborist/exceptions.rb
CHANGED
@@ -7,10 +7,10 @@ module Arborist
|
|
7
7
|
|
8
8
|
class ClientError < RuntimeError; end
|
9
9
|
|
10
|
-
class
|
10
|
+
class MessageError < ClientError
|
11
11
|
|
12
12
|
def initialize( reason )
|
13
|
-
super( "Invalid
|
13
|
+
super( "Invalid message (#{reason})" )
|
14
14
|
end
|
15
15
|
|
16
16
|
end
|
data/lib/arborist/manager.rb
CHANGED
@@ -6,11 +6,15 @@ require 'pathname'
|
|
6
6
|
require 'tempfile'
|
7
7
|
require 'configurability'
|
8
8
|
require 'loggability'
|
9
|
-
require '
|
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
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
92
|
+
@linger = self.class.linger
|
107
93
|
self.log.info "Linger set to %p" % [ @linger ]
|
108
94
|
|
109
|
-
@
|
110
|
-
|
111
|
-
@
|
112
|
-
@
|
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
|
-
|
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
|
155
|
-
attr_reader :
|
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
|
159
|
-
|
138
|
+
# The ZeroMQ PUB socket that publishes events for the Event API
|
139
|
+
attr_accessor :event_socket
|
160
140
|
|
161
141
|
##
|
162
|
-
# The
|
163
|
-
attr_reader :
|
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
|
176
|
-
|
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
|
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.
|
197
|
-
|
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
|
-
|
214
|
-
|
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.
|
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.
|
227
|
-
self.
|
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.
|
239
|
-
|
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
|
-
|
244
|
-
|
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.
|
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
|
-
###
|
337
|
-
|
338
|
-
|
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 %
|
341
|
-
heartbeat_timer =
|
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
|
-
###
|
349
|
-
|
350
|
-
|
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
|
-
|
364
|
-
|
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
|
-
###
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
-
|
386
|
-
|
387
|
-
|
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
|
-
###
|
395
|
-
def
|
396
|
-
self.
|
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
|
-
###
|
404
|
-
def
|
405
|
-
self.
|
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
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
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
|
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
|
-
###
|
700
|
-
def
|
701
|
-
|
702
|
-
|
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
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
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.
|
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.
|
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
|