bunny 0.8.0 → 0.9.0.pre1

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.
Files changed (91) hide show
  1. data/.gitignore +7 -1
  2. data/.travis.yml +14 -4
  3. data/ChangeLog.md +72 -0
  4. data/Gemfile +17 -11
  5. data/README.md +82 -0
  6. data/bunny.gemspec +6 -13
  7. data/examples/connection/heartbeat.rb +17 -0
  8. data/lib/bunny.rb +40 -56
  9. data/lib/bunny/channel.rb +615 -19
  10. data/lib/bunny/channel_id_allocator.rb +59 -0
  11. data/lib/bunny/compatibility.rb +24 -0
  12. data/lib/bunny/concurrent/condition.rb +63 -0
  13. data/lib/bunny/consumer.rb +42 -26
  14. data/lib/bunny/consumer_tag_generator.rb +22 -0
  15. data/lib/bunny/consumer_work_pool.rb +67 -0
  16. data/lib/bunny/exceptions.rb +128 -0
  17. data/lib/bunny/exchange.rb +131 -136
  18. data/lib/bunny/framing.rb +53 -0
  19. data/lib/bunny/heartbeat_sender.rb +59 -0
  20. data/lib/bunny/main_loop.rb +70 -0
  21. data/lib/bunny/message_metadata.rb +126 -0
  22. data/lib/bunny/queue.rb +102 -275
  23. data/lib/bunny/session.rb +478 -0
  24. data/lib/bunny/socket.rb +44 -0
  25. data/lib/bunny/system_timer.rb +9 -9
  26. data/lib/bunny/transport.rb +179 -0
  27. data/lib/bunny/version.rb +1 -1
  28. data/spec/compatibility/queue_declare_spec.rb +40 -0
  29. data/spec/higher_level_api/integration/basic_ack_spec.rb +54 -0
  30. data/spec/higher_level_api/integration/basic_consume_spec.rb +51 -0
  31. data/spec/higher_level_api/integration/basic_get_spec.rb +47 -0
  32. data/spec/higher_level_api/integration/basic_nack_spec.rb +39 -0
  33. data/spec/higher_level_api/integration/basic_publish_spec.rb +105 -0
  34. data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -0
  35. data/spec/higher_level_api/integration/basic_recover_spec.rb +18 -0
  36. data/spec/higher_level_api/integration/basic_reject_spec.rb +53 -0
  37. data/spec/higher_level_api/integration/basic_return_spec.rb +33 -0
  38. data/spec/higher_level_api/integration/channel_close_spec.rb +29 -0
  39. data/spec/higher_level_api/integration/channel_flow_spec.rb +24 -0
  40. data/spec/higher_level_api/integration/channel_open_spec.rb +57 -0
  41. data/spec/higher_level_api/integration/channel_open_stress_spec.rb +22 -0
  42. data/spec/higher_level_api/integration/confirm_select_spec.rb +19 -0
  43. data/spec/higher_level_api/integration/connection_spec.rb +340 -0
  44. data/spec/higher_level_api/integration/exchange_bind_spec.rb +31 -0
  45. data/spec/higher_level_api/integration/exchange_declare_spec.rb +183 -0
  46. data/spec/higher_level_api/integration/exchange_delete_spec.rb +37 -0
  47. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +40 -0
  48. data/spec/higher_level_api/integration/queue_bind_spec.rb +109 -0
  49. data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -0
  50. data/spec/higher_level_api/integration/queue_delete_spec.rb +38 -0
  51. data/spec/higher_level_api/integration/queue_purge_spec.rb +30 -0
  52. data/spec/higher_level_api/integration/queue_unbind_spec.rb +33 -0
  53. data/spec/higher_level_api/integration/tx_commit_spec.rb +21 -0
  54. data/spec/higher_level_api/integration/tx_rollback_spec.rb +21 -0
  55. data/spec/lower_level_api/integration/basic_cancel_spec.rb +57 -0
  56. data/spec/lower_level_api/integration/basic_consume_spec.rb +100 -0
  57. data/spec/spec_helper.rb +64 -0
  58. data/spec/unit/bunny_spec.rb +15 -0
  59. data/spec/unit/concurrent/condition_spec.rb +66 -0
  60. metadata +135 -93
  61. data/CHANGELOG +0 -21
  62. data/README.textile +0 -76
  63. data/Rakefile +0 -14
  64. data/examples/simple.rb +0 -32
  65. data/examples/simple_ack.rb +0 -35
  66. data/examples/simple_consumer.rb +0 -55
  67. data/examples/simple_fanout.rb +0 -41
  68. data/examples/simple_headers.rb +0 -42
  69. data/examples/simple_publisher.rb +0 -29
  70. data/examples/simple_topic.rb +0 -61
  71. data/ext/amqp-0.9.1.json +0 -389
  72. data/ext/config.yml +0 -4
  73. data/ext/qparser.rb +0 -426
  74. data/lib/bunny/client.rb +0 -370
  75. data/lib/bunny/subscription.rb +0 -92
  76. data/lib/qrack/amq-client-url.rb +0 -165
  77. data/lib/qrack/channel.rb +0 -20
  78. data/lib/qrack/client.rb +0 -247
  79. data/lib/qrack/errors.rb +0 -5
  80. data/lib/qrack/protocol/protocol.rb +0 -135
  81. data/lib/qrack/protocol/spec.rb +0 -525
  82. data/lib/qrack/qrack.rb +0 -20
  83. data/lib/qrack/queue.rb +0 -40
  84. data/lib/qrack/subscription.rb +0 -152
  85. data/lib/qrack/transport/buffer.rb +0 -305
  86. data/lib/qrack/transport/frame.rb +0 -102
  87. data/spec/spec_09/amqp_url_spec.rb +0 -19
  88. data/spec/spec_09/bunny_spec.rb +0 -76
  89. data/spec/spec_09/connection_spec.rb +0 -34
  90. data/spec/spec_09/exchange_spec.rb +0 -173
  91. data/spec/spec_09/queue_spec.rb +0 -240
