amqp-client 0.3.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30006ba97b26d7e73cf9503841e2248744db3d9e86d1a32fe4c9abecd9ee69f7
4
- data.tar.gz: a59d667d971da49c2657e57efe385853adbef958fd191a01f0d1f4dbaaa1b8a8
3
+ metadata.gz: ec3129a38420f19de4c0225aea1eaefc320463c6e032c54990a998f7654f8347
4
+ data.tar.gz: 3c8da5fccb818730958f47fdaf78837524381b116a322adaf585ee02d0492b2d
5
5
  SHA512:
6
- metadata.gz: 7ddb8f409abf9e773a6551ff53629b702e8324ea7ff794f6b14f09ef9dd7cb27a752e853bb312a8d9da88b826a7714e84006d5bb01b5efeef7bbaed56f14323a
7
- data.tar.gz: 642e332c0a031d27d7f364b66e0ae5beefef4bad402336f24052eef2679e306eec219369d7b8b2c1d8324eabafd0c40d45aa8feb474c3cd11ddc4578203f5bea
6
+ metadata.gz: 24b592bb7fc50f29499e32ca6348f2aa25bac9837e694740346fa24e1fe6c45c9f9ab6497f066ca6491c6f09e558b29ac3a1d84f5b31a3b2b68bb8c28cdf76e7
7
+ data.tar.gz: 6ad3e059d311894f4a023d75df89c72beb4508a2abc83083a47f4f7a07cc6f98edc157e06b5ed42eda55458612d89206de1cce4b0cc94fd598facad4433083e9
@@ -0,0 +1,25 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ docs:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v2
13
+ - name: Setup Ruby
14
+ uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: 3.0
17
+ - name: Install yard
18
+ run: gem install yard
19
+ - name: Generate docs
20
+ run: yard doc
21
+ - name: Deploy docs
22
+ uses: JamesIves/github-pages-deploy-action@4.1.5
23
+ with:
24
+ branch: gh-pages
25
+ folder: doc
@@ -19,7 +19,7 @@ jobs:
19
19
  strategy:
20
20
  fail-fast: false
21
21
  matrix:
22
- ruby: ['2.7', '3.0']
22
+ ruby: ['2.6', '2.7', '3.0']
23
23
  steps:
24
24
  - uses: actions/checkout@v2
25
25
  - name: Set up Ruby
@@ -32,4 +32,3 @@ jobs:
32
32
  bundle exec rake
33
33
  env:
34
34
  AMQP_PORT: ${{ job.services.rabbitmq.ports[5672] }}
35
-
data/.rubocop.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  inherit_from: .rubocop_todo.yml
2
2
 
3
3
  AllCops:
4
- TargetRubyVersion: 2.5
4
+ TargetRubyVersion: 2.6
5
5
 
6
6
  Style/StringLiterals:
7
7
  Enabled: true
@@ -12,4 +12,15 @@ Style/StringLiteralsInInterpolation:
12
12
  EnforcedStyle: double_quotes
13
13
 
14
14
  Layout/LineLength:
15
- Max: 120
15
+ Max: 130
16
+
17
+ Naming/FileName:
18
+ Exclude:
19
+ - 'lib/amqp-client.rb'
20
+
21
+ Metrics/PerceivedComplexity:
22
+ Exclude:
23
+ - 'lib/amqp/client/properties.rb'
24
+
25
+ Metrics/ParameterLists:
26
+ Max: 8
data/.rubocop_todo.yml CHANGED
@@ -1,92 +1,48 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2021-06-01 23:54:39 UTC using RuboCop version 1.15.0.
3
+ # on 2021-09-06 19:39:47 UTC using RuboCop version 1.19.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 1
10
- # Cop supports --auto-correct.
11
- # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
12
- # URISchemes: http, https
13
- Layout/LineLength:
14
- Max: 123
15
-
16
- # Offense count: 15
9
+ # Offense count: 18
17
10
  # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
18
11
  Metrics/AbcSize:
19
- Max: 142
12
+ Max: 179
20
13
 
21
14
  # Offense count: 1
22
15
  # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
23
16
  # IgnoredMethods: refine
24
17
  Metrics/BlockLength:
25
- Max: 37
18
+ Max: 40
26
19
 
27
- # Offense count: 2
20
+ # Offense count: 3
28
21
  # Configuration parameters: CountBlocks.
29
22
  Metrics/BlockNesting:
30
23
  Max: 4
31
24
 
32
- # Offense count: 2
25
+ # Offense count: 5
33
26
  # Configuration parameters: CountComments, CountAsOne.
34
27
  Metrics/ClassLength:
35
- Max: 191
28
+ Max: 400
36
29
 
37
30
  # Offense count: 9
38
31
  # Configuration parameters: IgnoredMethods.
39
32
  Metrics/CyclomaticComplexity:
40
- Max: 30
33
+ Max: 44
41
34
 
42
- # Offense count: 26
35
+ # Offense count: 40
43
36
  # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
44
37
  Metrics/MethodLength:
45
- Max: 124
38
+ Max: 170
46
39
 
47
- # Offense count: 3
40
+ # Offense count: 2
48
41
  # Configuration parameters: CountComments, CountAsOne.
49
42
  Metrics/ModuleLength:
50
- Max: 300
51
-
52
- # Offense count: 5
53
- # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
54
- Metrics/ParameterLists:
55
- Max: 7
43
+ Max: 500
56
44
 
57
- # Offense count: 7
45
+ # Offense count: 4
58
46
  # Configuration parameters: IgnoredMethods.
59
47
  Metrics/PerceivedComplexity:
