amqpop 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +95 -0
- data/Rakefile +2 -0
- data/amqpop.gemspec +20 -0
- data/bin/amqpop +5 -0
- data/lib/amqpop/amqpop.rb +32 -0
- data/lib/amqpop/auth_file.rb +66 -0
- data/lib/amqpop/cli.rb +197 -0
- data/lib/amqpop/lock_file.rb +78 -0
- data/lib/amqpop/message.rb +128 -0
- data/lib/amqpop/trollop.rb +782 -0
- data/lib/amqpop/version.rb +3 -0
- metadata +110 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/amqpop.gemspec
ADDED
@@ -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
|
data/bin/amqpop
ADDED
@@ -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
|
data/lib/amqpop/cli.rb
ADDED
@@ -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
|