amqp 0.7.1 → 0.7.2

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 CHANGED
@@ -8,4 +8,9 @@
8
8
  Gemfile.lock
9
9
 
10
10
  spec/amqp.yml
11
- vendor/cache
11
+ vendor/cache
12
+ vendor/amq*
13
+ vendor/evented-spec
14
+ doc/*
15
+ .yardoc/*
16
+ tmp/*
@@ -0,0 +1,9 @@
1
+ script: "bundle exec rake spec:ci"
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - ree
6
+ - jruby
7
+ gemfile:
8
+ - Gemfile
9
+ - gemfiles/eventmachine-pre
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ = Version 0.7.2
2
+
3
+ * [BUG] Server-named queues declared en masse now get their unique names instead of all beign assigned the first generated name
4
+ * [API] Connection URI (string) format for vhosts no longer assumes that vhosts begin with a slash (/), learn more at http://bit.ly/mfzwcB
5
+ * [BUG] Heartbeats broken for long running tasks [#43]
6
+ * [BUG] Queue#reset leaks consumer tags [#40].
7
+
1
8
  = Version 0.7.1
2
9
 
3
10
  * [BUG] AMQP gem no longer conflicts with Builder 2.1.2 on Ruby 1.9.
data/Gemfile CHANGED
@@ -1,13 +1,24 @@
1
1
  # encoding: utf-8
2
2
 
3
- source "http://gemcutter.org"
3
+ source :rubygems
4
+
5
+ # Use local clones if possible.
6
+ # If you want to use your local copy, just symlink it to vendor.
7
+ def custom_gem(name, options = Hash.new)
8
+ local_path = File.expand_path("../vendor/#{name}", __FILE__)
9
+ if File.exist?(local_path)
10
+ gem name, options.merge(:path => local_path).delete_if { |key, _| [:git, :branch].include?(key) }
11
+ else
12
+ gem name, options
13
+ end
14
+ end
15
+
4
16
 
5
17
  gem "eventmachine"
6
- gem "json" if RUBY_VERSION < "1.9" || ARGV.first == "install"
7
- gem "amq-client", :git => "git://github.com/ruby-amqp/amq-client.git", :branch => "master"
8
18
 
9
19
  group(:test) do
10
20
  gem "rspec", ">=2.0.0"
11
-
12
- gem "amqp-spec", :git => "git://github.com/ruby-amqp/amqp-spec.git", :branch => "master"
21
+ gem "rake"
22
+ # gem "amqp-spec", :git => "git://github.com/ruby-amqp/amqp-spec.git", :branch => "master"
23
+ custom_gem "evented-spec", :git => "git://github.com/ruby-amqp/evented-spec.git", :branch => "master"
13
24
  end
data/Rakefile CHANGED
@@ -1,8 +1,14 @@
1
+ require 'fileutils'
1
2
  require 'rspec/core/rake_task'
2
3
 
3
4
  desc "Run spec suite (uses Rspec2)"
4
5
  RSpec::Core::RakeTask.new(:spec) { |t|}
5
6
 
7
+ namespace :spec do
8
+ desc "Clean up rbx compiled files and run spec suite"
9
+ RSpec::Core::RakeTask.new(:ci) { |t| Dir.glob("**/*.rbc").each {|f| FileUtils.rm_f(f) } }
10
+ end
11
+
6
12
  desc "Run specs with RCov"
7
13
  RSpec::Core::RakeTask.new(:rcov) do |t|
8
14
  t.rcov = true
@@ -10,8 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.authors = ["Aman Gupta", "Jakub Stastny aka botanicus", "Michael S. Klishin"]
11
11
  s.homepage = "http://github.com/ruby-amqp/amqp"
12
12
  s.summary = "AMQP client implementation in Ruby/EventMachine."
13
- s.description = "An implementation of the AMQP protocol in Ruby/EventMachine for writing clients to the RabbitMQ message broker."
14
- s.cert_chain = nil
13
+ s.description = "Asynchronous AMQP 0.9.1 client for Ruby. Built on top of Eventmachine."
15
14
  s.email = ["bWljaGFlbEBub3ZlbWJlcmFpbi5jb20=\n", "c3Rhc3RueUAxMDFpZGVhcy5jeg==\n"].map { |i| Base64.decode64(i) }