60
- Max: 18
61
-
62
- # Offense count: 1
63
- # Cop supports --auto-correct.
64
- # Configuration parameters: EnforcedStyle, IgnoredMethods.
65
- # SupportedStyles: predicate, comparison
66
- Style/NumericPredicate:
67
- Exclude:
68
- - 'spec/**/*'
69
- - 'lib/amqp/client/channel.rb'
70
-
71
- # Offense count: 1
72
- # Cop supports --auto-correct.
73
- # Configuration parameters: EnforcedStyle.
74
- # SupportedStyles: implicit, explicit
75
- Style/RescueStandardError:
76
- Exclude:
77
- - 'lib/amqp/client.rb'
78
-
79
- # Offense count: 1
80
- # Cop supports --auto-correct.
81
- # Configuration parameters: EnforcedStyle.
82
- # SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only
83
- Style/YodaCondition:
84
- Exclude:
85
- - 'lib/amqp/client/channel.rb'
86
-
87
- # Offense count: 1
88
- # Cop supports --auto-correct.
89
- # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
90
- # URISchemes: http, https
91
- Layout/LineLength:
92
- Max: 123
48
+ Max: 22
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-api - LICENSE.txt CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2021-09-08
4
+
5
+ - Fixed: Due to a race condition publishers could get stuck waiting for publish confirms
6
+ - Change: Message, ReturnMessage and Properties are now classes and not structs (for performance reasons)
7
+ - Added: Ruby 2.6 support
8
+ - Added: RBS signatures in sig/amqp-client.rbs
9
+
10
+ ## [1.0.2] - 2021-09-07
11
+
12
+ - Changed: Raise ConnectionClosed and ChannelClosed correctly (previous always ChannelClosed)
13
+ - Fixed: Respect Connection#blocked sent by the broker, will block all writes/requests
14
+
15
+ ## [1.0.1] - 2021-09-06
16
+
17
+ - The API is fully documented! https://cloudamqp.github.io/amqp-client.rb/
18
+ - Fixed: Socket writing is now thread-safe
19
+ - Change: Block while waiting for basic_cancel by default
20
+ - Added: Can specify channel_max, heartbeat and frame_max as options to the Client/Connection
21
+ - Added: Reuse channel 1 to declare high level queues/exchanges
22
+ - Fixed: Only wait for exchange_delete confirmation if not no_wait is set
23
+ - Fixed: Don't raise if Connection#close detects a closed socket (expected)
24
+
25
+ ## [1.0.0] - 2021-08-27
26
+
27
+ - Verify TLS certificate matches hostname
28
+ - TLS thread-safety
29
+ - Assemble Messages in the (single threaded) read_loop thread
30
+ - Give read_loop_thread higher priority so that channel errors crop up faster
31
+ - One less Thread required per Consumer
32
+ - Read exactly one frame at a time, not trying to split/assemble frames over socket reads
33
+ - Heafty speedup for message assembling with StringIO
34
+ - Channel#queue_declare returns a struct for nicer API (still backward compatible)
35
+ - AMQP::Client#publish_and_forget for fast, non confirmed publishes
36
+ - Allow Properties#timestamp to be an integer (in addition to Time)
37
+ - Bug fix allow Properties#expiration to be an Integer
38
+ - Consistent use of named parameters
39
+ - High level Exchange API
40
+ - Don't try to reconnect if first connect fails
41
+ - Bug fix: Close all channels when connection is closed by server
42
+ - Raise error if run out of channels
43
+ - Improved retry in high level client
44
+ - Bug fix: Support channel_max 0
45
+
3
46
  ## [0.3.0] - 2021-08-20
4
47
 
5
48
  - Channel#wait_for_confirms is a smarter way of waiting for publish confirms
data/Gemfile CHANGED
@@ -10,3 +10,7 @@ gem "rake", "~> 13.0"
10
10
  gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "rubocop", "~> 1.7"
13
+
14
+ gem "yard", require: false
15
+
16
+ gem "rubocop-minitest", require: false
data/README.md CHANGED
@@ -1,54 +1,63 @@
1
1
  # AMQP::Client
2
2
 
3
- An AMQP 0-9-1 client alternative, trying to keep things as simple as possible.
3
+ A modern AMQP 0-9-1 Ruby client. Very fast (just as fast as the Java client, and >4x than other Ruby clients), fully thread-safe, blocking operations and straight-forward error handling.
4
4
 
5
- ## Installation
5
+ ## Support
6
6
 
