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.
@@ -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