16
15
 
17
16
  # files
@@ -19,20 +18,11 @@ Gem::Specification.new do |s|
19
18
  s.require_paths = ["lib"]
20
19
 
21
20
  # RDoc
22
- s.has_rdoc = true
23
21
  s.rdoc_options = '--include=examples --main README.md'
24
22
  s.extra_rdoc_files = ["README.md"] + Dir.glob("doc/*")
25
23
 
26
24
  # Dependencies
27
- s.add_dependency "eventmachine", ">= 0.12.4"
28
-
29
- begin
30
- require "changelog"
31
- rescue LoadError
32
- warn "You have to have changelog gem installed for post install message"
33
- else
34
- s.post_install_message = CHANGELOG.new.version_changes
35
- end
25
+ s.add_dependency "eventmachine"
36
26
 
37
27
  # RubyForge
38
28
  s.rubyforge_project = "amqp"
@@ -1,21 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
- # guest:guest has full access to /
3
+ # guest:guest has full access to /amqp_gem_07x_stable_testbed
4
4
 
5
- rabbitmqctl add_vhost /
5
+ rabbitmqctl add_vhost /amqp_gem_07x_stable_testbed
6
6
  rabbitmqctl add_user guest guest
7
7
  rabbitmqctl set_permissions -p / guest ".*" ".*" ".*"
8
-
9
-
10
- # amqp_gem:amqp_gem_password has full access to /amqp_gem_testbed
11
-
12
- rabbitmqctl add_vhost /amqp_gem_testbed
13
- rabbitmqctl add_user amqp_gem amqp_gem_password
14
- rabbitmqctl set_permissions -p /amqp_gem_testbed amqp_gem ".*" ".*" ".*"
15
-
16
-
17
- # amqp_gem_reader:reader_password has read access to /amqp_gem_testbed
18
-
19
- rabbitmqctl add_user amqp_gem_reader reader_password
20
- rabbitmqctl clear_permissions -p /amqp_gem_testbed guest
21
- rabbitmqctl set_permissions -p /amqp_gem_testbed amqp_gem_reader "^---$" "^---$" ".*"
@@ -20,7 +20,7 @@ end
20
20
 
21
21
  EM.run do
22
22
  connection = AMQP.connect
23
- mq = AMQP::Channel.new(connection)
23
+ ch = AMQP::Channel.new(connection)
24
24
 
25
25
  show_stopper = Proc.new do
26
26
  $stdout.puts "Stopping..."
@@ -35,14 +35,14 @@ EM.run do
35
35
 
36
36
  $stdout.puts "Bound! Running #{AMQP::VERSION} version of the gem."
37
37
 
38
- queue1 = mq.queue("queue1")
39
- queue2 = mq.queue("queue2")
40
- queue3 = mq.queue("queue3")
38
+ queue1 = ch.queue("queue1")
39
+ queue2 = ch.queue("queue2")
40
+ queue3 = ch.queue("queue3")
41
41
 
42
42
  queues = [queue1, queue2, queue3]
43
43
 
44
44
  # Rely on default direct exchange binding, see section 2.1.2.4 Automatic Mode in AMQP 0.9.1 spec.
45
- exchange = AMQP::Channel::Exchange.default
45
+ exchange = ch.default_exchange
46
46
 
47
47
  queue1.subscribe do |payload|