7
- Add this line to your application's Gemfile:
7
+ The library is fully supported by [CloudAMQP](https://www.cloudamqp.com), the largest RabbitMQ hosting provider in the world. Open [an issue](https://github.com/cloudamqp/amqp-client.rb/issues) or [email our support](mailto:support@cloudamqp.com) if you have problems or questions.
8
8
 
9
- ```ruby
10
- gem 'amqp-client'
11
- ```
9
+ ## Documentation
12
10
 
13
- And then execute:
11
+ [API reference](https://cloudamqp.github.io/amqp-client.rb/)
14
12
 
15
- $ bundle install
13
+ ## Usage
16
14
 
17
- Or install it yourself as:
15
+ The client has two APIs.
18
16
 
19
- $ gem install amqp-client
17
+ ### Low level API
20
18
 
21
- ## Usage
22
-
23
- Low level API
19
+ This API matches the AMQP protocol very well, it can do everything the protocol allows, but requires some knowledge about the protocol, and doesn't handle reconnects.
24
20
 
25
21
  ```ruby
26
22
  require "amqp-client"
27
23
 
28
- c = AMQP::Client.new("amqp://guest:guest@localhost")
29
- conn = c.connect
24
+ # Opens and establishes a connection
25
+ conn = AMQP::Client.new("amqp://guest:guest@localhost").connect
26
+
27
+ # Open a channel
30
28
  ch = conn.channel
29
+
30
+ # Create a temporary queue
31
31
  q = ch.queue_declare
32
- ch.basic_publish "Hello World!", "", q[:queue_name]
33
- msg = ch.basic_get q[:queue_name]
32
+
33
+ # Publish a message to said queue
34
+ ch.basic_publish "Hello World!", "", q.queue_name
35
+
36
+ # Poll the queue for a message
37
+ msg = ch.basic_get q.queue_name
38
+
39
+ # Print the message's body to STDOUT
34
40
  puts msg.body
35
41
  ```
36
42
 
37
- High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to be extreme throughput, be expect 100% delivery guarantees (messages might be deliviered twice, in the unlikely event of a connection loss between message publish and message confirmed by the server).
43
+ ### High level API
44
+
45
+ The library provides a high-level API that is a bit easier to get started with, and also handles reconnection automatically.
38
46
 
39
47
  ```ruby
40
- amqp = AMQP::Client.new("amqp://localhost")
41
- amqp.start
48
+ # Start the client, it will connect and once connected it will reconnect if that connection is lost
49
+ # Operation pending when the connection is lost will raise an exception (not timeout)
50
+ amqp = AMQP::Client.new("amqp://localhost").start
42
51
 
43
52
  # Declares a durable queue
44
- q = amqp.queue("myqueue")
53
+ myqueue = amqp.queue("myqueue")
45
54
 
46
55
  # Bind the queue to any exchange, with any binding key
47
- q.bind("amq.topic", "my.events.*")
56
+ myqueue.bind("amq.topic", "my.events.*")
48
57
 
49
- # The message will be reprocessed if the client lost connection to the server
50
- # between the message arrived and the message was supposed to be ack:ed.
51
- q.subscribe(prefetch: 20) do |msg|
58
+ # The message will be reprocessed if the client loses connection to the broker
59
+ # between message arrival and when the message was supposed to be ack'ed.
60
+ myqueue.subscribe(prefetch: 20) do |msg|
52
61
  process(JSON.parse(msg.body))
53
62
  msg.ack
54
63
  rescue
@@ -56,13 +65,37 @@ rescue
56
65
  end
57
66
 
58
67
  # Publish directly to the queue
59
- q.publish { foo: "bar" }.to_json, content_type: "application/json"
68
+ myqueue.publish({ foo: "bar" }.to_json, content_type: "application/json")
60
69
 
61
70
  # Publish to any exchange
62
71
  amqp.publish("my message", "amq.topic", "topic.foo", headers: { foo: 'bar' })
63
72
  amqp.publish(Zlib.gzip("an event"), "amq.topic", "my.event", content_encoding: 'gzip')
64
73
  ```
65
74
 
75
+ ## Supported Ruby versions
76
+
77
+ All maintained Ruby versions are supported.
78
+
79
+ - 3.0
80
+ - 2.7
81
+ - 2.6
82
+
83
+ ## Installation
84
+
85
+ Add this line to your application's Gemfile:
86
+
87
+ ```ruby
88
+ gem 'amqp-client'
89
+ ```
90
+
91
+ And then execute:
92
+
93
+ $ bundle install
94
+
95
+ Or install it yourself as:
96
+
97
+ $ gem install amqp-client
98
+
66
99
  ## Development
67
100
 
68
101
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -11,6 +11,8 @@ end
11
11
 
12
12
  require "rubocop/rake_task"
13
13
 
14
- RuboCop::RakeTask.new
14
+ RuboCop::RakeTask.new do |task|
15
+ task.requires << "rubocop-minitest"
16
+ end
15
17
 
16
18
  task default: %i[test rubocop]
data/amqp-client.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Work in progress"
13
13
  spec.homepage = "https://github.com/cloudamqp/amqp-client.rb"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
@@ -3,317 +3,549 @@
3
3
  require_relative "./message"
4
4
 
5
5
  module AMQP
6
- # AMQP Channel
7
- class Channel
8
- def initialize(connection, id)
9
- @connection = connection
10
- @id = id
11
- @replies = ::Queue.new
12
- @consumers = {}
13
- @closed = nil
14
- @open = false
15
- @on_return = nil
16
- @confirm = nil
17
- @unconfirmed = ::Queue.new
18
- @unconfirmed_empty = ::Queue.new
19
- end
6
+ class Client
7
+ class Connection
8
+ # AMQP Channel
9
+ class Channel
10
+ # Should only be called from Connection
11
+ # @param connection [Connection] The connection this channel belongs to
12
+ # @param id [Integer] ID of the channel
13
+ # @see Connection#channel
14
+ # @api private
15
+ def initialize(connection, id)
16
+ @connection = connection
17
+ @id = id
18
+ @replies = ::Queue.new
19
+ @consumers = {}
20
+ @closed = nil
21
+ @open = false
22
+ @on_return = nil
23
+ @confirm = nil
24
+ @unconfirmed = ::Queue.new
25
+ @unconfirmed_empty = ::Queue.new
26
+ @basic_gets = ::Queue.new
27
+ end
20
28
 
21
- attr_reader :id, :consumers
29
+ # Override #inspect
30
+ # @api private
31
+ def inspect
32
+ "#<#{self.class} @id=#{@id} @open=#{@open} @closed=#{@closed} confirm_selected=#{!@confirm.nil?}"\
33
+ " consumer_count=#{@consumers.size} replies_count=#{@replies.size} unconfirmed_count=#{@unconfirmed.size}>"
34
+ end
22
35
 
23
- def open
24
- return self if @open
36
+ # Channel ID
37
+ # @return [Integer]
38
+ attr_reader :id
25
39
 
26
- write_bytes FrameBytes.channel_open(@id)
27
- expect(:channel_open_ok)
28
- @open = true
29
- self
30
- end
40
+ # Open the channel (called from Connection)
41
+ # @return [Channel] self
42
+ # @api private
43
+ def open
44
+ return self if @open
31
45
 
32
- def close(reason = "", code = 200)
33
- return if @closed
46
+ @open = true
47
+ write_bytes FrameBytes.channel_open(@id)
48
+ expect(:channel_open_ok)
49
+ self
50
+ end
34
51
 
35
- write_bytes FrameBytes.channel_close(@id, reason, code)
36
- expect :channel_close_ok
37
- @closed = [code, reason]
38
- end
52
+ # Gracefully close a connection
53
+ # @return [nil]
54
+ def close(reason: "", code: 200)
55
+ return if @closed
56
+
57
+ write_bytes FrameBytes.channel_close(@id, reason, code)
58
+ @closed = [:channel, code, reason]
59
+ expect :channel_close_ok
60
+ @replies.close
61
+ @basic_gets.close
62
+ @unconfirmed_empty.close
63
+ @consumers.each_value(&:close)
64
+ nil
65
+ end
39
66
 
40
- # Called when closed by server
41
- def closed!(code, reason, classid, methodid)
42
- write_bytes FrameBytes.channel_close_ok(@id)
43
- @closed = [code, reason, classid, methodid]
44
- @replies.close
45
- @consumers.each { |_, q| q.close }
46
- @consumers.clear
47
- end
67
+ # Called when channel is closed by broker
68
+ # @param level [Symbol] :connection or :channel
69
+ # @return [nil]
70
+ # @api private
71
+ def closed!(level, code, reason, classid, methodid)
72
+ @closed = [level, code, reason, classid, methodid]
73
+ @replies.close
74
+ @basic_gets.close
75
+ @unconfirmed_empty.close
76
+ @consumers.each_value(&:close)
77
+ nil
78
+ end
48
79
 
49
- def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, **args)
50
- write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, args)
51
- expect :exchange_declare_ok
52
- end
80
+ # Handle returned messages in this block. If not set the message will just be logged to STDERR
81
+ # @yield [ReturnMessage] Messages returned by the broker when a publish has failed
82
+ # @return nil
83
+ def on_return(&block)
84
+ @on_return = block
85
+ nil
86
+ end
53
87
 
