bunny 0.8.0 → 0.9.0.pre1

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