data/.gitignore CHANGED
@@ -9,4 +9,10 @@
9
9
  Gemfile.lock
10
10
  .rbx/*
11
11
  .tags
12
- .tags_sorted_by_file
12
+ .tags_sorted_by_file
13
+ .Apple*
14
+ bin/*
15
+ .bundle/*
16
+ vendor/*
17
+ playground/*
18
+ *.org
@@ -1,11 +1,21 @@
1
1
  bundler_args: --without development
2
- script: "bundle exec rspec spec"
2
+ before_script: "./bin/ci/before_build.sh"
3
+ script: "bundle exec rspec -c spec"
3
4
  rvm:
4
5
  - 1.8.7
5
- - ree
6
6
  - 1.9.2
7
7
  - 1.9.3
8
+ - ruby-head
8
9
  - rbx-18mode
9
- # - rbx-19mode
10
+ - rbx-19mode
11
+ - jruby-19mode
10
12
  notifications:
11
- email: false
13
+ email: michael@defprotocol.org
14
+ services:
15
+ - rabbitmq
16
+ branches:
17
+ only:
18
+ - master
19
+ - 0.8.x-stable
20
+ - 0.9.x-stable
21
+ - migrate_to_amq_protocol
@@ -0,0 +1,72 @@
1
+ # Changes between Bunny 0.8.x and 0.9.0
2
+
3
+ ## New convenience functions: Bunny::Channel#fanout, Bunny::Channel#topic
4
+
5
+ `Bunny::Channel#fanout`, `Bunny::Channel#topic`, `Bunny::Channel#direct`, `Bunny::Channel#headers`,
6
+ and`Bunny::Channel#default_exchange` are new convenience methods to instantiate exchanges:
7
+
8
+ ``` ruby
9
+ conn = Bunny.new
10
+ conn.start
11
+
12
+ ch = conn.create_channel
13
+ x = ch.fanout("logging.events", :durable => true)
14
+ ```
15
+
16
+
17
+ ## Bunny::Queue#pop and consumer handlers (Bunny::Queue#subscribe) signatures have changed
18
+
19
+ Bunny `< 0.9.x` example:
20
+
21
+ ``` ruby
22
+ h = queue.pop
23
+
24
+ puts h[:delivery_info], h[:header], h[:payload]
25
+ ```
26
+
27
+ Bunny `>= 0.9.x` example:
28
+
29
+ ``` ruby
30
+ delivery_info, properties, payload = queue.pop
31
+ ```
32
+
33
+ The improve is both in that Ruby has positional destructuring, e.g.
34
+
35
+ ``` ruby
36
+ delivery_info, _, content = q.pop
37
+ ```
38
+
39
+ but not hash destructuring, like, say, Clojure does.
40
+
41
+ In addition we return nil for content when it should be nil
42
+ (basic.get-empty) and unify these arguments betwee
43
+
44
+ * Bunny::Queue#pop
45
+
46
+ * Consumer (Bunny::Queue#subscribe, etc) handlers
47
+
48
+ * Returned message handlers
49
+
50
+ The unification moment was the driving factor.
51
+
52
+
53
+
54
+ ## Bunny::Client#write now raises Bunny::ConnectionError
55
+
56
+ Bunny::Client#write now raises `Bunny::ConnectionError` instead of `Bunny::ServerDownError` when network
57
+ I/O operations fail.
58
+
59
+
60
+ ## Bunny::Client.create_channel now uses a bitset-based allocator
61
+
62
+ Instead of reusing channel instances, `Bunny::Client.create_channel` now opens new channels and
63
+ uses bitset-based allocator to keep track of used channel ids. This avoids situations when
64
+ channels are reused or shared without developer's explicit intent but also work well for
65
+ long running applications that aggressively open and release channels.
66
+
67
+ This is also how amqp gem and RabbitMQ Java client manage channel ids.
68
+
69
+
70
+ ## Bunny::ServerDownError is now Bunny::TCPConnectionFailed
71
+
72
+ `Bunny::ServerDownError` is now an alias for `Bunny::TCPConnectionFailed`
data/Gemfile CHANGED
@@ -21,19 +21,25 @@ extend Module.new {
21
21
 
22
22
  gem "SystemTimer", "1.2", :platform => :ruby_18
23
23
 
24
- group :development do
25
- gem "rake"
26
-
27
- gem "yard", ">= 0.7.2"
28
-
29
- # Yard tags this buddy along.
30
- gem "RedCloth", :platform => :mri
31
-
32
- gem "changelog"
33
- end
24
+ gem "rake"
25
+ gem "effin_utf8"
34
26
 
35
27
  group :test do
36
- gem "rspec", "~> 2.6.0"
28
+ gem "rspec", "~> 2.8.0"
37
29
  end
38
30
 
39
31
  gemspec
32
+
33
+ # Use local clones if possible.
34
+ # If you want to use your local copy, just symlink it to vendor.
35
+ def custom_gem(name, options = Hash.new)
36
+ local_path = File.expand_path("../vendor/#{name}", __FILE__)
37
+ if File.exist?(local_path)
38
+ puts "Using #{name} from #{local_path}..."
39
+ gem name, options.merge(:path => local_path).delete_if { |key, _| [:git, :branch].include?(key) }
40
+ else
41
+ gem name, options
42
+ end
43
+ end
44
+
45
+ custom_gem "amq-protocol", :git => "git://github.com/ruby-amqp/amq-protocol.git", :branch => "master"
@@ -0,0 +1,82 @@
1
+ # About Bunny
2
+
3
+ Bunny is a synchronous RabbitMQ client that focuses on ease of use.
4
+
5
+
6
+ ## Supported Ruby Versions
7
+
8
+ It supports Ruby 1.9.3, 1.9.2, 1.8.7, Rubinius 2 and JRuby.
9
+
10
+
11
+ ## Supported RabbitMQ Versions
12
+
13
+ Bunny versions `< 0.7.x` support RabbitMQ 1.x and 2.x. Bunny `0.8.x` and later versions only
14
+ supports RabbitMQ 2.x.
15
+
16
+
17
+ ## Important: Bunny is about to undergo a lot of internal changes
18
+
19
+ Bunny is a very old library with **a lot** of missing functionality. It also implements an older version of the spec
20
+ and may or may not work with future RabbitMQ versions. As such, Bunny is about to undergo serious internal changes.
21
+ We will make our best to keep them as backwards compatible as possible but within reason.
22
+
23
+ See [this announcement](https://groups.google.com/forum/?fromgroups#!topic/ruby-amqp/crNVGEuHm68) to learn more.
24
+
25
+ In the meantime, consider using [Hot Bunnies](http://github.com/ruby-amqp/hot_bunnies) (JRuby-only) or [amqp gem](http://rubyamqp.info) instead.
26
+
27
+
28
+ ## Quick Start for Bunny 0.7.x and 0.8.x
29
+
30
+ ``` ruby
31
+ require "bunny"
32
+
33
+ b = Bunny.new(:logging => true)
34
+
35
+ # start a communication session with the amqp server
36
+ b.start
37
+
38
+ # declare a queue
39
+ q = b.queue("test1")
40
+
41
+ # declare default direct exchange which is bound to all queues
42
+ e = b.exchange("")
43
+
44
+ # publish a message to the exchange which then gets routed to the queue
45
+ e.publish("Hello, everybody!", :key => 'test1')
46
+
47
+ # get message from the queue
48
+ msg = q.pop[:payload]
49
+
50
+ puts "This is the message: " + msg + "\n\n"
51
+
52
+ # close the connection
53
+ b.stop
54
+ ```
55
+
56
+ ... or just:
57
+
58
+ ```
59
+ require "bunny"
60
+
61
+ # Create a direct queue named "my_testq"
62
+ Bunny.run { |c| c.queue("my_testq") }
63
+ ```
64
+
65
+ ## Community & Getting Help
66
+
67
+ Please use [Ruby RabbitMQ clients Google Group](http://groups.google.com/group/ruby-amqp) for any questions you may
68
+ have.
69
+
70
+ For news and updates, [follow @rubyamqp](http://twitter.com/rubyamqp) on Twitter.
71
+
72
+
73
+
74
+ ## Other Resources
75
+
76
+ * [AMQP 0.9.1 model explained](): introductory explanation of the AMQP v0.9.1 specification with particular reference to RabbitMQ.
77
+
78
+
79
+ ## Links
80
+
81
+ * [Source code](http://github.com/ruby-amqp/bunny)
82
+ * [Blog](http://bunnyamqp.wordpress.com)
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.name = "bunny"
9
9
  s.version = Bunny::VERSION.dup
10
10
  s.homepage = "http://github.com/ruby-amqp/bunny"
11
- s.summary = "Synchronous Ruby AMQP 0.8.0 client"
12
- s.description = "A synchronous Ruby AMQP client that enables interaction with AMQP-compliant brokers."
11
+ s.summary = "Easy to use synchronous Ruby client for RabbitMQ"
12
+ s.description = "Easy to use synchronous Ruby client for RabbitMQ"
13
13
 
14
14
  # Sorted alphabetically.
15
15
  s.authors = [
@@ -27,20 +27,13 @@ Gem::Specification.new do |s|
27
27
  "c2thZXNAcmFpbHNleHByZXNzLmRl\n"].
28
28
  map { |mail| Base64.decode64(mail) }
29
29
 
30
+ # Dependencies
31
+ s.add_dependency "amq-protocol", ">= 1.0.0"
32
+
30
33
  # Files.
31
34
  s.has_rdoc = true
32
- s.extra_rdoc_files = ["README.textile"]
33
- s.rdoc_options = ["--main", "README.rdoc"]
35
+ s.extra_rdoc_files = ["README.md"]
34
36
  s.files = `git ls-files`.split("\n")
35
37
  s.test_files = `git ls-files -- spec/*`.split("\n")
36
38
  s.require_paths = ["lib"]
37
-
38
- begin
39
- require "changelog"
40
- s.post_install_message = CHANGELOG.new.version_changes
41
- rescue LoadError
42
- end
43
-
44
- # RubyForge
45
- s.rubyforge_project = "bunny-amqp"
46
39
  end
@@ -0,0 +1,17 @@
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 'bunny'
10
+
11
+
12
+ b = Bunny.new(:heartbeat_interval => 2)
13
+ b.start
14
+
15
+ c = b.create_channel
16
+
17
+ sleep 10
@@ -1,54 +1,67 @@
1
- # encoding: utf-8
1
+ # -*- encoding: utf-8; mode: ruby -*-
2
2
 
3
- require "socket"
4
- require "thread"
5
3
  require "timeout"
6
- require "logger"
7
4
 
8
- require File.expand_path("../bunny/version", __FILE__)
9
- # if we don't require the version file the same way as in the gemspec,
10
- # the version file will be loaded twice. and we hate warnings.
5
+ require "bunny/version"
6
+ require "amq/protocol/client"
7
+
8
+ require "bunny/framing"
9
+ require "bunny/exceptions"
10
+ require "bunny/socket"
11
+
12
+ # Core entities: connection, channel, exchange, queue, consumer
13
+ require "bunny/session"
14
+ require "bunny/channel"
15
+ require "bunny/exchange"
16
+ require "bunny/queue"
17
+ require "bunny/consumer"
11
18
 
12
19
  module Bunny
20
+ PROTOCOL_VERSION = AMQ::Protocol::PROTOCOL_VERSION
21
+
22
+ # unifies Ruby standard library's Timeout (which is not accurate on
23
+ # Ruby 1.8 and has other issues) and SystemTimer (the gem)
24
+ Timer = if RUBY_VERSION < "1.9"
25
+ begin
26
+ require "bunny/system_timer"
27
+ Bunny::SystemTimer
28
+ rescue LoadError
29
+ Timeout
30
+ end
31
+ else
32
+ Timeout
33
+ end
13
34
 
14
- class ConnectionError < StandardError; end
15
- class ForcedChannelCloseError < StandardError; end
16
- class ForcedConnectionCloseError < StandardError; end
17
- class MessageError < StandardError; end
18
- class ProtocolError < StandardError; end
19
- class ServerDownError < StandardError; end
20
- class UnsubscribeError < StandardError; end
21
- class AcknowledgementError < StandardError; end
22
35
 
23
- # Returns the Bunny version number
36
+ #
37
+ # API
38
+ #
24
39
 
25
40
  def self.version
26
41
  VERSION
27
42
  end
28
43
 
29
- # Print deprecation warning.
30
- def self.deprecation_warning(method, version, explanation)
31
- warn "~ #{method} will be removed in Bunny #{version}. #{explanation}"
44
+ def self.protocol_version
45
+ AMQ::Protocol::PROTOCOL_VERSION
32
46
  end
33
47
 
34
- # Instantiates new Bunny::Client
35
48
 
36
- def self.new(connection_string_or_opts = Hash.new, opts = Hash.new)
37
- # Set up Bunny
49
+ def self.new(connection_string_or_opts = {}, opts = {}, &block)
38
50
  if connection_string_or_opts.respond_to?(:keys) && opts.empty?
39
51
  opts = connection_string_or_opts
40
52
  end
41
53
 
42
- # Return client
43
- setup(connection_string_or_opts, opts)
54
+ conn = Session.new(connection_string_or_opts, opts)
55
+ @default_connection ||= conn
56
+
57
+ conn
44
58
  end
45
59
 
46
- # Runs a code block using a Bunny connection
60
+
47
61
  def self.run(connection_string_or_opts = {}, opts = {}, &block)
48
62
  raise ArgumentError, 'Bunny#run requires a block' unless block
49
63
 
50
- # Set up Bunny
51
- client = setup(connection_string_or_opts, opts)
64
+ client = Client.new(connection_string_or_opts, opts)
52
65
 
53
66
  begin
54
67
  client.start
@@ -57,35 +70,6 @@ module Bunny
57
70
  client.stop
58
71
  end
59
72
 
60
- # Return success
61
73
  :run_ok
62
74
  end
63
-
64
- Timer = if RUBY_VERSION < "1.9"
65
- begin
66
- require File.expand_path(File.join(File.dirname(__FILE__), 'system_timer.rb'))
67
- Bunny::SystemTimer
68
- rescue LoadError
69
- Timeout
70
- end
71
- else
72
- Timeout
73
- end
74
-
75
- private
76
-
77
- def self.setup(*args)
78
- # AMQP 0-9-1 specification
79
- require 'qrack/qrack'
80
- require 'bunny/client'
81
- require 'bunny/exchange'
82
- require 'bunny/queue'
83
- require 'bunny/channel'
84
- require 'bunny/subscription'
85
-
86
- include Qrack
87
-
88
- client = Bunny::Client.new(*args)
89
- end
90
-
91
75
  end
@@ -1,39 +1,635 @@
1
- # encoding: utf-8
1
+ require "thread"
2
+ require "amq/int_allocator"
3
+
4
+ require "bunny/consumer_work_pool"
5
+
6
+ require "bunny/exchange"
7
+ require "bunny/queue"
8
+ require "bunny/message_metadata"
2
9
 
3
10
  module Bunny
4
- class Channel < Qrack::Channel
11
+ class Channel
12
+
13
+ #
14
+ # API
15
+ #
16
+
17
+ attr_accessor :id, :connection, :status, :work_pool
18
+
19
+
20
+ def initialize(connection = nil, id = nil, work_pool = ConsumerWorkPool.new(1))
21
+ @connection = connection
22
+ @id = id || @connection.next_channel_id
23
+ @status = :opening
24
+
25
+ @connection.register_channel(self)
5
26
 
6
- def initialize(client)
7
- super
27
+ @queues = Hash.new
28
+ @exchanges = Hash.new
29
+ @consumers = Hash.new
30
+ @work_pool = work_pool
31
+
32
+ # synchronizes frameset delivery. MK.
33
+ @mutex = Mutex.new
34
+ @consumer_mutex = Mutex.new
35
+
36
+ @continuations = ::Queue.new
8
37
  end
9
38
 
39
+
10
40
  def open
11
- client.channel = self
12
- client.send_frame(Qrack::Protocol::Channel::Open.new)
41
+ @connection.open_channel(self)
42
+ @status = :open
43
+
44
+ self
45
+ end
46
+
47
+ def close
48
+ @connection.close_channel(self)
49
+ closed!
50
+ end
13
51
 
14
- method = client.next_method
52
+ def open?
53
+ @status == :open
54
+ end
15
55
 
16
- client.check_response(method, Qrack::Protocol::Channel::OpenOk, "Cannot open channel #{number}")
56
+ def closed?
57
+ @status == :closed
58
+ end
59
+
60
+ def queue(name = AMQ::Protocol::EMPTY_STRING, opts = {})
61
+ q = find_queue(name) || Bunny::Queue.new(self, name, opts)
17
62
 
18
- @active = true
19
- :open_ok
63
+ register_queue(q)
20
64
  end
21
65
 
22
- def close
23
- client.channel = self
24
- client.send_frame(Qrack::Protocol::Channel::Close.new(:reply_code => 200, :reply_text => 'bye', :method_id => 0, :class_id => 0))
25
66
 
26
- method = client.next_method
67
+ #
68
+ # Backwards compatibility with 0.8.0
69
+ #
27
70
 
28
- client.check_response(method, Qrack::Protocol::Channel::CloseOk, "Error closing channel #{number}")
71
+ def number
72
+ self.id
73
+ end
29
74
 
30
- @active = false
31
- :close_ok
75
+ def active
76
+ @active
32
77
  end
33
78
 
34
- def open?
35
- active
79
+ def client
80
+ @connection
81
+ end
82
+
83
+ def frame_size
84
+ @connection.frame_max
36
85
  end
37
86
 
87
+
88
+ #
89
+ # Higher-level API, similar to amqp gem
90
+ #
91
+
92
+ def fanout(name, opts = {})
93
+ Exchange.new(self, :fanout, name, opts)
94
+ end
95
+
96
+ def direct(name, opts = {})
97
+ Exchange.new(self, :direct, name, opts)
98
+ end
99
+
100
+ def topic(name, opts = {})
101
+ Exchange.new(self, :topic, name, opts)
102
+ end
103
+
104
+ def headers(name, opts = {})
105
+ Exchange.new(self, :headers, name, opts)
106
+ end
107
+
108
+ def default_exchange
109
+ self.direct("", :no_declare => true)
110
+ end
111
+
112
+ def prefetch(prefetch_count)
113
+ self.basic_qos(prefetch_count, false)
114
+ end
115
+
116
+ def flow(active)
117
+ channel_flow(active)
118
+ end
119
+
120
+ def recover(ignored = true)
121
+ # RabbitMQ only supports basic.recover with requeue = true
122
+ basic_recover(true)
123
+ end
124
+
125
+ def reject(delivery_tag, requeue = false)
126
+ basic_reject(delivery_tag, requeue)
127
+ end
128
+
129
+ def ack(delivery_tag, multiple)
130
+ basic_ack(delivery_tag, multiple)
131
+ end
132
+ alias acknowledge ack
133
+
134
+ def nack(delivery_tag, requeue, multiple = false)
135
+ basic_nack(delivery_tag, requeue, multiple)
136
+ end
137
+
138
+ def on_error(&block)
139
+ @default_error_handler = block
140
+ end
141
+
142
+
143
+ #
144
+ # Lower-level API, exposes protocol operations as they are defined in the protocol,
145
+ # without any OO sugar on top, by design.
146
+ #
147
+
148
+ # basic.*
149
+
150
+ def basic_publish(payload, exchange, routing_key, opts = {})
151
+ raise_if_no_longer_open!
152
+
153
+ exchange_name = if exchange.respond_to?(:name)
154
+ exchange.name
155
+ else
156
+ exchange
157
+ end
158
+
159
+ meta = { :priority => 0, :delivery_mode => 2, :content_type => "application/octet-stream" }.
160
+ merge(opts)
161
+ @connection.send_frameset(AMQ::Protocol::Basic::Publish.encode(@id, payload, meta, exchange_name, routing_key, meta[:mandatory], false, (frame_size || @connection.frame_max)), self)
162
+
163
+ self
164
+ end
165
+
166
+ def basic_get(queue, opts = {:ack => true})
167
+ raise_if_no_longer_open!
168
+
169
+ @connection.send_frame(AMQ::Protocol::Basic::Get.encode(@id, queue, !opts[:ack]))
170
+ @last_basic_get_response = @continuations.pop
171
+
172
+ raise_if_continuation_resulted_in_a_channel_error!
173
+ @last_basic_get_response
174
+ end
175
+
176
+ def basic_qos(prefetch_count, global = false)
177
+ raise ArgumentError.new("prefetch count must be a positive integer, given: #{prefetch_count}") if prefetch_count < 0
178
+ raise_if_no_longer_open!
179
+
180
+ @connection.send_frame(AMQ::Protocol::Basic::Qos.encode(@id, 0, prefetch_count, global))
181
+
182
+ Bunny::Timer.timeout(1, ClientTimeout) do
183
+ @last_basic_qos_ok = @continuations.pop
184
+ end
185
+ raise_if_continuation_resulted_in_a_channel_error!
186
+
187
+ @last_basic_qos_ok
188
+ end
189
+
190
+ def basic_recover(requeue)
191
+ raise_if_no_longer_open!
192
+
193
+ @connection.send_frame(AMQ::Protocol::Basic::Recover.encode(@id, requeue))
194
+ Bunny::Timer.timeout(1, ClientTimeout) do
195
+ @last_basic_recover_ok = @continuations.pop
196
+ end
197
+ raise_if_continuation_resulted_in_a_channel_error!
198
+
199
+ @last_basic_recover_ok
200
+ end
201
+
202
+ def basic_reject(delivery_tag, requeue)
203
+ raise_if_no_longer_open!
204
+ @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(@id, delivery_tag, requeue))
205
+
206
+ nil
207
+ end
208
+
209
+ def basic_ack(delivery_tag, multiple)
210
+ raise_if_no_longer_open!
211
+ @connection.send_frame(AMQ::Protocol::Basic::Ack.encode(@id, delivery_tag, multiple))
212
+
213
+ nil
214
+ end
215
+
216
+ def basic_nack(delivery_tag, requeue, multiple = false)
217
+ raise_if_no_longer_open!
218
+ @connection.send_frame(AMQ::Protocol::Basic::Nack.encode(@id, delivery_tag, requeue, multiple))
219
+
220
+ nil
221
+ end
222
+
223
+ def basic_consume(queue, consumer_tag = generate_consumer_tag, no_ack = false, exclusive = false, arguments = nil, &block)
224
+ raise_if_no_longer_open!
225
+ maybe_start_consumer_work_pool!
226
+
227
+ queue_name = if queue.respond_to?(:name)
228
+ queue.name
229
+ else
230
+ queue
231
+ end
232
+
233
+ @connection.send_frame(AMQ::Protocol::Basic::Consume.encode(@id, queue_name, consumer_tag, false, no_ack, exclusive, false, arguments))
234
+ Bunny::Timer.timeout(1, ClientTimeout) do
235
+ @last_basic_consume_ok = @continuations.pop
236
+ end
237
+
238
+ @consumer_mutex.synchronize do
239
+ c = Consumer.new(self, queue, consumer_tag, no_ack, exclusive, arguments)
240
+ c.on_delivery(&block) if block
241
+
242
+ @consumers[@last_basic_consume_ok.consumer_tag] = c
243
+ end
244
+
245
+ @last_basic_consume_ok
246
+ end
247
+
248
+ def basic_cancel(consumer_tag)
249
+ @connection.send_frame(AMQ::Protocol::Basic::Cancel.encode(@id, consumer_tag, false))
250
+
251
+ Bunny::Timer.timeout(1, ClientTimeout) do
252
+ @last_basic_cancel_ok = @continuations.pop
253
+ end
254
+
255
+ @last_basic_cancel_ok
256
+ end
257
+
258
+
259
+ # queue.*
260
+
261
+ def queue_declare(name, opts = {})
262
+ raise_if_no_longer_open!
263
+
264
+ @connection.send_frame(AMQ::Protocol::Queue::Declare.encode(@id, name, opts.fetch(:passive, false), opts.fetch(:durable, false), opts.fetch(:exclusive, false), opts.fetch(:auto_delete, false), false, opts[:arguments]))
265
+ @last_queue_declare_ok = @continuations.pop
266
+
267
+ raise_if_continuation_resulted_in_a_channel_error!
268
+
269
+ @last_queue_declare_ok
270
+ end
271
+
272
+ def queue_delete(name, opts = {})
273
+ raise_if_no_longer_open!
274
+
275
+ @connection.send_frame(AMQ::Protocol::Queue::Delete.encode(@id, name, opts[:if_unused], opts[:if_empty], false))
276
+ Bunny::Timer.timeout(1, ClientTimeout) do
277
+ @last_queue_delete_ok = @continuations.pop
278
+ end
279
+ raise_if_continuation_resulted_in_a_channel_error!
280
+
281
+ @last_queue_delete_ok
282
+ end
283
+
284
+ def queue_purge(name, opts = {})
285
+ raise_if_no_longer_open!
286
+
287
+ @connection.send_frame(AMQ::Protocol::Queue::Purge.encode(@id, name, false))
288
+
289
+ Bunny::Timer.timeout(1, ClientTimeout) do
290
+ @last_queue_purge_ok = @continuations.pop
291
+ end
292
+ raise_if_continuation_resulted_in_a_channel_error!
293
+
294
+ @last_queue_purge_ok
295
+ end
296
+
297
+ def queue_bind(name, exchange, opts = {})
298
+ raise_if_no_longer_open!
299
+
300
+ exchange_name = if exchange.respond_to?(:name)
301
+ exchange.name
302
+ else
303
+ exchange
304
+ end
305
+
306
+ @connection.send_frame(AMQ::Protocol::Queue::Bind.encode(@id, name, exchange_name, opts[:routing_key], false, opts[:arguments]))
307
+ Bunny::Timer.timeout(1, ClientTimeout) do
308
+ @last_queue_bind_ok = @continuations.pop
309
+ end
310
+
311
+ raise_if_continuation_resulted_in_a_channel_error!
312
+ @last_queue_bind_ok
313
+ end
314
+
315
+ def queue_unbind(name, exchange, opts = {})
316
+ raise_if_no_longer_open!
317
+
318
+ exchange_name = if exchange.respond_to?(:name)
319
+ exchange.name
320
+ else
321
+ exchange
322
+ end
323
+
324
+ @connection.send_frame(AMQ::Protocol::Queue::Unbind.encode(@id, name, exchange_name, opts[:routing_key], opts[:arguments]))
325
+ Bunny::Timer.timeout(1, ClientTimeout) do
326
+ @last_queue_unbind_ok = @continuations.pop
327
+ end
328
+
329
+ raise_if_continuation_resulted_in_a_channel_error!
330
+ @last_queue_unbind_ok
331
+ end
332
+
333
+
334
+ # exchange.*
335
+
336
+ def exchange_declare(name, type, opts = {})
337
+ raise_if_no_longer_open!
338
+
339
+ @connection.send_frame(AMQ::Protocol::Exchange::Declare.encode(@id, name, type.to_s, opts.fetch(:passive, false), opts.fetch(:durable, false), opts.fetch(:auto_delete, false), false, false, opts[:arguments]))
340
+ Bunny::Timer.timeout(1, ClientTimeout) do
341
+ @last_exchange_declare_ok = @continuations.pop
342
+ end
343
+
344
+ raise_if_continuation_resulted_in_a_channel_error!
345
+ @last_exchange_declare_ok
346
+ end
347
+
348
+ def exchange_delete(name, opts = {})
349
+ raise_if_no_longer_open!
350
+
351
+ @connection.send_frame(AMQ::Protocol::Exchange::Delete.encode(@id, name, opts[:if_unused], false))
352
+ Bunny::Timer.timeout(1, ClientTimeout) do
353
+ @last_exchange_delete_ok = @continuations.pop
354
+ end
355
+
356
+ raise_if_continuation_resulted_in_a_channel_error!
357
+ @last_exchange_delete_ok
358
+ end
359
+
360
+ def exchange_bind(source, destination, opts = {})
361
+ raise_if_no_longer_open!
362
+
363
+ source_name = if source.respond_to?(:name)
364
+ source.name
365
+ else
366
+ source
367
+ end
368
+
369
+ destination_name = if destination.respond_to?(:name)
370
+ destination.name
371
+ else
372
+ destination
373
+ end
374
+
375
+ @connection.send_frame(AMQ::Protocol::Exchange::Bind.encode(@id, destination_name, source_name, opts[:routing_key], false, opts[:arguments]))
376
+ Bunny::Timer.timeout(1, ClientTimeout) do
377
+ @last_exchange_bind_ok = @continuations.pop
378
+ end
379
+
380
+ raise_if_continuation_resulted_in_a_channel_error!
381
+ @last_exchange_bind_ok
382
+ end
383
+
384
+ def exchange_unbind(source, destination, opts = {})
385
+ raise_if_no_longer_open!
386
+
387
+ source_name = if source.respond_to?(:name)
388
+ source.name
389
+ else
390
+ source
391
+ end
392
+
393
+ destination_name = if destination.respond_to?(:name)
394
+ destination.name
395
+ else
396
+ destination
397
+ end
398
+
399
+ @connection.send_frame(AMQ::Protocol::Exchange::Unbind.encode(@id, destination_name, source_name, opts[:routing_key], false, opts[:arguments]))
400
+ Bunny::Timer.timeout(1, ClientTimeout) do
401
+ @last_exchange_unbind_ok = @continuations.pop
402
+ end
403
+
404
+ raise_if_continuation_resulted_in_a_channel_error!
405
+ @last_exchange_unbind_ok
406
+ end
407
+
408
+ # channel.*
409
+
410
+ def channel_flow(active)
411
+ raise_if_no_longer_open!
412
+
413
+ @connection.send_frame(AMQ::Protocol::Channel::Flow.encode(@id, active))
414
+ Bunny::Timer.timeout(1, ClientTimeout) do
415
+ @last_channel_flow_ok = @continuations.pop
416
+ end
417
+ raise_if_continuation_resulted_in_a_channel_error!
418
+
419
+ @last_channel_flow_ok
420
+ end
421
+
422
+ # tx.*
423
+
424
+ def tx_select
425
+ raise_if_no_longer_open!
426
+
427
+ @connection.send_frame(AMQ::Protocol::Tx::Select.encode(@id))
428
+ Bunny::Timer.timeout(1, ClientTimeout) do
429
+ @last_tx_select_ok = @continuations.pop
430
+ end
431
+ raise_if_continuation_resulted_in_a_channel_error!
432
+
433
+ @last_tx_select_ok
434
+ end
435
+
436
+ def tx_commit
437
+ raise_if_no_longer_open!
438
+
439
+ @connection.send_frame(AMQ::Protocol::Tx::Commit.encode(@id))
440
+ Bunny::Timer.timeout(1, ClientTimeout) do
441
+ @last_tx_commit_ok = @continuations.pop
442
+ end
443
+ raise_if_continuation_resulted_in_a_channel_error!
444
+
445
+ @last_tx_commit_ok
446
+ end
447
+
448
+ def tx_rollback
449
+ raise_if_no_longer_open!
450
+
451
+ @connection.send_frame(AMQ::Protocol::Tx::Rollback.encode(@id))
452
+ Bunny::Timer.timeout(1, ClientTimeout) do
453
+ @last_tx_rollback_ok = @continuations.pop
454
+ end
455
+ raise_if_continuation_resulted_in_a_channel_error!
456
+
457
+ @last_tx_rollback_ok
458
+ end
459
+
460
+ # confirm.*
461
+
462
+ def confirm_select
463
+ raise_if_no_longer_open!
464
+
465
+ @connection.send_frame(AMQ::Protocol::Confirm::Select.encode(@id, false))
466
+ Bunny::Timer.timeout(1, ClientTimeout) do
467
+ @last_confirm_select_ok = @continuations.pop
468
+ end
469
+ raise_if_continuation_resulted_in_a_channel_error!
470
+
471
+ @last_confirm_select_ok
472
+ end
473
+
474
+
475
+ #
476
+ # Implementation
477
+ #
478
+
479
+ def handle_method(method)
480
+ # puts "Channel#handle_frame on channel #{@id}: #{method.inspect}"
481
+ case method
482
+ when AMQ::Protocol::Queue::DeclareOk then
483
+ @continuations.push(method)
484
+ when AMQ::Protocol::Queue::DeleteOk then
485
+ @continuations.push(method)
486
+ when AMQ::Protocol::Queue::PurgeOk then
487
+ @continuations.push(method)
488
+ when AMQ::Protocol::Queue::BindOk then
489
+ @continuations.push(method)
490
+ when AMQ::Protocol::Queue::UnbindOk then
491
+ @continuations.push(method)
492
+ when AMQ::Protocol::Exchange::BindOk then
493
+ @continuations.push(method)
494
+ when AMQ::Protocol::Exchange::UnbindOk then
495
+ @continuations.push(method)
496
+ when AMQ::Protocol::Exchange::DeclareOk then
497
+ @continuations.push(method)
498
+ when AMQ::Protocol::Exchange::DeleteOk then
499
+ @continuations.push(method)
500
+ when AMQ::Protocol::Basic::QosOk then
501
+ @continuations.push(method)
502
+ when AMQ::Protocol::Basic::RecoverOk then
503
+ @continuations.push(method)
504
+ when AMQ::Protocol::Channel::FlowOk then
505
+ @continuations.push(method)
506
+ when AMQ::Protocol::Basic::ConsumeOk then
507
+ @continuations.push(method)
508
+ when AMQ::Protocol::Basic::CancelOk then
509
+ @continuations.push(method)
510
+ @consumers.delete(method.consumer_tag)
511
+ when AMQ::Protocol::Tx::SelectOk, AMQ::Protocol::Tx::CommitOk, AMQ::Protocol::Tx::RollbackOk then
512
+ @continuations.push(method)
513
+ when AMQ::Protocol::Tx::SelectOk then
514
+ @continuations.push(method)
515
+ when AMQ::Protocol::Confirm::SelectOk then
516
+ @continuations.push(method)
517
+ when AMQ::Protocol::Channel::Close then
518
+ # puts "Exception on channel #{@id}: #{method.reply_code} #{method.reply_text}"
519
+ closed!
520
+ @connection.send_frame(AMQ::Protocol::Channel::CloseOk.encode(@id))
521
+
522
+ @last_channel_error = instantiate_channel_level_exception(method)
523
+ @continuations.push(method)
524
+ when AMQ::Protocol::Channel::CloseOk then
525
+ @continuations.push(method)
526
+ else
527
+ raise "Do not know how to handle #{method.inspect} in Bunny::Channel#handle_method"
528
+ end
529
+ end
530
+
531
+ def handle_basic_get_ok(basic_get_ok, properties, content)
532
+ @continuations.push([basic_get_ok, properties, content])
533
+ end
534
+
535
+ def handle_basic_get_empty(basic_get_empty)
536
+ @continuations.push([nil, nil, nil])
537
+ end
538
+
539
+ def handle_frameset(basic_deliver, properties, content)
540
+ consumer = @consumers[basic_deliver.consumer_tag]
541
+ if consumer
542
+ @work_pool.submit do
543
+ consumer.call(MessageMetadata.new(basic_deliver, properties), content)
544
+ end
545
+ end
546
+ end
547
+
548
+ def handle_basic_return(basic_return, properties, content)
549
+ x = find_exchange(basic_return.exchange)
550
+
551
+ if x
552
+ x.handle_return(basic_return, properties, content)
553
+ else
554
+ # TODO: log a warning
555
+ end
556
+ end
557
+
558
+ # Starts consumer work pool. Lazily called by #basic_consume to avoid creating new threads
559
+ # that won't do any real work for channels that do not register consumers (e.g. only used for
560
+ # publishing). MK.
561
+ def maybe_start_consumer_work_pool!
562
+ @work_pool.start unless @work_pool.started?
563
+ end
564
+
565
+ def read_next_frame(options = {})
566
+ @connection.read_next_frame(options = {})
567
+ end
568
+
569
+ # Synchronizes given block using this channel's mutex.
570
+ # @api public
571
+ def synchronize(&block)
572
+ @mutex.synchronize(&block)
573
+ end
574
+
575
+ def register_queue(queue)
576
+ @queues[queue.name] = queue
577
+ end
578
+
579
+ def find_queue(name)
580
+ @queues[name]
581
+ end
582
+
583
+ def register_exchange(exchange)
584
+ @exchanges[exchange.name] = exchange
585
+ end
586
+
587
+ def find_exchange(name)
588
+ @exchanges[name]
589
+ end
590
+
591
+ protected
592
+
593
+ def closed!
594
+ @status = :closed
595
+ @work_pool.shutdown
596
+ @connection.release_channel_id(@id)
597
+ end
598
+
599
+ def instantiate_channel_level_exception(frame)
600
+ case frame
601
+ when AMQ::Protocol::Channel::Close then
602
+ klass = case frame.reply_code
603
+ when 403 then
604
+ AccessRefused
605
+ when 404 then
606
+ NotFound
607
+ when 405 then
608
+ ResourceLocked
609
+ when 406 then
610
+ PreconditionFailed
611
+ else
612
+ ChannelLevelException
613
+ end
614
+
615
+ klass.new(frame.reply_text, self, frame)
616
+ end
617
+ end
618
+
619
+ def raise_if_continuation_resulted_in_a_channel_error!
620
+ raise @last_channel_error if @last_channel_error
621
+ end
622
+
623
+ def raise_if_no_longer_open!
624
+ raise ChannelAlreadyClosed.new("cannot use a channel that was already closed! Channel id: #{@id}", self) if closed?
625
+ end
626
+
627
+ # Unique string supposed to be used as a consumer tag.
628
+ #
629
+ # @return [String] Unique string.
630
+ # @api plugin
631
+ def generate_consumer_tag(name = "bunny")
632
+ "#{name}-#{Time.now.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
633
+ end
38
634
  end
39
635
  end