54
- def exchange_delete(name, if_unused: false, no_wait: false)
55
- write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
56
- expect :exchange_delete_ok
57
- end
88
+ # @!group Exchange
89
+
90
+ # Declare an exchange
91
+ # @param name [String] Name of the exchange
92
+ # @param type [String] Type of exchange (amq.direct, amq.fanout, amq.topic, amq.headers, etc.)
93
+ # @param passive [Boolean] If true raise an exception if the exchange doesn't already exists
94
+ # @param durable [Boolean] If true the exchange will persist between broker restarts,
95
+ # also a requirement for persistent messages
96
+ # @param auto_delete [Boolean] If true the exchange will be deleted when the last queue/exchange is unbound
97
+ # @param internal [Boolean] If true the exchange can't be published to directly
98
+ # @param arguments [Hash] Custom arguments
99
+ # @return [nil]
100
+ def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
101
+ write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, arguments)
102
+ expect :exchange_declare_ok
103
+ nil
104
+ end
58
105
 
59
- def exchange_bind(destination, source, binding_key, arguments = {})
60
- write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
61
- expect :exchange_bind_ok
62
- end
106
+ # Delete an exchange
107
+ # @param name [String] Name of the exchange
108
+ # @param if_unused [Boolean] If true raise an exception if queues/exchanges is bound to this exchange
109
+ # @param no_wait [Boolean] If true don't wait for a broker confirmation
110
+ # @return [nil]
111
+ def exchange_delete(name, if_unused: false, no_wait: false)
112
+ write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
113
+ expect :exchange_delete_ok unless no_wait
114
+ nil
115
+ end
63
116
 
64
- def exchange_unbind(destination, source, binding_key, arguments = {})
65
- write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
66
- expect :exchange_unbind_ok
67
- end
117
+ # Bind an exchange to another exchange
118
+ # @param destination [String] Name of the exchange to bind
119
+ # @param source [String] Name of the exchange to bind to
120
+ # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
121
+ # @param arguments [Hash] Message headers to match on, but only when bound to header exchanges
122
+ # @return [nil]
123
+ def exchange_bind(destination, source, binding_key, arguments: {})
124
+ write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
125
+ expect :exchange_bind_ok
126
+ nil
127
+ end
68
128
 
69
- def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
70
- durable = false if name.empty?
71
- exclusive = true if name.empty?
72
- auto_delete = true if name.empty?
73
-
74
- write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
75
- name, message_count, consumer_count = expect(:queue_declare_ok)
76
- {
77
- queue_name: name,
78
- message_count: message_count,
79
- consumer_count: consumer_count
80
- }
81
- end
129
+ # Unbind an exchange from another exchange
130
+ # @param destination [String] Name of the exchange to unbind
131
+ # @param source [String] Name of the exchange to unbind from
132
+ # @param binding_key [String] Binding key which the queue is bound to the exchange with
133
+ # @param arguments [Hash] Arguments matching the binding that's being removed
134
+ # @return [nil]
135
+ def exchange_unbind(destination, source, binding_key, arguments: {})
136
+ write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
137
+ expect :exchange_unbind_ok
138
+ nil
139
+ end
82
140
 
