amqpop 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in amqpop.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Chris Cherry
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,95 @@
1
+ amqpop
2
+ ======
3
+
4
+ Tool for dispatching a child command for each message consumed from an AMQP
5
+ queue.
6
+
7
+
8
+ Benefits
9
+ --------
10
+
11
+ Designed for situations where you need to consume and act on messages from an
12
+ AMQP queue without a long running process.
13
+
14
+
15
+ Example
16
+ -------
17
+
18
+ amqpop -h rabbit.domain.com -u user -n 2 -x logs -w 10 -- /usr/bin/ruby log_processor.rb
19
+
20
+ This command would do the following:
21
+
22
+ - Make sure only 1 instance of this amqpop command is running by using a
23
+ lock/pid file keyed based on the arguments of the command
24
+ - Connect to the AMQP server `rabbit.domain.com` as `user` with password looked
25
+ up from the auth file ~/.amqpop_auth
26
+ - Attach a unique, non durable queue to the exchange named logs
27
+ - Pass the message payload in as STDIN to `/usr/bin/ruby log_processor.rb`
28
+ - Run up to 2 parallel processes of `/usr/bin/ruby log_processor.rb`
29
+ - Once the queue has been exhausted of messages, wait for another message for 10
30
+ seconds, if one shows up inside of that time, process it immediately, and then
31
+ wait for another 10 seconds and repeat, otherwise terminate.
32
+
33
+
34
+ Usage
35
+ -----
36
+
37
+ amqpop [options] -- <child-command>
38
+
39
+ [options] are:
40
+ -h <s>: AMQP host (default: localhost)
41
+ -u <s>: AMQP user (default: guest)
42
+ -p <s>: AMQP password (default: guest)
43
+ -n <i>: Number of child message processing commands that can be executed in
44
+ parallel (default: 1)
45
+ -q <s>: Name of the queue on the to connect to, unique if not provided
46
+ (default: )
47
+ -e: Is the queue persistent (default: true)
48
+ -a: Does the queue remove itself
49
+ -x <s>: Exchange to bind the queue to. Format [name]:<routing>, example:
50
+ logs or weather:usa.* (default: )
51
+ -w <i>: Amount of time in seconds to wait for more messages to show up.
52
+ Waits forever if 0 (default: 30)
53
+
54
+ --verbose, -v: Verbose logging
55
+ --version, -r: Print version and exit
56
+ --help, -l: Show this message
57
+
58
+
59
+ Auth File
60
+ ---------
61
+
62
+ You can put a file at ~/.amqpop_auth which is used to intelligently look up
63
+ login credentials when calling amqpop. No more sensative information in the
64
+ process list or in your history! Use one entry per line, in the format:
65
+
66
+ host user pass
67
+
68
+ Example:
69
+
70
+ 192.168.1.1 consumer 1Consumer2
71
+ 192.168.1.1 other XYother$Z
72
+ rabbithost.domain.com publisher_user 98pubPass234
73
+
74
+ The amount and kind of whitespace between host, user and pass don't matter, just
75
+ don't use newlines.
76
+
77
+ Note: The auth file must have world readable and writable disabled (so 660 or
78
+ 600 are ok)
79
+
80
+
81
+ Tested On
82
+ ---------
83
+ * OSX 10.8.2, 10.7.5
84
+ * Ruby 1.9.3-p327, 1.8.7-p358
85
+ * RabbitMQ 2.8.7
86
+ * on Erlang R14A, Debian 6.0, kernel 2.6.32-5-686
87
+ * on Erlang R14B04, RedHat EL5, kernel 2.6.18-238.9.1.el5xen
88
+
89
+
90
+ Acknowledgements
91
+ ----------------
92
+
93
+ Thank you to William Morgan and the contributors of Trollop
94
+ (http://trollop.rubyforge.org/)
95
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/amqpop/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Chris Cherry"]
6
+ gem.email = ["ctcherry@gmail.com"]
7
+ gem.description = %q{Command line AMQP consumer}
8
+ gem.summary = %q{Command line tool for consuming messages off of an AMQP queue and dispatching them to a user specified command}
9
+ gem.homepage = "https://github.com/ctcherry/amqpop"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^spec/})
14
+ gem.name = "amqpop"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Amqpop::VERSION
17
+
18
+ gem.add_dependency "amqp", "~> 0.9.8"
19
+ gem.add_development_dependency "rspec", "~> 2.12.0"
20
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'amqpop/cli'
4
+
5
+ Amqpop::CLI.start
@@ -0,0 +1,32 @@
1
+ module Amqpop
2
+
3
+ class << self
4
+ attr_accessor :options
5
+ end
6
+
7
+ self.options = nil
8
+
9
+ def self.require_ack?
10
+ return nil if options.nil?
11
+ !temp_queue?
12
+ end
13
+
14
+ def self.temp_queue?
15
+ return nil if options.nil?
16
+ options[:queue_name] == ""
17
+ end
18
+
19
+ def self.eputs(msg)
20
+ STDERR.puts msg
21
+ end
22
+
23
+ def self.vputs(msg)
24
+ eputs "> #{msg}" if verbose?
25
+ end
26
+
27
+ def self.verbose?
28
+ return nil if options.nil?
29
+ options[:verbose]
30
+ end
31
+
32
+ end
@@ -0,0 +1,66 @@
1
+ require 'digest/md5'
2
+
3
+ module Amqpop
4
+ class AuthFile
5
+
6
+ def initialize
7
+ @hosts = {}
8
+ load_available_files
9
+ end
10
+
11
+ def lookup(match_host, match_user = "")
12
+ host = @hosts[match_host]
13
+ return nil if host.nil?
14
+ return nil if host.keys.empty?
15
+
16
+ match_user = host.keys.first if match_user.to_s == ""
17
+ pass = host[match_user]
18
+ return nil if pass.to_s == ""
19
+
20
+ {:host => match_host, :username => match_user, :password => pass}
21
+ end
22
+
23
+ private
24
+
25
+ def load_available_files
26
+ ["~/.amqpop_auth"].each do |f|
27
+ f = File.expand_path(f)
28
+ load_file(f)
29
+ end
30
+ end
31
+
32
+ def load_file(file)
33
+ if File.exist?(file)
34
+ if world_readable?(file) || world_writable?(file)
35
+ STDERR.puts "WARNING: Auth file: #{file} has unsafe permissions, not loaded"
36
+ return false
37
+ end
38
+ parse_file(file)
39
+ end
40
+ end
41
+
42
+ def parse_file(file)
43
+ File.readlines(file).each do |line|
44
+ host, user, pass = line.split(/\s+/)
45
+ if host.to_s == "" || user.to_s == "" || pass.to_s == ""
46
+ STDERR.pust "WARNING: Auth file: #{file} has invalid line, skipped that line"
47
+ return false
48
+ end
49
+
50
+ @hosts[host] ||= {}
51
+ @hosts[host][user] = pass
52
+ end
53
+ end
54
+
55
+ def world_writable?(file)
56
+ world_write_perms = 0000002
57
+ (File.stat(file).mode & world_write_perms) != 0
58
+ end
59
+
60
+ def world_readable?(file)
61
+ world_read_perms = 0000004
62
+ (File.stat(file).mode & world_read_perms) != 0
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,197 @@
1
+ require 'amqpop/amqpop'
2
+ require 'amqpop/trollop'
3
+ require 'amqpop/lock_file'
4
+ require 'amqpop/auth_file'
5
+ require 'amqpop/message'
6
+ require 'eventmachine'
7
+ require 'amqp'
8
+
9
+ module Amqpop
10
+
11
+ class CLI
12
+
13
+ def self.start
14
+ self.new.start
15
+ end
16
+
17
+ def initialize
18
+ STDOUT.sync = true
19
+ options
20
+ @lock = LockFile.new(options)
21
+ @lock.acquire!
22
+ @auth = AuthFile.new
23
+ end
24
+
25
+ def start
26
+ begin
27
+ EventMachine.threadpool_size = options[:num_children]
28
+ EventMachine.run do
29
+ Signal.trap("INT") { shutdown }
30
+ Signal.trap("TERM") { shutdown }
31
+
32
+ vputs "Running #{AMQP::VERSION} version of the AMQP gem."
33
+ vputs "Connecting to AMQP broker on #{connection_params[:host]} as #{connection_params[:username]}."
34
+ AMQP.connect(connection_params) do |connection|
35
+ AMQP::Channel.new(connection) do |channel|
36
+
37
+ channel.on_error do |ch, close|
38
+ eputs "ERROR: Channel-level exception: #{close.reply_text}, #{close.inspect}"
39
+ connection.close {
40
+ shutdown
41
+ }
42
+ end
43
+
44
+ if options[:wait] == 0
45
+ vputs "No timeout set, process will stay running"
46
+ else
47
+ vputs "Timeout of #{options[:wait]} seconds set"
48
+ end
49
+
50
+ queue = get_queue(channel)
51
+
52
+ queue.once_declared do
53
+ vputs "Connected to queue: #{queue.name}"
54
+ vputs "Ack mode: #{Amqpop.require_ack? ? 'explicit' : 'auto'}"
55
+ end
56
+
57
+ bind_queue(queue)
58
+
59
+ queue.subscribe(:confirm => proc{ wait_exit_timer }, :ack => Amqpop.require_ack?) do |meta, payload|
60
+ cancel_wait_exit_timer
61
+ m = Message.new(payload, meta)
62
+ vputs "Received a message: #{payload}. Executing..."
63
+ EventMachine.defer(m.command_proc, m.callback_proc)
64
+ wait_exit_timer
65
+ end
66
+
67
+ end
68
+ end
69
+ end
70
+ rescue => e
71
+ eputs "ERROR: #{e.class} - #{e.message}"
72
+ exit 2
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def shutdown
79
+ EventMachine.stop
80
+ @lock.release!
81
+ exit 0
82
+ end
83
+
84
+ def connection_params
85
+ return @connection_params if defined?(@connection_params)
86
+
87
+ params = {:host => options[:host], :username => options[:user], :password => options[:pass]}
88
+
89
+ if options[:host] != "localhost" && options[:user] != "guest" && options[:pass] != "guest"
90
+ # Every option was changed from the default, so we don't need to lookup anything
91
+ @connection_params = params
92
+ return @connection_params
93
+ end
94
+
95
+ if options[:host] != "localhost" && options[:user] == "guest" && options[:pass] == "guest"
96
+ # They set host, but not user or pass
97
+ creds = @auth.lookup(options[:host], options[:user])
98
+ creds = @auth.lookup(options[:host]) if creds.nil?
99
+
100
+ @connection_params = (creds.nil?) ? params : creds
101
+
102
+ return @connection_params
103
+ end
104
+
105
+ if options[:host] != "localhost" && options[:user] != "guest" && options[:pass] == "guest"
106
+ # They set host and user, but not pass
107
+ creds = @auth.lookup(options[:host], options[:user])
108
+ @connection_params = (creds.nil?) ? params : creds
109
+
110
+ return @connection_params
111
+ end
112
+
113
+ @connection_params = params
114
+
115
+ end
116
+
117
+ def cancel_wait_exit_timer
118
+ @wait_timer.cancel if defined?(@wait_timer)
119
+ end
120
+
121
+ def wait_exit_timer
122
+ return if options[:wait] == 0
123
+ cancel_wait_exit_timer
124
+ @wait_timer = EventMachine::Timer.new(options[:wait]) do
125
+ vputs "Timeout of #{options[:wait]} seconds expired, exiting"
126
+ shutdown
127
+ end
128
+ end
129
+
130
+ def bind_queue(queue)
131
+ if options[:exchange][:name] == ""
132
+ vputs "Binding queue to default exchange implicitly, with routing key '#{queue.name}'"
133
+ else
134
+ vputs "Binding queue to exchange: #{options[:exchange][:name]}, with routing key '#{options[:exchange][:routing_key]}'"
135
+ queue.bind(options[:exchange][:name], :routing_key => options[:exchange][:routing_key])
136
+ end
137
+ end
138
+
139
+ def get_queue(channel)
140
+ if Amqpop.temp_queue?
141
+ channel.queue('', :auto_delete => true, :durable => false, :exclusive => true)
142
+ else
143
+ channel.queue(options[:queue_name], :auto_delete => false, :durable => true)
144
+ end
145
+ end
146
+
147
+ def eputs(msg)
148
+ Amqpop.eputs msg
149
+ end
150
+
151
+ def vputs(msg)
152
+ Amqpop.vputs msg
153
+ end
154
+
155
+ def options
156
+ return Amqpop.options unless Amqpop.options.nil?
157
+ @options = Trollop::options do
158
+ version "amqpop 0.0.1 (c) 2012 Chris Cherry"
159
+ banner <<-EOS
160
+ Command line tool for consuming messages off of an AMQP queue and dispatching them to a user specified command.
161
+
162
+ Usage:
163
+ amqpop [options] -- <child-command>
164
+
165
+ [options] are:
166
+
167
+ EOS
168
+ opt :host, "AMQP host", :type => :string, :short => "-h", :default => 'localhost'
169
+ opt :user, "AMQP user", :type => :string, :short => "-u", :default => 'guest'
170
+ opt :pass, "AMQP password", :type => :string, :short => "-p", :default => 'guest'
171
+ opt :num_children, "Number of child message processing commands that can be executed in parallel", :short => "-n", :default => 1
172
+ opt :queue_name, "Name of the queue on the to connect to, unique if not provided", :type => :string, :default => ''
173
+ opt :queue_durable, "Is the queue persistant", :default => true
174
+ opt :queue_auto_delete, "Does the queue remove itself", :default => false
175
+ opt :exchange, "Exchange to bind the queue to. Format [name]:<routing_key>, example: logs or weather:usa.*", :type => :string, :short => "-x", :default => ''
176
+
177
+ opt :wait, "Amount of time in seconds to wait for more messages to show up. Waits forever if 0", :default => 30
178
+ opt :verbose, "Verbose logging", :short => :none
179
+ opt :help, "Show this help message", :short => :none
180
+ stop_on "--"
181
+ end
182
+
183
+ # Break exchange into hash of its parts
184
+ ename, eroute = @options[:exchange].to_s.split(":")
185
+ @options[:exchange] = {:name => ename.to_s, :routing_key => eroute.to_s}
186
+
187
+ # 1 second is too fast, make it a minimum of 2
188
+ @options[:wait] = 2 if @options[:wait] == 1
189
+
190
+ # Store child command, or empty array
191
+ dbldash = ARGV.shift
192
+ @options[:child_command] = ARGV.dup
193
+ Amqpop.options = @options
194
+ end
195
+
196
+ end
197
+ end