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.
@@ -66,7 +66,7 @@ class Arborist::Observer::Summarize
66
66
 
67
67
 
68
68
  ### Handle a timing event by calling the block with any events in the history.
69
- def on_timer
69
+ def on_timer( * )
70
70
  self.log.debug "Timer event: %d pending event/s" % [ self.event_history.size ]
71
71
  self.call_block unless self.event_history.empty?
72
72
  end
@@ -1,7 +1,9 @@
1
1
  # -*- ruby -*-
2
2
  #encoding: utf-8
3
3
 
4
- require 'rbczmq'
4
+ require 'cztop'
5
+ require 'cztop/reactor'
6
+ require 'cztop/reactor/signal_handling'
5
7
  require 'loggability'
6
8
 
7
9
  require 'arborist' unless defined?( Arborist )
@@ -9,119 +11,29 @@ require 'arborist/client'
9
11
  require 'arborist/observer'
10
12
 
11
13
 
12
- # Undo the useless scoping
13
- class ZMQ::Loop
14
- public_class_method :instance
15
- end
16
-
17
-
18
14
  # An event-driven runner for Arborist::Observers.
19
15
  class Arborist::ObserverRunner
20
16
  extend Loggability
17
+ include CZTop::Reactor::SignalHandling
21
18
 
22
- log_to :arborist
23
-
24
-
25
- # A ZMQ::Handler object for managing IO for all running observers.
26
- class Handler < ZMQ::Handler
27
- extend Loggability,
28
- Arborist::MethodUtilities
29
-
30
- log_to :arborist
31
-
32
- ### Create a ZMQ::Handler that acts as the agent that runs the specified
33
- ### +observer+.
34
- def initialize( runner, reactor )
35
- @runner = runner
36
- @client = Arborist::Client.new
37
- @pollitem = ZMQ::Pollitem.new( @client.event_api, ZMQ::POLLIN )
38
- @pollitem.handler = self
39
- @subscriptions = {}
40
-
41
- reactor.register( @pollitem )
42
- end
43
-
44
-
45
- ######
46
- public
47
- ######
48
-
49
- # The Arborist::ObserverRunner that owns this handler.
50
- attr_reader :runner
51
-
52
- # The Arborist::Client that will be used for creating and tearing down subscriptions
53
- attr_reader :client
54
-
55
- # The map of subscription IDs to the Observer which it was created for.
56
- attr_reader :subscriptions
57
-
58
-
59
- ### Unsubscribe from and clear all current subscriptions.
60
- def reset
61
- self.log.warn "Resetting the observer handler."
62
- self.subscriptions.keys.each do |subid|
63
- self.client.event_api.unsubscribe( subid )
64
- end
65
- self.subscriptions.clear
66
- end
67
-
68
-
69
- ### Add the specified +observer+ and subscribe to the events it wishes to receive.
70
- def add_observer( observer )
71
- self.log.info "Adding observer: %s" % [ observer.description ]
72
- observer.subscriptions.each do |sub|
73
- subid = self.client.subscribe( sub )
74
- self.subscriptions[ subid ] = observer
75
- self.client.event_api.subscribe( subid )
76
- self.log.debug " subscribed to %p with subscription %s" % [ sub, subid ]
77
- end
78
- end
79
-
80
-
81
- ### Remove the specified +observer+ after unsubscribing from its events.
82
- def remove_observer( observer )
83
- self.log.info "Removing observer: %s" % [ observer.description ]
84
-
85
- self.subscriptions.keys.each do |subid|
86
- next unless self.subscriptions[ subid ] == observer
87
-
88
- self.client.unsubscribe( subid )
89
- self.subscriptions.delete( subid )
90
- self.client.event_api.unsubscribe( subid )
91
- self.log.debug " unsubscribed from %p" % [ subid ]
92
- end
93
- end
94
19
 
20
+ # Signals the observer runner responds to
21
+ QUEUE_SIGS = [
22
+ :INT, :TERM, :HUP,
23
+ # :TODO: :QUIT, :WINCH, :USR1, :USR2, :TTIN, :TTOU
24
+ ] & Signal.list.keys.map( &:to_sym )
95
25
 
96
- ### Read events from the event socket when it becomes readable, and dispatch them to
97
- ### the correct observer.
98
- def on_readable
99
- subid = self.recv
100
- raise "Partial write?!" unless self.pollitem.pollable.rcvmore?
101
- raw_event = self.recv
102
- event = MessagePack.unpack( raw_event )
103
26
 