83
- def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
84
- write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
85
- message_count, = expect :queue_delete
86
- message_count
87
- end
141
+ # @!endgroup
142
+ # @!group Queue
143
+
144
+ # Response when declaring a Queue
145
+ # @!attribute queue_name
146
+ # @return [String] The name of the queue
147
+ # @!attribute message_count
148
+ # @return [Integer] Number of messages in the queue at the time of declaration
149
+ # @!attribute consumer_count
150
+ # @return [Integer] Number of consumers subscribed to the queue at the time of declaration
151
+ QueueOk = Struct.new(:queue_name, :message_count, :consumer_count)
152
+
153
+ # Create a queue (operation is idempotent)
154
+ # @param name [String] Name of the queue, can be empty, but will then be generated by the broker
155
+ # @param passive [Boolean] If true an exception will be raised if the queue doesn't already exists
156
+ # @param durable [Boolean] If true the queue will survive broker restarts,
157
+ # messages in the queue will only survive if they are published as persistent
158
+ # @param exclusive [Boolean] If true the queue will be deleted when the channel is closed
159
+ # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
160
+ # (it won't be deleted until at least one consumer has consumed from it)
161
+ # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
162
+ # @return [QueueOk] The QueueOk struct got `queue_name`, `message_count` and `consumer_count` properties
163
+ def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
164
+ durable = false if name.empty?
165
+ exclusive = true if name.empty?
166
+ auto_delete = true if name.empty?
167
+
168
+ write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
169
+ name, message_count, consumer_count = expect(:queue_declare_ok)
170
+
171
+ QueueOk.new(name, message_count, consumer_count)
172
+ end
88
173
 
89
- def queue_bind(name, exchange, binding_key, arguments = {})
90
- write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
91
- expect :queue_bind_ok
92
- end
174
+ # Delete a queue
175
+ # @param name [String] Name of the queue
176
+ # @param if_unused [Boolean] Only delete if the queue doesn't have consumers, raises a ChannelClosed error otherwise
177
+ # @param if_empty [Boolean] Only delete if the queue is empty, raises a ChannelClosed error otherwise
178
+ # @param no_wait [Boolean] Don't wait for a broker confirmation if true
179
+ # @return [Integer] Number of messages in queue when deleted
180
+ # @return [nil] If no_wait was set true
181
+ def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
182
+ write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
183
+ message_count, = expect :queue_delete unless no_wait
184
+ message_count
185
+ end
93
186
 
94
- def queue_purge(name, no_wait: false)
95
- write_bytes FrameBytes.queue_purge(@id, name, no_wait)
96
- expect :queue_purge_ok unless no_wait
97
- end
187
+ # Bind a queue to an exchange
188
+ # @param name [String] Name of the queue to bind
189
+ # @param exchange [String] Name of the exchange to bind to
190
+ # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
191
+ # @param arguments [Hash] Message headers to match on, but only when bound to header exchanges
192
+ # @return [nil]
193
+ def queue_bind(name, exchange, binding_key, arguments: {})
194
+ write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
195
+ expect :queue_bind_ok
196
+ nil
197
+ end
98
198
 
99
- def queue_unbind(name, exchange, binding_key, arguments = {})
100
- write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
101
- expect :queue_unbind_ok
102
- end
199
+ # Purge a queue
200
+ # @param name [String] Name of the queue
201
+ # @param no_wait [Boolean] Don't wait for a broker confirmation if true
202
+ # @return [nil]
203
+ def queue_purge(name, no_wait: false)
204
+ write_bytes FrameBytes.queue_purge(@id, name, no_wait)
205
+ expect :queue_purge_ok unless no_wait
206
+ nil
207
+ end
103
208
 
104
- def basic_get(queue_name, no_ack: true)
105
- write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
106
- frame, *rest = @replies.shift
107
- case frame
108
- when :basic_get_ok
109
- delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
110
- body_size, properties = expect(:header)
111
- pos = 0
112
- body = String.new("", capacity: body_size)
113
- while pos < body_size
114
- body_part, = expect(:body)
115
- body += body_part
116
- pos += body_part.bytesize
117
- end
118
- Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
119
- when :basic_get_empty then nil
120
- when nil then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
121
- else raise AMQP::Client::UnexpectedFrame.new(%i[basic_get_ok basic_get_empty], frame)
122
- end
123
- end
209
+ # Unbind a queue from an exchange
210
+ # @param name [String] Name of the queue to unbind
211
+ # @param exchange [String] Name of the exchange to unbind from
212
+ # @param binding_key [String] Binding key which the queue is bound to the exchange with
213
+ # @param arguments [Hash] Arguments matching the binding that's being removed
214
+ # @return [nil]
215
+ def queue_unbind(name, exchange, binding_key, arguments: {})
216
+ write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
217
+ expect :queue_unbind_ok
218
+ nil
219
+ end
124
220
 
125
- def basic_publish(body, exchange, routing_key, **properties)
126
- frame_max = @connection.frame_max - 8
127
- id = @id
128
- mandatory = properties.delete(:mandatory) || false
129
-
130
- if 0 < body.bytesize && body.bytesize <= frame_max
131
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
132
- FrameBytes.header(id, body.bytesize, properties),
133
- FrameBytes.body(id, body)
134
- @unconfirmed.push @confirm += 1 if @confirm
135
- return
136
- end
221
+ # @!endgroup
222
+ # @!group Basic
223
+
224
+ # Get a message from a queue (by polling)
225
+ # @param queue_name [String]
226
+ # @param no_ack [Boolean] When false the message have to be manually acknowledged
227
+ # @return [Message] If the queue had a message
228
+ # @return [nil] If the queue doesn't have any messages
229
+ def basic_get(queue_name, no_ack: true)
230
+ write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
231
+ case (msg = @basic_gets.pop)
232
+ when Message then msg
233
+ when :basic_get_empty then nil
234
+ when nil then raise Error::Closed.new(@id, *@closed)
235
+ end
236
+ end
137
237
 
