symphony 0.3.0.pre20140327204419
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.simplecov +9 -0
- data/ChangeLog +508 -0
- data/History.rdoc +15 -0
- data/Manifest.txt +30 -0
- data/README.rdoc +89 -0
- data/Rakefile +77 -0
- data/TODO.md +5 -0
- data/USAGE.rdoc +381 -0
- data/bin/symphony +8 -0
- data/bin/symphony-task +10 -0
- data/etc/config.yml.example +9 -0
- data/lib/symphony/daemon.rb +372 -0
- data/lib/symphony/metrics.rb +84 -0
- data/lib/symphony/mixins.rb +75 -0
- data/lib/symphony/queue.rb +313 -0
- data/lib/symphony/routing.rb +98 -0
- data/lib/symphony/signal_handling.rb +107 -0
- data/lib/symphony/task.rb +407 -0
- data/lib/symphony/tasks/auditor.rb +51 -0
- data/lib/symphony/tasks/failure_logger.rb +106 -0
- data/lib/symphony/tasks/pinger.rb +64 -0
- data/lib/symphony/tasks/simulator.rb +57 -0
- data/lib/symphony/tasks/ssh.rb +126 -0
- data/lib/symphony/tasks/sshscript.rb +168 -0
- data/lib/symphony.rb +56 -0
- data/spec/helpers.rb +36 -0
- data/spec/symphony/mixins_spec.rb +78 -0
- data/spec/symphony/queue_spec.rb +368 -0
- data/spec/symphony/task_spec.rb +147 -0
- data/spec/symphony_spec.rb +14 -0
- data.tar.gz.sig +0 -0
- metadata +332 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,313 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bunny'
|
4
|
+
require 'loggability'
|
5
|
+
require 'configurability'
|
6
|
+
|
7
|
+
require 'symphony' unless defined?( Symphony )
|
8
|
+
require 'symphony/mixins'
|
9
|
+
|
10
|
+
|
11
|
+
# An object class that encapsulates queueing logic for Symphony jobs.
|
12
|
+
class Symphony::Queue
|
13
|
+
extend Loggability,
|
14
|
+
Configurability,
|
15
|
+
Symphony::MethodUtilities
|
16
|
+
|
17
|
+
|
18
|
+
# Configurability defaults
|
19
|
+
CONFIG_DEFAULTS = {
|
20
|
+
broker_uri: nil,
|
21
|
+
exchange: 'symphony',
|
22
|
+
heartbeat: 'server',
|
23
|
+
}
|
24
|
+
|
25
|
+
# The default number of messages to prefetch
|
26
|
+
DEFAULT_PREFETCH = 10
|
27
|
+
|
28
|
+
|
29
|
+
# Loggability API -- set up symphony's logger
|
30
|
+
log_to :symphony
|
31
|
+
|
32
|
+
# Configurability API -- use the 'amqp' section of the config
|
33
|
+
config_key :amqp
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
##
|
38
|
+
# The URL of the AMQP broker to connect to
|
39
|
+
singleton_attr_accessor :broker_uri
|
40
|
+
|
41
|
+
##
|
42
|
+
# The name of the exchang to bind queues to.
|
43
|
+
singleton_attr_accessor :exchange
|
44
|
+
|
45
|
+
##
|
46
|
+
# The options to pass to Bunny when setting up the session
|
47
|
+
singleton_attr_accessor :session_opts
|
48
|
+
|
49
|
+
|
50
|
+
### Configurability API -- install the 'symphony' section of the config
|
51
|
+
### when it's loaded.
|
52
|
+
def self::configure( config=nil )
|
53
|
+
config = self.defaults.merge( config || {} )
|
54
|
+
|
55
|
+
self.broker_uri = config.delete( :broker_uri )
|
56
|
+
self.exchange = config.delete( :exchange )
|
57
|
+
self.session_opts = config
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
### Fetch a Hash of AMQP options.
|
62
|
+
def self::amqp_session_options
|
63
|
+
opts = self.session_opts.merge({
|
64
|
+
logger: Loggability[ Symphony ],
|
65
|
+
})
|
66
|
+
opts[:heartbeat] = opts[:heartbeat].to_sym if opts[:heartbeat].is_a?( String )
|
67
|
+
|
68
|
+
return opts
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
### Clear any created Bunny objects
|
73
|
+
def self::reset
|
74
|
+
@session = nil
|
75
|
+
self.amqp.clear
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
### Fetch the current session for AMQP connections.
|
80
|
+
def self::amqp_session
|
81
|
+
unless @session
|
82
|
+
options = self.amqp_session_options
|
83
|
+
if self.broker_uri
|
84
|
+
self.log.info "Using the broker URI-style config"
|
85
|
+
@session = Bunny.new( self.broker_uri, options )
|
86
|
+
else
|
87
|
+
self.log.info "Using the options hash-style config"
|
88
|
+
@session = Bunny.new( options )
|
89
|
+
end
|
90
|
+
end
|
91
|
+
return @session
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
### Fetch a Hash that stores per-thread AMQP objects.
|
96
|
+
def self::amqp
|
97
|
+
@symphony ||= {}
|
98
|
+
return @symphony
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
### Fetch the AMQP channel, creating one if necessary.
|
103
|
+
def self::amqp_channel
|
104
|
+
unless self.amqp[:channel]
|
105
|
+
self.log.debug "Creating a new AMQP channel"
|
106
|
+
self.amqp_session.start
|
107
|
+
channel = self.amqp_session.create_channel
|
108
|
+
self.amqp[:channel] = channel
|
109
|
+
end
|
110
|
+
return self.amqp[:channel]
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
### Close and remove the current AMQP channel, e.g., after an error.
|
115
|
+
def self::reset_amqp_channel
|
116
|
+
if self.amqp[:channel]
|
117
|
+
self.log.info "Resetting AMQP channel."
|
118
|
+
self.amqp[:channel].close if self.amqp[:channel].open?
|
119
|
+
self.amqp.delete( :channel )
|
120
|
+
end
|
121
|
+
|
122
|
+
return self.amqp_channel
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### Fetch the configured AMQP exchange interface object.
|
127
|
+
def self::amqp_exchange
|
128
|
+
unless self.amqp[:exchange]
|
129
|
+
self.amqp[:exchange] = self.amqp_channel.topic( self.exchange, passive: true )
|
130
|
+
end
|
131
|
+
return self.amqp[:exchange]
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
### Return a queue configured for the specified +task_class+.
|
136
|
+
def self::for_task( task_class )
|
137
|
+
args = [
|
138
|
+
task_class.queue_name,
|
139
|
+
task_class.acknowledge,
|
140
|
+
task_class.consumer_tag,
|
141
|
+
task_class.routing_keys,
|
142
|
+
task_class.prefetch
|
143
|
+
]
|
144
|
+
return new( *args )
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
### Create a new Queue with the specified configuration.
|
150
|
+
def initialize( name, acknowledge, consumer_tag, routing_keys, prefetch )
|
151
|
+
@name = name
|
152
|
+
@acknowledge = acknowledge
|
153
|
+
@consumer_tag = consumer_tag
|
154
|
+
@routing_keys = routing_keys
|
155
|
+
@prefetch = prefetch
|
156
|
+
|
157
|
+
@amqp_queue = nil
|
158
|
+
@shutting_down = false
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
######
|
163
|
+
public
|
164
|
+
######
|
165
|
+
|
166
|
+
# The name of the queue
|
167
|
+
attr_reader :name
|
168
|
+
|
169
|
+
# Acknowledge mode
|
170
|
+
attr_reader :acknowledge
|
171
|
+
|
172
|
+
# The tag to use when setting up consumer
|
173
|
+
attr_reader :consumer_tag
|
174
|
+
|
175
|
+
# The Array of routing keys to use when binding the queue to the exchange
|
176
|
+
attr_reader :routing_keys
|
177
|
+
|
178
|
+
# The maximum number of un-acked messages to prefetch
|
179
|
+
attr_reader :prefetch
|
180
|
+
|
181
|
+
# The Bunny::Consumer that is dispatching messages for the queue.
|
182
|
+
attr_accessor :consumer
|
183
|
+
|
184
|
+
##
|
185
|
+
# The flag for shutting the queue down.
|
186
|
+
attr_predicate_accessor :shutting_down
|
187
|
+
|
188
|
+
|
189
|
+
### The main work loop -- subscribe to the message queue and yield the payload and
|
190
|
+
### associated metadata when one is received.
|
191
|
+
def wait_for_message( only_one=false, &work_callback )
|
192
|
+
raise LocalJumpError, "no work_callback given" unless work_callback
|
193
|
+
session = self.class.amqp_session
|
194
|
+
|
195
|
+
self.shutting_down = only_one
|
196
|
+
amqp_queue = self.create_amqp_queue( only_one ? 1 : self.prefetch )
|
197
|
+
self.consumer = self.create_consumer( amqp_queue, &work_callback )
|
198
|
+
|
199
|
+
self.log.debug "Subscribing to queue with consumer: %p" % [ self.consumer ]
|
200
|
+
amqp_queue.subscribe_with( self.consumer, block: true )
|
201
|
+
amqp_queue.channel.close
|
202
|
+
session.close
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
### Create the Bunny::Consumer that will dispatch messages from the broker.
|
207
|
+
def create_consumer( amqp_queue, &work_callback )
|
208
|
+
ackmode = self.acknowledge
|
209
|
+
tag = self.consumer_tag
|
210
|
+
|
211
|
+
# Last argument is *no_ack*, so need to invert the logic
|
212
|
+
self.log.debug "Creating Bunny::Consumer for %p with tag: %s" % [ amqp_queue, tag ]
|
213
|
+
cons = Bunny::Consumer.new( amqp_queue.channel, amqp_queue, tag, !ackmode )
|
214
|
+
|
215
|
+
cons.on_delivery do |delivery_info, properties, payload|
|
216
|
+
rval = self.handle_message( delivery_info, properties, payload, &work_callback )
|
217
|
+
self.log.debug "Done with message %s. Session is %s" %
|
218
|
+
[ delivery_info.delivery_tag, self.class.amqp_session.closed? ? "closed" : "open" ]
|
219
|
+
cons.cancel if self.shutting_down?
|
220
|
+
end
|
221
|
+
|
222
|
+
cons.on_cancellation do
|
223
|
+
self.log.warn "Consumer cancelled."
|
224
|
+
self.shutdown
|
225
|
+
end
|
226
|
+
|
227
|
+
return cons
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
### Create the AMQP queue from the task class and bind it to the configured exchange.
|
232
|
+
def create_amqp_queue( prefetch_count=DEFAULT_PREFETCH )
|
233
|
+
exchange = self.class.amqp_exchange
|
234
|
+
channel = self.class.amqp_channel
|
235
|
+
|
236
|
+
begin
|
237
|
+
queue = channel.queue( self.name, passive: true )
|
238
|
+
channel.prefetch( prefetch_count )
|
239
|
+
self.log.info "Using pre-existing queue: %s" % [ self.name ]
|
240
|
+
return queue
|
241
|
+
rescue Bunny::NotFound => err
|
242
|
+
self.log.info "%s; using an auto-delete queue instead." % [ err.message ]
|
243
|
+
channel = self.class.reset_amqp_channel
|
244
|
+
channel.prefetch( prefetch_count )
|
245
|
+
|
246
|
+
queue = channel.queue( self.name, auto_delete: true )
|
247
|
+
self.routing_keys.each do |key|
|
248
|
+
self.log.info " binding queue %s to the %s exchange with topic key: %s" %
|
249
|
+
[ self.name, exchange.name, key ]
|
250
|
+
queue.bind( exchange, routing_key: key )
|
251
|
+
end
|
252
|
+
|
253
|
+
return queue
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
### Handle each subscribed message.
|
259
|
+
def handle_message( delivery_info, properties, payload, &work_callback )
|
260
|
+
metadata = {
|
261
|
+
delivery_info: delivery_info,
|
262
|
+
properties: properties,
|
263
|
+
content_type: properties[:content_type],
|
264
|
+
}
|
265
|
+
rval = work_callback.call( payload, metadata )
|
266
|
+
return self.ack_message( delivery_info.delivery_tag, rval )
|
267
|
+
|
268
|
+
# Re-raise errors from AMQP
|
269
|
+
rescue Bunny::Exception => err
|
270
|
+
self.log.error "%p while handling a message: %s" % [ err.class, err.message ]
|
271
|
+
self.log.debug " " + err.backtrace.join( "\n " )
|
272
|
+
raise
|
273
|
+
|
274
|
+
rescue => err
|
275
|
+
self.log.error "%p while handling a message: %s" % [ err.class, err.message ]
|
276
|
+
self.log.debug " " + err.backtrace.join( "\n " )
|
277
|
+
return self.ack_message( delivery_info.delivery_tag, false, false )
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
### Signal a acknowledgement or rejection for a message.
|
282
|
+
def ack_message( tag, success, try_again=true )
|
283
|
+
return unless self.acknowledge
|
284
|
+
|
285
|
+
channel = self.consumer.channel
|
286
|
+
|
287
|
+
if success
|
288
|
+
self.log.debug "ACKing message %s" % [ tag ]
|
289
|
+
channel.acknowledge( tag )
|
290
|
+
else
|
291
|
+
self.log.debug "NACKing message %s %s retry" % [ tag, try_again ? 'with' : 'without' ]
|
292
|
+
channel.reject( tag, try_again )
|
293
|
+
end
|
294
|
+
|
295
|
+
return success
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
### Close the AMQP session associated with this queue.
|
300
|
+
def shutdown
|
301
|
+
self.shutting_down = true
|
302
|
+
self.consumer.cancel
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
### Forcefully halt the queue.
|
307
|
+
def halt
|
308
|
+
self.shutting_down = true
|
309
|
+
self.consumer.channel.close
|
310
|
+
end
|
311
|
+
|
312
|
+
end # class Symphony::Queue
|
313
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'loggability'
|
5
|
+
|
6
|
+
require 'symphony' unless defined?( Symphony )
|
7
|
+
require 'symphony/mixins'
|
8
|
+
|
9
|
+
|
10
|
+
# A mixin for adding handlers for multiple topic keys to a Task.
|
11
|
+
module Symphony::Routing
|
12
|
+
extend Loggability
|
13
|
+
log_to :symphony
|
14
|
+
|
15
|
+
### Add some instance data to inheriting +subclass+es.
|
16
|
+
def self::included( mod )
|
17
|
+
self.log.info "Adding routing to %p" % [ mod ]
|
18
|
+
super
|
19
|
+
mod.extend( Symphony::MethodUtilities )
|
20
|
+
mod.extend( Symphony::Routing::ClassMethods )
|
21
|
+
mod.singleton_attr_accessor( :routes )
|
22
|
+
mod.routes = Hash.new {|h,k| h[k] = [] }
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Methods to add to including classes
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
### Register an event pattern and a block to execute when an event
|
30
|
+
### matching that pattern is received.
|
31
|
+
def on( *route_patterns, &block )
|
32
|
+
route_patterns.each do |pattern|
|
33
|
+
methodobj = self.make_handler_method( pattern, &block )
|
34
|
+
self.routing_keys << pattern
|
35
|
+
|
36
|
+
pattern_re = self.make_routing_pattern( pattern )
|
37
|
+
self.routes[ pattern_re ] << methodobj
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
### Install the given +block+ as an instance method of the receiver, using
|
43
|
+
### the given +pattern+ to derive the name, and return it as an UnboundMethod
|
44
|
+
### object.
|
45
|
+
def make_handler_method( pattern, &block )
|
46
|
+
methname = self.make_handler_method_name( pattern, block )
|
47
|
+
self.log.info "Setting up #%s as a handler for %s" % [ methname, pattern ]
|
48
|
+
define_method( methname, &block )
|
49
|
+
return self.instance_method( methname )
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
### Return the name of the method that the given +block+ should be installed
|
54
|
+
### as, derived from the specified +pattern+.
|
55
|
+
def make_handler_method_name( pattern, block )
|
56
|
+
_, line = block.source_location
|
57
|
+
pattern = pattern.
|
58
|
+
gsub( /#/, 'hash' ).
|
59
|
+
gsub( /\*/, 'star' ).
|
60
|
+
gsub( /\./, '_' )
|
61
|
+
|
62
|
+
return "on_%s_%d" % [ pattern, line ]
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
### Return a regular expression that will match messages matching the given
|
67
|
+
### +routing_key+.
|
68
|
+
def make_routing_pattern( routing_key )
|
69
|
+
re_string = routing_key.gsub( /\./, '\\.' )
|
70
|
+
re_string = re_string.gsub( /\*/, '([^\.]*)' )
|
71
|
+
re_string = re_string.gsub( /#/, '(.*)' )
|
72
|
+
|
73
|
+
return Regexp.compile( re_string )
|
74
|
+
end
|
75
|
+
|
76
|
+
end # module ClassMethods
|
77
|
+
|
78
|
+
|
79
|
+
### Route the work based on the blocks registered with 'on'.
|
80
|
+
def work( payload, metadata )
|
81
|
+
key = metadata[:delivery_info].routing_key
|
82
|
+
self.log.debug "Routing a %s message..." % [ key ]
|
83
|
+
|
84
|
+
blocks = self.class.routes.inject([]) do |accum, (re, re_blocks)|
|
85
|
+
accum += re_blocks if re.match( key )
|
86
|
+
accum
|
87
|
+
end
|
88
|
+
|
89
|
+
self.log.debug " calling %d block/s" % [ blocks.length ]
|
90
|
+
return blocks.all? do |block|
|
91
|
+
block.bind( self ).call( payload, metadata )
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
end # module Symphony::Routing
|
97
|
+
|
98
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'symphony'
|
5
|
+
|
6
|
+
|
7
|
+
# A module containing signal-handling logic for both tasks and the Symphony
|
8
|
+
# daemon.
|
9
|
+
module Symphony::SignalHandling
|
10
|
+
|
11
|
+
|
12
|
+
### Wrap a block in signal-handling.
|
13
|
+
def with_signal_handler( *signals )
|
14
|
+
self.set_up_signal_handling
|
15
|
+
self.set_signal_traps( *signals )
|
16
|
+
self.start_signal_handler
|
17
|
+
|
18
|
+
return yield
|
19
|
+
|
20
|
+
ensure
|
21
|
+
self.stop_signal_handler
|
22
|
+
self.reset_signal_traps( *signals )
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
### Set up data structures for signal handling.
|
27
|
+
def set_up_signal_handling
|
28
|
+
# Self-pipe for deferred signal-handling (ala djb:
|
29
|
+
# http://cr.yp.to/docs/selfpipe.html)
|
30
|
+
reader, writer = IO.pipe
|
31
|
+
reader.close_on_exec = true
|
32
|
+
writer.close_on_exec = true
|
33
|
+
@selfpipe = { reader: reader, writer: writer }
|
34
|
+
|
35
|
+
# Set up a global signal queue
|
36
|
+
Thread.main[:signal_queue] = []
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
### The body of the signal handler. Wait for at least one signal to arrive and
|
41
|
+
### handle it. This should be called inside a loop, either in its own thread or
|
42
|
+
### in another loop that doesn't block anywhere else.
|
43
|
+
def wait_for_signals
|
44
|
+
|
45
|
+
# Wait on the selfpipe for signals
|
46
|
+
self.log.debug " waiting for the selfpipe"
|
47
|
+
fds = IO.select( [@selfpipe[:reader]] )
|
48
|
+
begin
|
49
|
+
rval = @selfpipe[:reader].read_nonblock( 11 )
|
50
|
+
self.log.debug " read from the selfpipe: %p" % [ rval ]
|
51
|
+
rescue Errno::EAGAIN, Errno::EINTR => err
|
52
|
+
self.log.debug " %p: %s!" % [ err.class, err.message ]
|
53
|
+
# ignore
|
54
|
+
end
|
55
|
+
|
56
|
+
# Look for any signals that arrived and handle them
|
57
|
+
while sig = Thread.main[:signal_queue].shift
|
58
|
+
self.log.debug " got a queued signal: %p" % [ sig ]
|
59
|
+
self.handle_signal( sig )
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
### Wake the main thread up through the self-pipe.
|
65
|
+
### Note: since this is a signal-handler method, it needs to be re-entrant.
|
66
|
+
def wake_up
|
67
|
+
@selfpipe[:writer].write_nonblock('.')
|
68
|
+
rescue Errno::EAGAIN
|
69
|
+
# Ignore.
|
70
|
+
rescue Errno::EINTR
|
71
|
+
# Repeated signal. :TODO: Does this need a counter?
|
72
|
+
retry
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
### Set up signal handlers for common signals that will shut down, restart, etc.
|
77
|
+
def set_signal_traps( *signals )
|
78
|
+
self.log.debug "Setting up deferred signal handlers."
|
79
|
+
signals.each do |sig|
|
80
|
+
Signal.trap( sig ) do
|
81
|
+
Thread.main[:signal_queue] << sig
|
82
|
+
self.wake_up
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
### Set all signal handlers to ignore.
|
89
|
+
def ignore_signals( *signals )
|
90
|
+
self.log.debug "Ignoring signals."
|
91
|
+
signals.each do |sig|
|
92
|
+
Signal.trap( sig, :IGNORE )
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
### Set the signal handlers back to their defaults.
|
98
|
+
def reset_signal_traps( *signals )
|
99
|
+
self.log.debug "Restoring default signal handlers."
|
100
|
+
signals.each do |sig|
|
101
|
+
Signal.trap( sig, :DEFAULT )
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
end # module Symphony::SignalHandling
|
107
|
+
|