104
- if (( observer = self.subscriptions[subid] ))
105
- observer.handle_event( subid, event )
106
- elsif subid.start_with?( 'sys.' )
107
- self.log.debug "System event! %p" % [ event ]
108
- self.runner.handle_system_event( subid, event )
109
- else
110
- self.log.warn "Ignoring event %p for which we have no observer." % [ subid ]
111
- end
112
-
113
- return true
114
- end
115
-
116
- end # class Handler
27
+ log_to :arborist
117
28
 
118
29
 
119
30
  ### Create a new Arborist::ObserverRunner
120
31
  def initialize
121
- @observers = []
122
- @timers = []
123
- @handler = nil
124
- @reactor = ZMQ::Loop.new
32
+ @observers = []
33
+ @timers = []
34
+ @subscriptions = {}
35
+ @reactor = CZTop::Reactor.new
36
+ @client = Arborist::Client.new
125
37
  @manager_last_runid = nil
126
38
  end
127
39
 
@@ -136,49 +48,69 @@ class Arborist::ObserverRunner
136
48
  # The Array of registered ZMQ::Timers
137
49
  attr_reader :timers
138
50
 
139
- # The ZMQ::Handler subclass that handles all async IO
140
- attr_accessor :handler
141
-
142
- # The reactor (a ZMQ::Loop) the runner uses to drive everything
51
+ # The reactor (a CZTop::Reactor) the runner uses to drive everything
143
52
  attr_accessor :reactor
144
53
 
54
+ # The Arborist::Client that will be used for creating and tearing down subscriptions
55
+ attr_reader :client
56
+
57
+ # The map of subscription IDs to the Observer which it was created for.
58
+ attr_reader :subscriptions
59
+
145
60
 
146
61
  ### Load observers from the specified +enumerator+.
147
62
  def load_observers( enumerator )
148
- @observers += enumerator.to_a
63
+ self.observers.concat( enumerator.to_a )
149
64
  end
150
65
 
151
66
 
152
67
  ### Run the specified +observers+
153
68
  def run
154
- self.handler = Arborist::ObserverRunner::Handler.new( self, self.reactor )
155
-
69
+ self.log.info "Starting!"
156
70
  self.register_observers
157
71
  self.register_observer_timers
158
72
  self.subscribe_to_system_events
159
73
 
160
- self.reactor.start
161
- rescue Interrupt
162
- $stderr.puts "Interrupted!"
163
- self.stop
74
+ self.reactor.register( self.client.event_api, :read, &self.method(:on_subscription_event) )
75
+
76
+ self.with_signal_handler( self.reactor, *QUEUE_SIGS ) do
77
+ self.reactor.start_polling( ignore_interrupts: true )
78
+ end
164
79
  end
165
80
 
166
81
 
167
82
  ### Stop the observer
168
83
  def stop
169
- self.observers.each do |observer|
170
- self.remove_timers
171
- self.handler.remove_observer( observer )
172
- end
84
+ self.log.info "Stopping!"
85
+ self.remove_timers
86
+ self.unregister_observers
87
+ self.reactor.stop_polling
88
+ end
89
+
90
+
91
+ ### Restart the observer, resetting all of its observers' subscriptions.
92
+ def restart
93
+ self.log.info "Restarting!"
94
+ self.reactor.timers.pause
95
+ self.unregister_observers
96
+
97
+ self.register_observers
98
+ self.reactor.timers.resume
99
+ end
173
100
 
174
- self.reactor.stop
101
+
102
+ ### Returns true if the ObserverRunner is running.
103
+ def running?
104
+ return self.reactor &&
105
+ self.client &&
106
+ self.reactor.registered?( self.client.event_api )
175
107
  end
176
108
 
177
109
 
178
- ### Register each of the runner's Observers with its handler.
110
+ ### Add subscriptions for all of the observers loaded into the runner.
179
111
  def register_observers
180
112
  self.observers.each do |observer|
181
- self.handler.add_observer( observer )
113
+ self.add_observer( observer )
182
114
  end
183
115
  end
184
116
 
@@ -191,9 +123,17 @@ class Arborist::ObserverRunner
191
123
  end
192
124
 
193
125
 
126
+ ### Remove the subscriptions belonging to the loaded observers.
127
+ def unregister_observers
128
+ self.observers.each do |observer|
129
+ self.remove_observer( observer )
130
+ end
131
+ end
132
+
133
+
194
134
  ### Subscribe the runner to system events published by the Manager.