48
48
  puts "Got #{payload} for #{queue1.name}"
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift(File.expand_path("../../lib", __FILE__))
4
+ require 'amqp'
5
+
6
+ AMQP.start(:host => 'localhost') do |connection|
7
+
8
+ # Send Connection.Close on Ctrl+C
9
+ trap(:INT) do
10
+ AMQP.stop do
11
+ puts "Closing and AMQP.channel is #{AMQP.channel.open? ? 'open' : 'closed'}"
12
+ exit
13
+ end
14
+ end
15
+
16
+ puts "AMQP.connection is #{AMQP.connection}"
17
+ puts "AMQP.channel is #{AMQP.channel}"
18
+
19
+ end
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "bundler"
5
+ Bundler.setup
6
+
7
+ $:.unshift(File.expand_path("../../lib", __FILE__))
8
+
9
+ require 'amqp'
10
+
11
+
12
+ puts "=> AMQP gem #{AMQP::VERSION}"
13
+ puts
14
+ AMQP.start do |connection|
15
+ channel = AMQP::Channel.new
16
+ puts "Channel ##{channel.channel} is now open!"
17
+
18
+ xchange = channel.fanout("amq.fanout")
19
+ q = AMQP::Queue.new(channel, "", :auto_delete => true).bind(xchange).subscribe do |header, payload|
20
+ puts "Got a payload: #{payload}"
21
+ end
22
+
23
+ EventMachine.add_periodic_timer(0.5) {
24
+ xchange.publish("à bientôt!", :key => q.name)
25
+ }
26
+
27
+
28
+ show_stopper = Proc.new do
29
+ $stdout.puts "Stopping..."
30
+
31
+ connection.close {
32
+ EM.stop { exit }
33
+ }
34
+ end
35
+
36
+ Signal.trap "INT", show_stopper
37
+ EM.add_timer(3, show_stopper)
38
+ end
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "bundler"
5
+ Bundler.setup
6
+
7
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
8
+
9
+ require 'amqp'
10
+
11
+ AMQP.start(:host => "localhost") do |connection|
12
+ channel = AMQP::Channel.new(connection)
13
+ channel.fanout("logs.nad", :auto_delete => false)
14
+ channel.fanout("logs.ad", :auto_delete => true)
15
+
16
+ EM.add_timer(1) do
17
+ connection.close do
18
+ EM.stop { exit }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ source :rubygems
4
+
5
+ # Use local clones if possible.
6
+ # If you want to use your local copy, just symlink it to vendor.
7
+ def custom_gem(name, options = Hash.new)
8
+ local_path = File.expand_path("../vendor/#{name}", __FILE__)
9
+ if File.exist?(local_path)
10
+ gem name, options.merge(:path => local_path).delete_if { |key, _| [:git, :branch].include?(key) }
11
+ else
12
+ gem name, options
13
+ end
14
+ end
15
+
16
+
17
+ gem "eventmachine", "~> 1.0.0.beta3"
18
+
19
+ group(:test) do
20
+ gem "rspec", ">=2.0.0"
21
+ gem "rake"
22
+ # gem "amqp-spec", :git => "git://github.com/ruby-amqp/amqp-spec.git", :branch => "master"
23
+ custom_gem "evented-spec", :git => "git://github.com/ruby-amqp/evented-spec.git", :branch => "master"
24
+ end
@@ -138,6 +138,8 @@ module AMQP
138
138
 
139
139
  @connection = connection || AMQP.start
140
140
 
