sqsrun 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ require 'rubygems' unless defined?(gem)
4
+ here = File.dirname(__FILE__)
5
+ $LOAD_PATH << File.expand_path(File.join(here, '..', 'lib'))
6
+ require 'sqsrun/command/sqsrun'
@@ -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
+