195
135
  def subscribe_to_system_events
196
- self.handler.client.event_api.subscribe( 'sys.' )
136
+ self.client.event_api.subscribe( 'sys.' )
197
137
  end
198
138
 
199
139
 
@@ -202,8 +142,7 @@ class Arborist::ObserverRunner
202
142
  observer.timers.each do |interval, callback|
203
143
  self.log.info "Creating timer for %s observer to run %p every %ds" %
204
144
  [ observer.description, callback, interval ]
205
- timer = ZMQ::Timer.new( interval, 0, &callback )
206
- self.reactor.register_timer( timer )
145
+ timer = self.reactor.add_periodic_timer( interval, &callback )
207
146
  self.timers << timer
208
147
  end
209
148
  end
@@ -212,7 +151,65 @@ class Arborist::ObserverRunner
212
151
  ### Remove any registered timers.
213
152
  def remove_timers
214
153
  self.timers.each do |timer|
215
- self.reactor.cancel_timer( timer )
154
+ self.reactor.remove_timer( timer )
155
+ end
156
+ end
157
+
158
+
159
+ ### Unsubscribe from and clear all current subscriptions.
160
+ def reset
161
+ self.log.warn "Resetting observer subscriptions."
162
+ self.subscriptions.keys.each do |subid|
163
+ self.client.event_api.unsubscribe( subid )
164
+ end
165
+ self.subscriptions.clear
166
+ end
167
+
168
+
169
+ ### Add the specified +observer+ and subscribe to the events it wishes to receive.
170
+ def add_observer( observer )
171
+ self.log.info "Adding observer: %s" % [ observer.description ]
172
+ observer.subscriptions.each do |sub|
173
+ subid = self.client.subscribe( sub )
174
+ self.subscriptions[ subid ] = observer
175
+ self.client.event_api.subscribe( subid )
176
+ self.log.debug " subscribed to %p with subscription %s" % [ sub, subid ]
177
+ end
178
+ end
179
+
180
+
181
+ ### Remove the specified +observer+ after unsubscribing from its events.
182
+ def remove_observer( observer )
183
+ self.log.info "Removing observer: %s" % [ observer.description ]
184
+
185
+ self.subscriptions.keys.each do |subid|
186
+ next unless self.subscriptions[ subid ] == observer
187
+
188
+ self.client.unsubscribe( subid )
189
+ self.subscriptions.delete( subid )
190
+ self.client.event_api.unsubscribe( subid )
191
+ self.log.debug " unsubscribed from %p" % [ subid ]
192
+ end
193
+ end
194
+
195
+
196
+ ### Handle IO events from the reactor.
197
+ def on_subscription_event( event )
198
+ if event.readable?
199
+ msg = event.socket.receive
200
+ subid, event = Arborist::EventAPI.decode( msg )
201
+
202
+ if (( observer = self.subscriptions[subid] ))
203
+ self.log.debug "Got %p event for %p" % [ subid, observer ]
204
+ observer.handle_event( subid, event )
205
+ elsif subid.start_with?( 'sys.' )
206
+ self.log.debug "System event! %p" % [ event ]
207
+ self.handle_system_event( subid, event )
208
+ else
209
+ self.log.warn "Ignoring event %p for which we have no observer." % [ subid ]
210
+ end
211
+ else
212
+ raise "Unhandled event %p on the event socket" % [ event ]
216
213
  end
217
214
  end
218
215
 
@@ -226,7 +223,7 @@ class Arborist::ObserverRunner
226
223
  this_runid = event['run_id']
227
224
  if @manager_last_runid && this_runid != @manager_last_runid
228
225
  self.log.warn "Manager run ID changed: re-subscribing"
229
- self.handler.reset
226
+ self.reset
230
227
  self.register_observers
231
228
  end
232
229
 
@@ -238,5 +235,45 @@ class Arborist::ObserverRunner
238
235
  end
239
236
  end
240
237
 
