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