amqp 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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