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.
- 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
|