238
+
239
+ #
240
+ # :section: Signal Handling
241
+ # These methods set up some behavior for starting, restarting, and stopping
242
+ # the runner when a signal is received.
243
+ #
244
+
245
+ ### Handle signals.
246
+ def handle_signal( sig )
247
+ self.log.debug "Handling signal %s" % [ sig ]
248
+ case sig
249
+ when :INT, :TERM
250
+ self.on_termination_signal( sig )
251
+
252
+ when :HUP
253
+ self.on_hangup_signal( sig )
254
+
255
+ else
256
+ self.log.warn "Unhandled signal %s" % [ sig ]
257
+ end
258
+
259
+ end
260
+
261
+
262
+ ### Handle a TERM signal. Shuts the handler down after handling any current request/s. Also
263
+ ### aliased to #on_interrupt_signal.
264
+ def on_termination_signal( signo )
265
+ self.log.warn "Terminated (%p)" % [ signo ]
266
+ self.stop
267
+ end
268
+ alias_method :on_interrupt_signal, :on_termination_signal
269
+
270
+
271
+ ### Handle a HUP signal. The default is to restart the handler.
272
+ def on_hangup_signal( signo )
273
+ self.log.warn "Hangup (%p)" % [ signo ]
274
+ self.restart
275
+ end
276
+
277
+
241
278
  end # class Arborist::ObserverRunner
242
279
 
@@ -0,0 +1,113 @@
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::TreeAPI
11
+ extend Loggability,
12
+ Arborist::MethodUtilities,
13
+ Arborist::HashUtilities
14
+
15
+ # The version of the application protocol
16
+ PROTOCOL_VERSION = 1
17
+
18
+
19
+ # Loggability API -- log to the arborist logger
20
+ log_to :arborist
21
+
22
+
23
+
24
+ ### Return a CZTop::Message with a payload containing the specified +header+ and +body+.
25
+ def self::encode( header, body=nil )
26
+ header = stringify_keys( header )
27
+ header['version'] = PROTOCOL_VERSION
28
+
29
+ self.check_header( header )
30
+ self.check_body( body )
31
+
32
+ payload = MessagePack.pack([ header, body ])
33
+
34
+ return CZTop::Message.new( payload )
35
+ end
36
+
37
+
38
+ ### Return the header and body from the TreeAPI request or response in the specified +msg+
39
+ ### (a CZTop::Message).
40
+ def self::decode( msg )
41
+ raw_message = msg.pop or raise Arborist::MessageError, "empty message"
42
+
43
+ parts = begin
44
+ MessagePack.unpack( raw_message )
45
+ rescue => err
46
+ raise Arborist::MessageError, err.message
47
+ end
48
+
49
+ raise Arborist::MessageError, 'not an Array' unless parts.is_a?( Array )
50
+ raise Arborist::MessageError,
51
+ "malformed message: expected 1-2 parts, got %d" % [ parts.length ] unless
52
+ parts.length.between?( 1, 2 )
53
+
54
+ header = parts.shift or
55
+ raise Arborist::MessageError, "no header"
56
+ self.check_header( header )
57
+
58
+ body = parts.shift
59
+ self.check_body( body )
60
+
61
+ return header, body
62
+ end
63
+
64
+
65
+ ### Return a CZTop::Message containing a TreeAPI request with the specified
66
+ ### +verb+ and +data+.
67
+ def self::request( verb, *data )
68
+ header = data.shift || {}
69
+ body = data.shift
70
+
71
+ header.merge!( action: verb )
72
+
73
+ return self.encode( header, body )
74
+ end
75
+
76
+
77
+ ### Build an error response message for the specified +category+ and +reason+.
78
+ def self::error_response( category, reason )
79
+ return self.encode({ category: category, reason: reason, success: false })
80
+ end
81
+
82
+
83
+ ### Build a successful response with the specified +body+.
84
+ def self::successful_response( body )
85
+ return self.encode({ success: true }, body )
86
+ end
87
+
88
+
89
+ ### Check the given +header+ for validity, raising an Arborist::MessageError if
90
+ ### it isn't.
91
+ def self::check_header( header )
92
+ raise Arborist::MessageError, "header is not a Map" unless
93
+ header.is_a?( Hash )
94
+ version = header['version'] or
95
+ raise Arborist::MessageError, "missing required header 'version'"
96
+ raise Arborist::MessageError, "unknown protocol version %p" % [version] unless
97
+ version == PROTOCOL_VERSION
98
+ end
99
+
100
+
101
+ ### Check the given +body+ for validity, raising an Arborist::MessageError if it
102
+ ### isn't.
103
+ def self::check_body( body )
104
+ unless body.is_a?( Hash ) ||
105
+ body.nil? ||
106
+ ( body.is_a?(Array) && body.all? {|obj| obj.is_a?(Hash) } )
107
+ self.log.error "Invalid message body: %p" % [ body]
108
+ raise Arborist::MessageError, "body must be Nil, a Map, or an Array of Maps"
109
+ end
110
+ end
111
+
112
+ end # class Arborist::TreeAPI
113
+