141
+ @queues_awaiting_declare_ok = Array.new
142
+
141
143
  conn.callback { |c|
142
144
  @channel = c.add_channel(self)
143
145
  send Protocol::Channel::Open.new
@@ -147,6 +149,10 @@ module AMQP
147
149
  attr_reader :channel, :connection, :status
148
150
  alias :conn :connection
149
151
 
152
+ attr_reader :queues_awaiting_declare_ok
153
+
154
+
155
+
150
156
  def closed?
151
157
  @status.eql?(:closed)
152
158
  end
@@ -240,6 +246,18 @@ module AMQP
240
246
  end
241
247
  end
242
248
 
249
+ # Returns exchange object with the same name as default (aka unnamed) exchange.
250
+ # Default exchange is a direct exchange and automatically routes messages to
251
+ # queues when routing key matches queue name exactly. This feature is known as
252
+ # "automatic binding" (of queues to default exchange).
253
+ #
254
+ # *Use default exchange when you want to route messages directly to specific queues*
255
+ # (queue names are known, you don't mind this kind of coupling between applications).
256
+ def default_exchange
257
+ Exchange.default(self)
258
+ end
259
+
260
+
243
261
  # Defines, intializes and returns an Exchange to act as an ingress
244
262
  # point for all published messages.
245
263
  #
@@ -609,14 +627,19 @@ module AMQP
609
627
  # method it will raise a channel or connection exception.
610
628
  #
611
629
  def queue(name, opts = {}, &block)
612
- if queue = self.queues.find { |queue| queue.name == name }
630
+ raise ArgumentError, "queue name must not be nil. Use '' (empty string) for server-named queues." if name.nil?
631
+
632
+ if name && !name.empty? && (queue = self.queues.find { |queue| queue.name == name })
613
633
  extended_opts = Queue.add_default_options(name, opts, block)
614
634
 
615
635
  validate_parameters_match!(queue, extended_opts)
616
636
 
617
637
  queue
618
638
  else
619
- self.queues << Queue.new(self, name, opts, &block)
639
+ q = Queue.new(self, name, opts, &block)
640
+ self.queues << q
641
+
642
+ q
620
643
  end
621
644
  end
622
645
 
@@ -746,6 +769,9 @@ module AMQP
746
769
  def reset
747
770
  @deferred_status = nil
748
771
  @channel = nil
772
+
773
+ @queues_awaiting_declare_ok = Array.new
774
+
749
775
  initialize @connection
750
776
 
751
777
  @consumers = {}
@@ -857,12 +883,9 @@ module AMQP
857
883
  exchange.receive_response method
858
884
 
859
885
  when Protocol::Queue::DeclareOk
860
- # We can't use queues[method.queue] because if the name would
861
- # be an empty string, then AMQP broker generated a random one.
862
- queues = self.queues.select { |queue| queue.opts[:nowait].eql?(false) }
863
- queue = queues.reverse.find { |queue| queue.status.eql?(:unfinished) }
864
- queue.receive_status method
886
+ queue = @queues_awaiting_declare_ok.shift
865
887
 
888
+ queue.receive_status method
866
889
  when Protocol::Queue::BindOk
867
890
  # We can't use queues[method.queue] because if the name would
868
891
  # be an empty string, then AMQP broker generated a random one.
@@ -935,3 +958,25 @@ end # AMQP
935
958
 
936
959
 
937
960
  MQ = AMQP::Channel
961
+
962
+ #
963
+ # Backwards compatibility with 0.6.x
964
+ #
965
+
966
+ class MQ
967
+ # unique identifier
968
+ def MQ.id
969
+ Thread.current[:mq_id] ||= "#{`hostname`.strip}-#{Process.pid}-#{Thread.current.object_id}"
970
+ end
971
+
972
+ def MQ.default
973
+ # TODO: clear this when connection is closed
974
+ Thread.current[:mq] ||= MQ.new
975
+ end
976
+
977
+ # Allows for calls to all MQ instance methods. This implicitly calls
978
+ # MQ.new so that a new channel is allocated for subsequent operations.
979
+ def MQ.method_missing(meth, *args, &blk)
980
+ MQ.default.__send__(meth, *args, &blk)
981
+ end
982
+ end
@@ -14,9 +14,28 @@ module AMQP
14
14
  @client = mod
15
15
  end
16
16
 
17
+ def self.mutex
18
+ @mutex ||= Mutex.new
19
+ end
20
+
21
+ def self.channel
22
+ self.mutex.synchronize { @channel }
23
+ end
24
+
25
+ def self.channel=(value)
26
+ self.mutex.synchronize { @channel = value }
27
+ end
28
+
29
+
17
30
  module Client
18
31
  include EM::Deferrable
19
32
 
33
+ #
34
+ # API
35
+ #
36
+
37
+ attr_reader :settings
38
+
20
39
  def initialize(opts = {})
21
40
  @settings = opts
22
41
  extend AMQP.client
@@ -27,6 +46,7 @@ module AMQP
27
46
 
28
47
  timeout @settings[:timeout] if @settings[:timeout]
29
48
  errback { @on_disconnect.call } unless @reconnecting
49
+ @connection_status = @settings[:connection_status]
30
50
 
31
51
  # TCP connection "openness"
32
52
  @tcp_connection_established = false
@@ -61,13 +81,15 @@ module AMQP
61
81
  def init_heartbeat
62
82
  @last_server_heartbeat = Time.now
63
83
 
64
- @timer ||= EM::PeriodicTimer.new(@settings[:heartbeat]) do
84
+ @timer.cancel if @timer
85
+ @timer = EM::PeriodicTimer.new(@settings[:heartbeat]) do
65
86
  if connected?
66
87
  if @last_server_heartbeat < (Time.now - (@settings[:heartbeat] * 2))
67
88
  log "Reconnecting due to missing server heartbeats"
68
89
  reconnect(true)
69
90
  else
70
- send AMQP::Frame::Heartbeat.new
91
+ @last_server_heartbeat = Time.now
92
+ send AMQP::Frame::Heartbeat.new, :channel => 0
71
93
  end
72
94
  end
73
95
  end
@@ -207,18 +229,63 @@ module AMQP
207
229
 
208
230
  private
209
231
 
232
+ AMQP_PORTS = Hash["amqp" => 5672, "amqps" => 5671].freeze
233
+
234
+ # Parses AMQP connection URI and returns its components as a hash.
235
+ #
236
+ # h2. vhost naming schemes
237
+ #
238
+ # It is convenient to be able to specify the AMQP connection
239
+ # parameters as a URI string, and various "amqp" URI schemes
240
+ # exist. Unfortunately, there is no standard for these URIs, so
241
+ # while the schemes share the basic idea, they differ in some
242
+ # details. This implementation aims to encourage URIs that work
243
+ # as widely as possible.
244
+ #
245
+ # The URI scheme should be "amqp", or "amqps" if SSL is required.
246
+ #
247
+ # The host, port, username and password are represented in the
248
+ # authority component of the URI in the same way as in http URIs.
249
+ #
250
+ # The vhost is obtained from the first segment of the path, with the
251
+ # leading slash removed. The path should contain only a single
252
+ # segment (i.e, the only slash in it should be the leading one).
253
+ # If the vhost is to include slashes or other reserved URI
254
+ # characters, these should be percent-escaped.
255
+ #
256
+ # @example How vhost is parsed
257
+ #
258
+ # AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com") # => vhost is nil, so default (/) will be used
259
+ # AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/") # => vhost is an empty string
260
+ # AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/%2Fvault") # => vhost is /vault
261
+ # AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/production") # => vhost is production
262
+ # AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/a.b.c") # => vhost is a.b.c
263
+ # AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/foo/bar") # => ArgumentError
264
+ #
265
+ #
266
+ # @param [String] connection_string AMQP connection URI, à la JDBC connection string. For example: amqp://bus.megacorp.internal:5877.
267
+ # @return [Hash] Connection parameters (:username, :password, :vhost, :host, :port, :ssl)
268
+ #
269
+ # @raise [ArgumentError] When connection URI schema is not amqp or amqps, or the path contains multiple segments
270
+ #
271
+ # @see http://bit.ly/ks8MXK Connecting to The Broker documentation guide
272
+ # @api public
210
273
  def self.parse_connection_uri(connection_string)
211
274
  uri = URI.parse(connection_string)
212
- raise("amqp:// uri required!") unless %w{amqp amqps}.include?(uri.scheme)
275
+ raise ArgumentError.new("Connection URI must use amqp or amqps schema (example: amqp://bus.megacorp.internal:5766), learn more at http://bit.ly/ks8MXK") unless %w{amqp amqps}.include?(uri.scheme)
213
276
 
214
277
  opts = {}
215
278
 
216
- opts[:user] = URI.unescape(uri.user) if uri.user
217
- opts[:pass] = URI.unescape(uri.password) if uri.password
218
- opts[:vhost] = URI.unescape(uri.path) if uri.path
219
- opts[:host] = uri.host if uri.host
220
- opts[:port] = uri.port || Hash["amqp" => 5672, "amqps" => 5671][uri.scheme]
221
- opts[:ssl] = uri.scheme == "amqps"
279
+ opts[:scheme] = uri.scheme
280
+ opts[:user] = URI.unescape(uri.user) if uri.user
281
+ opts[:pass] = URI.unescape(uri.password) if uri.password
282
+ opts[:host] = uri.host if uri.host
283
+ opts[:port] = uri.port || AMQP_PORTS[uri.scheme]
284
+ opts[:ssl] = uri.scheme == "amqps"
285
+ if uri.path =~ %r{^/(.*)}
286
+ raise ArgumentError.new("#{uri} has multiple-segment path; please percent-encode any slashes in the vhost name (e.g. /production => %2Fproduction). Learn more at http://bit.ly/amqp-gem-and-connection-uris") if $1.index('/')
287
+ opts[:vhost] = URI.unescape($1)
288
+ end
222
289
 
223
290
  opts
224
291
  end