symphony 0.3.0.pre20140327204419
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|