sqsrun 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +89 -0
- data/bin/sqsrun +6 -0
- data/lib/sqsrun/command/sqsrun.rb +444 -0
- metadata +84 -0
data/README.rdoc
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
= sqsrun
|
2
|
+
|
3
|
+
A Generics Worker Executor Service for Amazon SQS. It polls SQS queue and run command or script when a message is received. The message is removed only when the command succeeded.
|
4
|
+
|
5
|
+
|
6
|
+
== Architecture
|
7
|
+
|
8
|
+
1. sqsrun receives a message from the queue.
|
9
|
+
2. sqsrun executes a command or runs a script.
|
10
|
+
* if the process takes long time, sqsrun extends visibility timeout. this is repeated until it is killed.
|
11
|
+
* if the process takes more long time, sqsrun kills the process.
|
12
|
+
3. if it succeeded, sqsrun removes the message from the queue.
|
13
|
+
4. if it failed, sqsrun set the visibility timeout to 0 and expect to be retried.
|
14
|
+
|
15
|
+
|
16
|
+
== Usage
|
17
|
+
|
18
|
+
Usage: sqsrun [options] [-- <ARGV-for-exec-or-run>]
|
19
|
+
-k, --key-id ID AWS Access Key ID
|
20
|
+
-s, --secret-key KEY AWS Secret Access Key
|
21
|
+
-q, --queue NAME SQS queue name
|
22
|
+
-t, --timeout SEC SQS visibility timeout (default: 30)
|
23
|
+
--push MESSAGE Push maessage to the queue
|
24
|
+
--list List queues
|
25
|
+
--configure PATH.yaml Write configuration file
|
26
|
+
--exec COMMAND Execute command
|
27
|
+
--run SCRIPT.rb Run method named 'run' defined in the script
|
28
|
+
-e, --extend-timeout SEC Threashold time before extending visibility timeout (default: timeout * 3/4)
|
29
|
+
-x, --kill-timeout SEC Threashold time before killing process (default: timeout * 5)
|
30
|
+
-i, --interval SEC Polling interval (default: 1)
|
31
|
+
-d, --daemon PIDFILE Daemonize (default: foreground)
|
32
|
+
-f, --file PATH.yaml Read configuration file
|
33
|
+
|
34
|
+
One of --push, --list, --configure, --exec or --run is required. The behavior of the commands is described below:
|
35
|
+
|
36
|
+
|
37
|
+
=== push
|
38
|
+
|
39
|
+
Push a message to the queue. -k, -s and -q options are required.
|
40
|
+
|
41
|
+
|
42
|
+
=== list
|
43
|
+
|
44
|
+
Show list of queues. -k and -s options are required.
|
45
|
+
|
46
|
+
|
47
|
+
=== configure
|
48
|
+
|
49
|
+
Write configuration file and exit. Written configuration file can be used with -f option:
|
50
|
+
|
51
|
+
_Example:_
|
52
|
+
|
53
|
+
## create sqsrun.yaml file
|
54
|
+
$ sqsrun --configure sqsrun.yaml -k KEY_ID -s SEC_KEY -q myqueue --run myrun.rb -- my run args
|
55
|
+
|
56
|
+
## run sqsrun using the configuration file
|
57
|
+
$ sqsrun -f sqsrun.yaml
|
58
|
+
|
59
|
+
|
60
|
+
=== exec
|
61
|
+
|
62
|
+
Execute a command when a message is received. Body of the message is passed to the stdin. The command have to exit with status code 0 when it succeeded.
|
63
|
+
|
64
|
+
-k, -s and -q options are required.
|
65
|
+
|
66
|
+
_Example:_
|
67
|
+
|
68
|
+
#!/usr/bin/env ruby
|
69
|
+
require 'json'
|
70
|
+
js = JSON.load(STDIN.read)
|
71
|
+
puts "received: #{js.inspect}"
|
72
|
+
|
73
|
+
# $ sqsrun -k AWS_KEY_ID -s AWS_SEC_KEY -q SQS_NAME --exec ./this_file
|
74
|
+
|
75
|
+
|
76
|
+
=== run
|
77
|
+
|
78
|
+
This is same as 'exec' except that this calls a method named 'run' defined in the file instead of executing the file. Body of the message is passed to the argument. It is assumed it succeeded if the method doesn't any raise errors.
|
79
|
+
|
80
|
+
_Example:_
|
81
|
+
|
82
|
+
require 'json'
|
83
|
+
def run(msg)
|
84
|
+
js = JSON.load(msg)
|
85
|
+
puts "received: #{js.inspect}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# $ sqsrun -k AWS_KEY_ID -s AWS_SEC_KEY -q SQS_NAME --run ./this_file.rb
|
89
|
+
|
data/bin/sqsrun
ADDED
@@ -0,0 +1,444 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module SQSRun
|
4
|
+
|
5
|
+
|
6
|
+
class Worker
|
7
|
+
def initialize(conf)
|
8
|
+
require 'right_aws'
|
9
|
+
@key_id = conf[:key_id]
|
10
|
+
@secret_key = conf[:secret_key]
|
11
|
+
@queue_name = conf[:queue]
|
12
|
+
@visibility_timeout = conf[:timeout]
|
13
|
+
@extend_timeout = conf[:extend_timeout]
|
14
|
+
@kill_timeout = conf[:kill_timeout]
|
15
|
+
@interval = conf[:interval]
|
16
|
+
@finished = false
|
17
|
+
|
18
|
+
@extender = VisibilityExtender.new(@visibility_timeout, @extend_timeout)
|
19
|
+
@sqs = RightAws::SqsGen2.new(@key_id, @secret_key)
|
20
|
+
@queue = @sqs.queue(@queue_name)
|
21
|
+
|
22
|
+
@mutex = Mutex.new
|
23
|
+
@cond = ConditionVariable.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(run_proc)
|
27
|
+
@run_proc = run_proc
|
28
|
+
@extender.start
|
29
|
+
until @finished
|
30
|
+
msg = @queue.receive(@visibility_timeout)
|
31
|
+
if msg
|
32
|
+
process(msg)
|
33
|
+
else
|
34
|
+
cond_wait(@interval)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def shutdown
|
40
|
+
@finished = true
|
41
|
+
@extender.shutdown
|
42
|
+
receive!
|
43
|
+
end
|
44
|
+
|
45
|
+
def receive!
|
46
|
+
@mutex.synchronize {
|
47
|
+
@cond.broadcast
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def finished?
|
52
|
+
@finished
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
if ConditionVariable.new.method(:wait).arity == 1
|
57
|
+
require 'timeout'
|
58
|
+
def cond_wait(sec)
|
59
|
+
@mutex.synchronize {
|
60
|
+
Timeout.timeout(sec) {
|
61
|
+
@cond.wait(@mutex)
|
62
|
+
}
|
63
|
+
}
|
64
|
+
rescue Timeout::Error
|
65
|
+
end
|
66
|
+
else
|
67
|
+
def cond_wait(sec)
|
68
|
+
@mutex.synchronize {
|
69
|
+
@cond.wait(@mutex, sec)
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def process(msg)
|
75
|
+
puts "started id=#{msg.id}"
|
76
|
+
thread = Thread.new(msg.to_s, &@run_proc.method(:call))
|
77
|
+
|
78
|
+
@extender.set_message(msg)
|
79
|
+
|
80
|
+
success = false
|
81
|
+
begin
|
82
|
+
joined = thread.join(@kill_timeout)
|
83
|
+
if joined
|
84
|
+
thread.value
|
85
|
+
success = true
|
86
|
+
puts "finished id=#{msg.id}"
|
87
|
+
else
|
88
|
+
thread.kill
|
89
|
+
puts "killed id=#{msg.id}"
|
90
|
+
end
|
91
|
+
rescue
|
92
|
+
puts "failed id=#{msg.id}: #{$!}"
|
93
|
+
$!.backtrace.each {|bt|
|
94
|
+
puts " #{bt}"
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
@extender.reset_message
|
99
|
+
|
100
|
+
if success
|
101
|
+
msg.delete
|
102
|
+
else
|
103
|
+
msg.visibility = 0
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class VisibilityExtender
|
108
|
+
include MonitorMixin
|
109
|
+
|
110
|
+
def initialize(visibility_timeout, extend_timeout)
|
111
|
+
super()
|
112
|
+
@visibility_timeout = visibility_timeout
|
113
|
+
@extend_timeout = extend_timeout
|
114
|
+
@extend_time = nil
|
115
|
+
@message = nil
|
116
|
+
@finished = false
|
117
|
+
end
|
118
|
+
|
119
|
+
def start
|
120
|
+
@thread = Thread.new(&method(:run))
|
121
|
+
end
|
122
|
+
|
123
|
+
def join
|
124
|
+
@thread.join
|
125
|
+
end
|
126
|
+
|
127
|
+
def set_message(msg)
|
128
|
+
synchronize do
|
129
|
+
@extend_time = Time.now.to_i + @extend_timeout
|
130
|
+
@message = msg
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def reset_message
|
135
|
+
synchronize do
|
136
|
+
@message = nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def shutdown
|
141
|
+
@finished = true
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
def run
|
146
|
+
until @finished
|
147
|
+
sleep 1
|
148
|
+
synchronize do
|
149
|
+
try_extend(@message) if @message
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def try_extend(msg)
|
155
|
+
now = Time.now.to_i
|
156
|
+
if now > @extend_time
|
157
|
+
ntime = msg.visibility + @visibility_timeout
|
158
|
+
puts "extending timeout=#{ntime} id=#{msg.id}"
|
159
|
+
msg.visibility = ntime
|
160
|
+
@extend_time = now + @extend_timeout
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
class ExecRunner
|
168
|
+
def initialize(cmd)
|
169
|
+
@cmd = cmd + ' ' + ARGV.map {|a| Shellwords.escape(a) }.join(' ')
|
170
|
+
@iobuf = ''
|
171
|
+
end
|
172
|
+
|
173
|
+
def call(message)
|
174
|
+
IO.popen(@cmd, "r+") {|io|
|
175
|
+
io.write(message) rescue nil
|
176
|
+
io.close_write
|
177
|
+
begin
|
178
|
+
while true
|
179
|
+
io.sysread(1024, @iobuf)
|
180
|
+
print @iobuf
|
181
|
+
end
|
182
|
+
rescue EOFError
|
183
|
+
end
|
184
|
+
}
|
185
|
+
if $?.to_i != 0
|
186
|
+
raise "Command failed"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
class Controller
|
193
|
+
def initialize(conf)
|
194
|
+
require 'right_aws'
|
195
|
+
@key_id = conf[:key_id]
|
196
|
+
@secret_key = conf[:secret_key]
|
197
|
+
@queue_name = conf[:queue]
|
198
|
+
@visibility_timeout = conf[:visibility_timeout]
|
199
|
+
end
|
200
|
+
|
201
|
+
def push(body)
|
202
|
+
@sqs = RightAws::SqsGen2.new(@key_id, @secret_key)
|
203
|
+
@queue = @sqs.queue(@queue_name, true, @visibility_timeout)
|
204
|
+
@queue.send_message(body)
|
205
|
+
end
|
206
|
+
|
207
|
+
def list
|
208
|
+
@sqs = RightAws::SqsGen2.new(@key_id, @secret_key)
|
209
|
+
@sqs.queues.map {|q| q.name }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
def self.worker=(worker)
|
215
|
+
@worker = worker
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.receive!
|
219
|
+
@worker.receive!
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.finished?
|
223
|
+
@worker.finished?
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
require 'optparse'
|
231
|
+
|
232
|
+
op = OptionParser.new
|
233
|
+
|
234
|
+
op.banner += " [-- <ARGV-for-exec-or-run>]"
|
235
|
+
|
236
|
+
type = nil
|
237
|
+
message = nil
|
238
|
+
confout = nil
|
239
|
+
|
240
|
+
defaults = {
|
241
|
+
:timeout => 30,
|
242
|
+
:interval => 1,
|
243
|
+
}
|
244
|
+
|
245
|
+
conf = { }
|
246
|
+
|
247
|
+
op.on('-k', '--key-id ID', 'AWS Access Key ID') {|s|
|
248
|
+
conf[:key_id] = s
|
249
|
+
}
|
250
|
+
|
251
|
+
op.on('-s', '--secret-key KEY', 'AWS Secret Access Key') {|s|
|
252
|
+
conf[:secret_key] = s
|
253
|
+
}
|
254
|
+
|
255
|
+
op.on('-q', '--queue NAME', 'SQS queue name') {|s|
|
256
|
+
conf[:queue] = s
|
257
|
+
}
|
258
|
+
|
259
|
+
op.on('-t', '--timeout SEC', 'SQS visibility timeout (default: 30)', Integer) {|i|
|
260
|
+
conf[:timeout] = i
|
261
|
+
}
|
262
|
+
|
263
|
+
op.on('--push MESSAGE', 'Push maessage to the queue') {|s|
|
264
|
+
type = :push
|
265
|
+
message = s
|
266
|
+
}
|
267
|
+
|
268
|
+
op.on('--list', 'List queues') {|s|
|
269
|
+
type = :list
|
270
|
+
}
|
271
|
+
|
272
|
+
op.on('--configure PATH.yaml', 'Write configuration file') {|s|
|
273
|
+
type = :conf
|
274
|
+
confout = s
|
275
|
+
}
|
276
|
+
|
277
|
+
op.on('--exec COMMAND', 'Execute command') {|s|
|
278
|
+
type = :exec
|
279
|
+
conf[:exec] = s
|
280
|
+
}
|
281
|
+
|
282
|
+
op.on('--run SCRIPT.rb', 'Run method named \'run\' defined in the script') {|s|
|
283
|
+
type = :run
|
284
|
+
conf[:run] = s
|
285
|
+
}
|
286
|
+
|
287
|
+
op.on('-e', '--extend-timeout SEC', 'Threashold time before extending visibility timeout (default: timeout * 3/4)', Integer) {|i|
|
288
|
+
conf[:extend_timeout] = i
|
289
|
+
}
|
290
|
+
|
291
|
+
op.on('-x', '--kill-timeout SEC', 'Threashold time before killing process (default: timeout * 5)', Integer) {|i|
|
292
|
+
conf[:kill_timeout] = i
|
293
|
+
}
|
294
|
+
|
295
|
+
op.on('-i', '--interval SEC', 'Polling interval (default: 1)', Integer) {|i|
|
296
|
+
conf[:interval] = i
|
297
|
+
}
|
298
|
+
|
299
|
+
op.on('-d', '--daemon PIDFILE', 'Daemonize (default: foreground)') {|s|
|
300
|
+
conf[:daemon] = s
|
301
|
+
}
|
302
|
+
|
303
|
+
op.on('-f', '--file PATH.yaml', 'Read configuration file') {|s|
|
304
|
+
conf[:file] = s
|
305
|
+
}
|
306
|
+
|
307
|
+
|
308
|
+
(class<<self;self;end).module_eval do
|
309
|
+
define_method(:usage) do |msg|
|
310
|
+
puts op.to_s
|
311
|
+
puts "error: #{msg}" if msg
|
312
|
+
exit 1
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
begin
|
318
|
+
if eqeq = ARGV.index('--')
|
319
|
+
argv = ARGV.slice!(0, eqeq)
|
320
|
+
ARGV.slice!(0)
|
321
|
+
else
|
322
|
+
argv = ARGV.slice!(0..-1)
|
323
|
+
end
|
324
|
+
op.parse!(argv)
|
325
|
+
|
326
|
+
if argv.length != 0
|
327
|
+
usage nil
|
328
|
+
end
|
329
|
+
|
330
|
+
if conf[:file]
|
331
|
+
require 'yaml'
|
332
|
+
yaml = YAML.load File.read(conf[:file])
|
333
|
+
y = {}
|
334
|
+
yaml.each_pair {|k,v| y[k.to_sym] = v }
|
335
|
+
|
336
|
+
conf = defaults.merge(y).merge(conf)
|
337
|
+
|
338
|
+
if ARGV.empty? && conf[:args]
|
339
|
+
ARGV.clear
|
340
|
+
ARGV.concat conf[:args]
|
341
|
+
end
|
342
|
+
else
|
343
|
+
conf = defaults.merge(conf)
|
344
|
+
end
|
345
|
+
|
346
|
+
unless type
|
347
|
+
if conf[:run]
|
348
|
+
type = :run
|
349
|
+
elsif conf[:exec]
|
350
|
+
type = :exec
|
351
|
+
else
|
352
|
+
raise "--push, --list, --configure, --exec or --run is required"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
unless conf[:key_id]
|
357
|
+
raise "-k, --key-id ID option is required"
|
358
|
+
end
|
359
|
+
|
360
|
+
unless conf[:secret_key]
|
361
|
+
raise "-s, --secret-key KEY option is required"
|
362
|
+
end
|
363
|
+
|
364
|
+
unless conf[:extend_timeout]
|
365
|
+
conf[:extend_timeout] = conf[:timeout] / 4 * 3
|
366
|
+
end
|
367
|
+
|
368
|
+
unless conf[:kill_timeout]
|
369
|
+
conf[:kill_timeout] = conf[:timeout] * 5
|
370
|
+
end
|
371
|
+
|
372
|
+
if !conf[:queue] && (type == :push || type == :exec || type == :run)
|
373
|
+
raise "-q, --queue NAME option is required"
|
374
|
+
end
|
375
|
+
|
376
|
+
rescue
|
377
|
+
usage $!.to_s
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
if confout
|
382
|
+
require 'yaml'
|
383
|
+
|
384
|
+
conf.delete(:file)
|
385
|
+
conf[:args] = ARGV
|
386
|
+
|
387
|
+
y = {}
|
388
|
+
conf.each_pair {|k,v| y[k.to_s] = v }
|
389
|
+
|
390
|
+
File.open(confout, "w") {|f|
|
391
|
+
f.write y.to_yaml
|
392
|
+
}
|
393
|
+
exit 0
|
394
|
+
end
|
395
|
+
|
396
|
+
|
397
|
+
case type
|
398
|
+
when :push
|
399
|
+
pro = SQSRun::Controller.new(conf)
|
400
|
+
pro.push(message)
|
401
|
+
|
402
|
+
when :list
|
403
|
+
pro = SQSRun::Controller.new(conf)
|
404
|
+
pro.list.each {|name|
|
405
|
+
puts name
|
406
|
+
}
|
407
|
+
|
408
|
+
when :exec, :run
|
409
|
+
if conf[:daemon]
|
410
|
+
exit!(0) if fork
|
411
|
+
Process.setsid
|
412
|
+
exit!(0) if fork
|
413
|
+
File.umask(0)
|
414
|
+
STDIN.reopen("/dev/null")
|
415
|
+
STDOUT.reopen("/dev/null", "w")
|
416
|
+
STDERR.reopen("/dev/null", "w")
|
417
|
+
File.open(conf[:daemon], "w") {|f|
|
418
|
+
f.write Process.pid.to_s
|
419
|
+
}
|
420
|
+
end
|
421
|
+
|
422
|
+
worker = SQSRun::Worker.new(conf)
|
423
|
+
SQSRun.worker = worker
|
424
|
+
|
425
|
+
trap :INT do
|
426
|
+
puts "shutting down..."
|
427
|
+
worker.shutdown
|
428
|
+
end
|
429
|
+
|
430
|
+
trap :TERM do
|
431
|
+
puts "shutting down..."
|
432
|
+
worker.shutdown
|
433
|
+
end
|
434
|
+
|
435
|
+
if type == :run
|
436
|
+
load File.expand_path(conf[:run])
|
437
|
+
run_proc = method(:run)
|
438
|
+
else
|
439
|
+
run_proc = SQSRun::ExecRunner.new(conf[:exec])
|
440
|
+
end
|
441
|
+
|
442
|
+
worker.run(run_proc)
|
443
|
+
end
|
444
|
+
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sqsrun
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Sadayuki Furuhashi
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-11 00:00:00 +09:00
|
19
|
+
default_executable: sqsrun
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: right_aws
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 11
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 1
|
33
|
+
- 0
|
34
|
+
version: 2.1.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description:
|
38
|
+
email: frsyuki@gmail.com
|
39
|
+
executables:
|
40
|
+
- sqsrun
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- README.rdoc
|
45
|
+
files:
|
46
|
+
- bin/sqsrun
|
47
|
+
- lib/sqsrun/command/sqsrun.rb
|
48
|
+
- README.rdoc
|
49
|
+
has_rdoc: true
|
50
|
+
homepage:
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options:
|
55
|
+
- --charset=UTF-8
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.3.7
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: Generic SQS Worker Executor Service
|
83
|
+
test_files: []
|
84
|
+
|