138
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
139
- FrameBytes.header(id, body.bytesize, properties)
140
- pos = 0
141
- while pos < body.bytesize # split body into multiple frame_max frames
142
- len = [frame_max, body.bytesize - pos].min
143
- body_part = body.byteslice(pos, len)
144
- write_bytes FrameBytes.body(id, body_part)
145
- pos += len
146
- end
147
- @unconfirmed.push @confirm += 1 if @confirm
148
- nil
149
- end
238
+ # Publishes a message to an exchange
239
+ # @param body [String] The body, can be a string or a byte array
240
+ # @param exchange [String] Name of the exchange to publish to
241
+ # @param routing_key [String] The routing key that the exchange might use to route the message to a queue
242
+ # @param properties [Properties]
243
+ # @option properties [Boolean] mandatory The message will be returned if the message can't be routed to a queue
244
+ # @option properties [Boolean] persistent Same as delivery_mode: 2
245
+ # @option properties [String] content_type Content type of the message body
246
+ # @option properties [String] content_encoding Content encoding of the body
247
+ # @option properties [Hash<String, Object>] headers Custom headers
248
+ # @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
249
+ # @option properties [Integer] priority A priority of the message (between 0 and 255)
250
+ # @option properties [Integer] correlation_id A correlation id, most often used used for RPC communication
251
+ # @option properties [String] reply_to Queue to reply RPC responses to
252
+ # @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
253
+ # @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
254
+ # @option properties [Date] timestamp Often used for the time the message was originally generated
255
+ # @option properties [String] type Can indicate what kind of message this is
256
+ # @option properties [String] user_id Can be used to verify that this is the user that published the message
257
+ # @option properties [String] app_id Can be used to indicates which app that generated the message
258
+ # @return [nil]
259
+ def basic_publish(body, exchange, routing_key, **properties)
260
+ body_max = @connection.frame_max - 8
261
+ id = @id
262
+ mandatory = properties.delete(:mandatory) || false
263
+ case properties.delete(:persistent)
264
+ when true then properties[:delivery_mode] = 2
265
+ when false then properties[:delivery_mode] = 1
266
+ end
150
267
 
151
- def basic_publish_confirm(body, exchange, routing_key, **properties)
152
- confirm_select(no_wait: true)
153
- basic_publish(body, exchange, routing_key, **properties)
154
- wait_for_confirms
155
- end
268
+ if body.bytesize.between?(1, body_max)
269
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
270
+ FrameBytes.header(id, body.bytesize, properties),
271
+ FrameBytes.body(id, body)
272
+ @unconfirmed.push @confirm += 1 if @confirm
273
+ return
274
+ end
275
+
276
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
277
+ FrameBytes.header(id, body.bytesize, properties)
278
+ pos = 0
279
+ while pos < body.bytesize # split body into multiple frame_max frames
280
+ len = [body_max, body.bytesize - pos].min
281
+ body_part = body.byteslice(pos, len)
282
+ write_bytes FrameBytes.body(id, body_part)
283
+ pos += len
284
+ end
285
+ @unconfirmed.push @confirm += 1 if @confirm
286
+ nil
287
+ end
288
+
289
+ # Publish a message and block until the message has confirmed it has received it
290
+ # @param (see #basic_publish)
291
+ # @option (see #basic_publish)
292
+ # @return [Boolean] True if the message was successfully published
293
+ # @raise (see #basic_publish)
294
+ def basic_publish_confirm(body, exchange, routing_key, **properties)
295
+ confirm_select(no_wait: true)
296
+ basic_publish(body, exchange, routing_key, **properties)
297
+ wait_for_confirms
298
+ end
156
299
 
157
- # Consume from a queue
158
- # worker_threads: 0 => blocking, messages are executed in the thread calling this method
159
- def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {},
160
- worker_threads: 1)
161
- write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
162
- tag, = expect(:basic_consume_ok)
163
- q = @consumers[tag] = ::Queue.new
164
- msgs = ::Queue.new
165
- Thread.new { recv_deliveries(tag, q, msgs) }
166
- if worker_threads.zero?
167
- while (msg = msgs.shift)
168
- yield msg
169
- end
170
- else
171
- threads = Array.new(worker_threads) do
172
- Thread.new do
173
- while (msg = msgs.shift)
174
- yield(msg)
300
+ # Consume messages from a queue
301
+ # @param queue [String] Name of the queue to subscribe to
302
+ # @param tag [String] Custom consumer tag, will be auto assigned by the broker if empty.
303
+ # Has to be uniqe among this channel's consumers only
304
+ # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
305
+ # @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
306
+ # @param arguments [Hash] Custom arguments for the consumer
307
+ # @param worker_threads [Integer] Number of threads processing messages,
308
+ # 0 means that the thread calling this method will process the messages and thus this method will block
309
+ # @yield [Message] Delivered message from the queue
310
+ # @return [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
311
+ # @return [nil] When `worker_threads` is 0 the method will return when the consumer is cancelled
312
+ def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, worker_threads: 1)
313
+ write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
314
+ tag, = expect(:basic_consume_ok)
315
+ q = @consumers[tag] = ::Queue.new
316
+ if worker_threads.zero?
317
+ loop do
318
+ yield (q.pop || break)
319
+ end
320
+ nil
321
+ else
322
+ threads = Array.new(worker_threads) do
323
+ Thread.new do
324
+ loop do
325
+ yield (q.pop || break)
326
+ end
327
+ end
175
328
  end
329
+ [tag, threads]
176
330
  end
177
331
  end
178
- [tag, threads]
179
- end
180
- end
181
332
 
182
- def basic_cancel(consumer_tag, no_wait: false)
183
- consumer = @consumers.fetch(consumer_tag)
184
- return if consumer.closed?
333
+ # Cancel/abort/stop a consumer
334
+ # @param consumer_tag [String] Tag of the consumer to cancel
335
+ # @param no_wait [Boolean] Will wait for a confirmation from the broker that the consumer is cancelled
336
+ # @return [nil]
337
+ def basic_cancel(consumer_tag, no_wait: false)
338
+ consumer = @consumers.fetch(consumer_tag)
339
+ return if consumer.closed?
340
+
341
+ write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
342
+ expect(:basic_cancel_ok) unless no_wait
343
+ consumer.close
344
+ nil
345
+ end
185
346
 
186
- write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
187
- expect(:basic_cancel_ok) unless no_wait
188
- consumer.close
189
- end
347
+ # Specify how many messages to prefetch for consumers with `no_ack: false`
348
+ # @param prefetch_count [Integer] Number of messages to maxium keep in flight
349
+ # @param prefetch_size [Integer] Number of bytes to maxium keep in flight
350
+ # @param global [Boolean] If true the limit will apply to channel rather than the consumer
351
+ # @return [nil]
352
+ def basic_qos(prefetch_count, prefetch_size: 0, global: false)
353
+ write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
354
+ expect :basic_qos_ok
355
+ nil
356
+ end
190
357
 
191
- def basic_qos(prefetch_count, prefetch_size: 0, global: false)
192
- write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
193
- expect :basic_qos_ok
194
- end
358
+ # Acknowledge a message
359
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
360
+ # @return [nil]
361
+ def basic_ack(delivery_tag, multiple: false)
362
+ write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
363
+ nil
364
+ end
195
365
 
196
- def basic_ack(delivery_tag, multiple: false)
197
- write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
198
- end
366
+ # Negatively acknowledge a message
367
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
368
+ # @param multiple [Boolean] Nack all messages up to this message
369
+ # @param requeue [Boolean] Requeue the message
370
+ # @return [nil]
371
+ def basic_nack(delivery_tag, multiple: false, requeue: false)
372
+ write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
373
+ nil
374
+ end
199
375
 
200
- def basic_nack(delivery_tag, multiple: false, requeue: false)
201
- write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
202
- end
376
+ # Reject a message
377
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
378
+ # @param requeue [Boolean] Requeue the message into the queue again
379
+ # @return [nil]
380
+ def basic_reject(delivery_tag, requeue: false)
381
+ write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
382
+ nil
383
+ end
203
384
 
204
- def basic_reject(delivery_tag, requeue: false)
205
- write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
206
- end
385
+ # Recover all the unacknowledge messages
386
+ # @param requeue [Boolean] If false the currently unack:ed messages will be deliviered to this consumer again,
387
+ # if true to any consumer
388
+ # @return [nil]
389
+ def basic_recover(requeue: false)
390
+ write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
391
+ expect :basic_recover_ok
392
+ nil
393
+ end
207
394
 
208
- def basic_recover(requeue: false)
209
- write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
210
- expect :basic_recover_ok
211
- end
395
+ # @!endgroup
396
+ # @!group Confirm
212
397
 
213
- def confirm_select(no_wait: false)
214
- return if @confirm
398
+ # Put the channel in confirm mode, each published message will then be confirmed by the broker
399
+ # @param no_wait [Boolean] If false the method will block until the broker has confirmed the request
400
+ # @return [nil]
401
+ def confirm_select(no_wait: false)
402
+ return if @confirm
215
403
 
216
- write_bytes FrameBytes.confirm_select(@id, no_wait)
217
- expect :confirm_select_ok unless no_wait
218
- @confirm = 0
219
- end
404
+ write_bytes FrameBytes.confirm_select(@id, no_wait)
405
+ expect :confirm_select_ok unless no_wait
406
+ @confirm = 0
407
+ nil
408
+ end
220
409
 
221
- # Block until all publishes messages are confirmed
222
- def wait_for_confirms
223
- return true if @unconfirmed.empty?
410
+ # Block until all publishes messages are confirmed
411
+ # @return [Boolean] True if all message where positivly acknowledged, false if not
412
+ def wait_for_confirms
413
+ return true if @unconfirmed.empty?
224
414
 
225
- @unconfirmed_empty.pop
226
- end
415
+ ok = @unconfirmed_empty.pop
416
+ raise Error::Closed.new(@id, *@closed) if ok.nil?
227
417
 
228
- def confirm(args)
229
- ack_or_nack, delivery_tag, multiple = *args
230
- loop do
231
- tag = @unconfirmed.pop(true)
232
- break if tag == delivery_tag
233
- next if multiple && tag < delivery_tag
418
+ ok
419
+ end
234
420
 
235
- @unconfirmed << tag # requeue
236
- rescue ThreadError
237
- break
238
- end
239
- return unless @unconfirmed.empty?
421
+ # Called by Connection when received ack/nack from broker
422
+ # @api private
423
+ def confirm(args)
424
+ ack_or_nack, delivery_tag, multiple = *args
425
+ loop do
426
+ tag = @unconfirmed.pop(true)
427
+ break if tag == delivery_tag
428
+ next if multiple && tag < delivery_tag
429
+
430
+ @unconfirmed << tag # requeue
431
+ rescue ThreadError
432
+ break
433
+ end
434
+ return unless @unconfirmed.empty?
240
435
 
241
- @unconfirmed_empty.num_waiting.times do
242
- @unconfirmed_empty << ack_or_nack == :ack
243
- end
244
- end
436
+ ok = ack_or_nack == :ack
437
+ @unconfirmed_empty.push(ok) until @unconfirmed_empty.num_waiting.zero?
438
+ end
245
439
 
246
- def tx_select
247
- write_bytes FrameBytes.tx_select(@id)
248
- expect :tx_select_ok
249
- end
440
+ # @!endgroup
441
+ # @!group Transaction
250
442
 
251
- def tx_commit
252
- write_bytes FrameBytes.tx_commit(@id)
253
- expect :tx_commit_ok
254
- end
443
+ # Put the channel in transaction mode, make sure that you #tx_commit or #tx_rollback after publish
444
+ # @return [nil]
445
+ def tx_select
446
+ write_bytes FrameBytes.tx_select(@id)
447
+ expect :tx_select_ok
448
+ nil
449
+ end
255
450
 
256
- def tx_rollback
257
- write_bytes FrameBytes.tx_rollback(@id)
258
- expect :tx_rollback_ok
259
- end
451
+ # Commmit a transaction, requires that the channel is in transaction mode
452
+ # @return [nil]
453
+ def tx_commit
454
+ write_bytes FrameBytes.tx_commit(@id)
455
+ expect :tx_commit_ok
456
+ nil
457
+ end
260
458
 
261
- def reply(args)
262
- @replies.push(args)
263
- end
459
+ # Rollback a transaction, requires that the channel is in transaction mode
460
+ # @return [nil]
461
+ def tx_rollback
462
+ write_bytes FrameBytes.tx_rollback(@id)
463
+ expect :tx_rollback_ok
464
+ nil
465
+ end
466
+
467
+ # @!endgroup
264
468
 
265
- def message_returned(reply_code, reply_text, exchange, routing_key)
266
- Thread.new do
267
- body_size, properties = expect(:header)
268
- body = String.new("", capacity: body_size)
269
- while body.bytesize < body_size
270
- body_part, = expect(:body)
271
- body += body_part
469
+ # @api private
470
+ def reply(args)
471
+ @replies.push(args)
272
472
  end
273
- msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
274
473
 
275
- if @on_return
276
- @on_return.call(msg)
277
- else
278
- puts "[WARN] Message returned: #{msg.inspect}"
474
+ # @api private
475
+ def message_returned(reply_code, reply_text, exchange, routing_key)
476
+ @next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key)
279
477
  end
280
- end
281
- end
282
478
 
283
- def on_return(&block)
284
- @on_return = block
285
- end
479
+ # @api private
480
+ def message_delivered(consumer_tag, delivery_tag, redelivered, exchange, routing_key)
481
+ @next_msg = Message.new(self, consumer_tag, delivery_tag, exchange, routing_key, redelivered)
482
+ end
286
483
 
287
- private
484
+ # @api private
485
+ def basic_get_empty
486
+ @basic_gets.push :basic_get_empty
487
+ end
288
488
 
289
- def recv_deliveries(consumer_tag, deliver_queue, msgs)
290
- loop do
291
- _, delivery_tag, redelivered, exchange, routing_key = deliver_queue.shift || raise(ClosedQueueError)
292
- body_size, properties = expect(:header)
293
- body = String.new("", capacity: body_size)
294
- while body.bytesize < body_size
295
- body_part, = expect(:body)
296
- body += body_part
489
+ # @api private
490
+ def header_delivered(body_size, properties)
491
+ @next_msg.properties = properties
492
+ if body_size.zero?
493
+ next_message_finished!
494
+ else
495
+ @next_body = StringIO.new(String.new(capacity: body_size))
496
+ @next_body_size = body_size
497
+ end
297
498
  end
298
- msgs.push Message.new(self, delivery_tag, exchange, routing_key, properties, body, redelivered, consumer_tag)
299
- end
300
- ensure
301
- msgs.close
302
- end
303
499
 
304
- def write_bytes(*bytes)
305
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if @closed
500
+ # @api private
501
+ def body_delivered(body_part)
502
+ @next_body.write(body_part)
503
+ return unless @next_body.pos == @next_body_size
306
504
 
307
- @connection.write_bytes(*bytes)
308
- end
505
+ @next_msg.body = @next_body.string
506
+ next_message_finished!
507
+ end
508
+
509
+ # @api private
510
+ def close_consumer(tag)
511
+ @consumers.fetch(tag).close
512
+ nil
513
+ end
514
+
515
+ private
516
+
517
+ def next_message_finished!
518
+ next_msg = @next_msg
519
+ if next_msg.is_a? ReturnMessage
520
+ if @on_return
521
+ Thread.new { @on_return.call(next_msg) }
522
+ else
523
+ warn "AMQP-Client message returned: #{msg.inspect}"
524
+ end
525
+ elsif next_msg.consumer_tag.nil?
526
+ @basic_gets.push next_msg
527
+ else
528
+ Thread.pass until (consumer = @consumers[next_msg.consumer_tag])
529
+ consumer.push next_msg
530
+ end
531
+ nil
532
+ ensure
533
+ @next_msg = @next_body = @next_body_size = nil
534
+ end
535
+
536
+ def write_bytes(*bytes)
537
+ raise Error::Closed.new(@id, *@closed) if @closed
309
538
 
310
- def expect(expected_frame_type)
311
- loop do
312
- frame_type, *args = @replies.shift
313
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
314
- return args if frame_type == expected_frame_type
539
+ @connection.write_bytes(*bytes)
540
+ end
541
+
542
+ def expect(expected_frame_type)
543
+ frame_type, *args = @replies.pop
544
+ raise Error::Closed.new(@id, *@closed) if frame_type.nil?
545
+ raise Error::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
315
546
 
316
- @replies.push [frame_type, *args]
547
+ args
548
+ end
317
549
  end
318
550
  end
319
551
  end