amqp-client 0.2.3 → 1.0.2

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: 83f84f34aa2aacb3719e300483d4a874bc54892e12165b74f895a7503fd3c550
4
- data.tar.gz: b53ba2a5bdbb1620f839f2cca8d66c1ee3db13549360b04e51d018208c1f7b49
3
+ metadata.gz: 59f619f7aec324221f4c5dd66a10e5e30b1904a224d34702f94f9f75ac19f1c3
4
+ data.tar.gz: 4d3e3fc1234f44ab741b15b61156617a7f8f181ce5fdc2aacbbc4c07a787129e
5
5
  SHA512:
6
- metadata.gz: 62aa1fddfbc285ab34e52b3f0421c2a017e157a1e4e6d7c887da78f55c56bb4717a4466bf0ad1ee08a03b485dd55910d38a1b6809d35e373eda1eb248e4cdd33
7
- data.tar.gz: e60e191045e97a268faceb9e3a8112458aa920307e0bf29cdf0d24194c10b0c4254794ab335995019c7b3b41a2fafc383aaee1736584cd618684f6044d760f15
6
+ metadata.gz: 6ade93665b42b3c64ebed869be2af64cde53b259a6f376483c8c4d7e9de6432e2453be88caddf58655c249ae0d531a924348eb97c92b0a6a21b13a3276e3019b
7
+ data.tar.gz: '0479fe0b0c6f120e4bf0c5b72b905d7946037f8b5e1d83f462c18c2df790c8ed67ff5e32be693deac570d7d82dc9411875f252384364feea3b6e8b85c6abd27d'
@@ -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
data/.rubocop.yml CHANGED
@@ -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,45 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.2] - 2021-09-07
4
+
5
+ - Changed: Raise ConnectionClosed and ChannelClosed correctly (previous always ChannelClosed)
6
+ - Fixed: Respect Connection#blocked sent by the broker, will block all writes/requests
7
+
8
+ ## [1.0.1] - 2021-09-06
9
+
10
+ - The API is fully documented! https://cloudamqp.github.io/amqp-client.rb/
11
+ - Fixed: Socket writing is now thread-safe
12
+ - Change: Block while waiting for basic_cancel by default
13
+ - Added: Can specify channel_max, heartbeat and frame_max as options to the Client/Connection
14
+ - Added: Reuse channel 1 to declare high level queues/exchanges
15
+ - Fixed: Only wait for exchange_delete confirmation if not no_wait is set
16
+ - Fixed: Don't raise if Connection#close detects a closed socket (expected)
17
+
18
+ ## [1.0.0] - 2021-08-27
19
+
20
+ - Verify TLS certificate matches hostname
21
+ - TLS thread-safety
22
+ - Assemble Messages in the (single threaded) read_loop thread
23
+ - Give read_loop_thread higher priority so that channel errors crop up faster
24
+ - One less Thread required per Consumer
25
+ - Read exactly one frame at a time, not trying to split/assemble frames over socket reads
26
+ - Heafty speedup for message assembling with StringIO
27
+ - Channel#queue_declare returns a struct for nicer API (still backward compatible)
28
+ - AMQP::Client#publish_and_forget for fast, non confirmed publishes
29
+ - Allow Properties#timestamp to be an integer (in addition to Time)
30
+ - Bug fix allow Properties#expiration to be an Integer
31
+ - Consistent use of named parameters
32
+ - High level Exchange API
33
+ - Don't try to reconnect if first connect fails
34
+ - Bug fix: Close all channels when connection is closed by server
35
+ - Raise error if run out of channels
36
+ - Improved retry in high level client
37
+ - Bug fix: Support channel_max 0
38
+
39
+ ## [0.3.0] - 2021-08-20
40
+
41
+ - Channel#wait_for_confirms is a smarter way of waiting for publish confirms
42
+ - Default connection_name to $PROGRAM_NAME
3
43
 
4
44
  ## [0.2.3] - 2021-08-19
5
45
 
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # AMQP::Client
2
2
 
3
- An AMQP 0-9-1 client alternative, trying to keep things as simple as possible.
3
+ An AMQP 0-9-1 Ruby client, trying to keep things as simple as possible.
4
+
5
+ ## Documentation
6
+
7
+ [API reference](https://cloudamqp.github.io/amqp-client.rb/)
4
8
 
5
9
  ## Installation
6
10
 
@@ -34,7 +38,7 @@ msg = ch.basic_get q[:queue_name]
34
38
  puts msg.body
35
39
  ```
36
40
 
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).
41
+ 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 have extreme throughput, but expect 100% delivery guarantees (messages might be delivered twice, in the unlikely event of connection loss between message publish and message confirmation by the broker).
38
42
 
39
43
  ```ruby
40
44
  amqp = AMQP::Client.new("amqp://localhost")
@@ -46,8 +50,8 @@ q = amqp.queue("myqueue")
46
50
  # Bind the queue to any exchange, with any binding key
47
51
  q.bind("amq.topic", "my.events.*")
48
52
 
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.
53
+ # The message will be reprocessed if the client loses connection to the broker
54
+ # between message arrival and when the message was supposed to be ack'ed.
51
55
  q.subscribe(prefetch: 20) do |msg|
52
56
  process(JSON.parse(msg.body))
53
57
  msg.ack
@@ -3,305 +3,548 @@
3
3
  require_relative "./message"
4
4
 
5
5
  module AMQP
6
- # AMQP Channel
7
- class Channel
8
- def initialize(connection, id)
9
- @replies = ::Queue.new
10
- @connection = connection
11
- @id = id
12
- @consumers = {}
13
- @confirm = nil
14
- @last_confirmed = 0
15
- @closed = nil
16
- @on_return = nil
17
- @open = false
18
- 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
19
28
 
20
- 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
21
35
 
22
- def open
23
- return self if @open
36
+ # Channel ID
37
+ # @return [Integer]
38
+ attr_reader :id
24
39
 
25
- write_bytes FrameBytes.channel_open(@id)
26
- expect(:channel_open_ok)
27
- @open = true
28
- self
29
- end
40
+ # Open the channel (called from Connection)
41
+ # @return [Channel] self
42
+ # @api private
43
+ def open
44
+ return self if @open
30
45
 
31
- def close(reason = "", code = 200)
32
- return if @closed
46
+ @open = true
47
+ write_bytes FrameBytes.channel_open(@id)
48
+ expect(:channel_open_ok)
49
+ self
50
+ end
33
51
 
34
- write_bytes FrameBytes.channel_close(@id, reason, code)
35
- expect :channel_close_ok
36
- @closed = [code, reason]
37
- 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
38
66
 
39
- # Called when closed by server
40
- def closed!(code, reason, classid, methodid)
41
- write_bytes FrameBytes.channel_close_ok(@id)
42
- @closed = [code, reason, classid, methodid]
43
- @replies.close
44
- @consumers.each { |_, q| q.close }
45
- @consumers.clear
46
- 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
47
79
 
48
- def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, **args)
49
- write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, args)
50
- expect :exchange_declare_ok
51
- 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
52
87
 
53
- def exchange_delete(name, if_unused: false, no_wait: false)
54
- write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
55
- expect :exchange_delete_ok
56
- 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
57
105
 
58
- def exchange_bind(destination, source, binding_key, arguments = {})
59
- write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
60
- expect :exchange_bind_ok
61
- 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
62
116
 
63
- def exchange_unbind(destination, source, binding_key, arguments = {})
64
- write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
65
- expect :exchange_unbind_ok
66
- 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
67
128
 
68
- def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
69
- durable = false if name.empty?
70
- exclusive = true if name.empty?
71
- auto_delete = true if name.empty?
72
-
73
- write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
74
- name, message_count, consumer_count = expect(:queue_declare_ok)
75
- {
76
- queue_name: name,
77
- message_count: message_count,
78
- consumer_count: consumer_count
79
- }
80
- 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
81
140
 
82
- def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
83
- write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
84
- message_count, = expect :queue_delete
85
- message_count
86
- 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
87
173
 
88
- def queue_bind(name, exchange, binding_key, arguments = {})
89
- write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
90
- expect :queue_bind_ok
91
- 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
92
186
 
93
- def queue_purge(name, no_wait: false)
94
- write_bytes FrameBytes.queue_purge(@id, name, no_wait)
95
- expect :queue_purge_ok unless no_wait
96
- 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
97
198
 
98
- def queue_unbind(name, exchange, binding_key, arguments = {})
99
- write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
100
- expect :queue_unbind_ok
101
- 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
102
208
 
103
- def basic_get(queue_name, no_ack: true)
104
- write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
105
- frame, *rest = @replies.shift
106
- case frame
107
- when :basic_get_ok
108
- delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
109
- body_size, properties = expect(:header)
110
- pos = 0
111
- body = String.new("", capacity: body_size)
112
- while pos < body_size
113
- body_part, = expect(:body)
114
- body += body_part
115
- pos += body_part.bytesize
116
- end
117
- Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
118
- when :basic_get_empty then nil
119
- when nil then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
120
- else raise AMQP::Client::UnexpectedFrame.new(%i[basic_get_ok basic_get_empty], frame)
121
- end
122
- 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
+ end
123
219
 
124
- def basic_publish(body, exchange, routing_key, **properties)
125
- frame_max = @connection.frame_max - 8
126
- id = @id
220
+ # @!endgroup
221
+ # @!group Basic
222
+
223
+ # Get a message from a queue (by polling)
224
+ # @param queue_name [String]
225
+ # @param no_ack [Boolean] When false the message have to be manually acknowledged
226
+ # @return [Message] If the queue had a message
227
+ # @return [nil] If the queue doesn't have any messages
228
+ def basic_get(queue_name, no_ack: true)
229
+ write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
230
+ case (msg = @basic_gets.pop)
231
+ when Message then msg
232
+ when :basic_get_empty then nil
233
+ when nil then raise Error::Closed.new(@id, *@closed)
234
+ end
235
+ end
127
236
 
128
- if 0 < body.bytesize && body.bytesize <= frame_max
129
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
130
- FrameBytes.header(id, body.bytesize, properties),
131
- FrameBytes.body(id, body)
132
- return @confirm ? @confirm += 1 : nil
133
- end
237
+ # Publishes a message to an exchange
238
+ # @param body [String] The body, can be a string or a byte array
239
+ # @param exchange [String] Name of the exchange to publish to
240
+ # @param routing_key [String] The routing key that the exchange might use to route the message to a queue
241
+ # @param properties [Properties]
242
+ # @option properties [Boolean] mandatory The message will be returned if the message can't be routed to a queue
243
+ # @option properties [Boolean] persistent Same as delivery_mode: 2
244
+ # @option properties [String] content_type Content type of the message body
245
+ # @option properties [String] content_encoding Content encoding of the body
246
+ # @option properties [Hash<String, Object>] headers Custom headers
247
+ # @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
248
+ # @option properties [Integer] priority A priority of the message (between 0 and 255)
249
+ # @option properties [Integer] correlation_id A correlation id, most often used used for RPC communication
250
+ # @option properties [String] reply_to Queue to reply RPC responses to
251
+ # @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
252
+ # @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
253
+ # @option properties [Date] timestamp Often used for the time the message was originally generated
254
+ # @option properties [String] type Can indicate what kind of message this is
255
+ # @option properties [String] user_id Can be used to verify that this is the user that published the message
256
+ # @option properties [String] app_id Can be used to indicates which app that generated the message
257
+ # @return [nil]
258
+ def basic_publish(body, exchange, routing_key, **properties)
259
+ body_max = @connection.frame_max - 8
260
+ id = @id
261
+ mandatory = properties.delete(:mandatory) || false
262
+ case properties.delete(:persistent)
263
+ when true then properties[:delivery_mode] = 2
264
+ when false then properties[:delivery_mode] = 1
265
+ end
134
266
 
135
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
136
- FrameBytes.header(id, body.bytesize, properties)
137
- pos = 0
138
- while pos < body.bytesize # split body into multiple frame_max frames
139
- len = [frame_max, body.bytesize - pos].min
140
- body_part = body.byteslice(pos, len)
141
- write_bytes FrameBytes.body(id, body_part)
142
- pos += len
143
- end
144
- @confirm += 1 if @confirm
145
- end
267
+ if body.bytesize.between?(1, body_max)
268
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
269
+ FrameBytes.header(id, body.bytesize, properties),
270
+ FrameBytes.body(id, body)
271
+ @unconfirmed.push @confirm += 1 if @confirm
272
+ return
273
+ end
146
274
 
147
- def basic_publish_confirm(body, exchange, routing_key, **properties)
148
- confirm_select(no_wait: true)
149
- id = basic_publish(body, exchange, routing_key, **properties)
150
- wait_for_confirm(id)
151
- end
275
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
276
+ FrameBytes.header(id, body.bytesize, properties)
277
+ pos = 0
278
+ while pos < body.bytesize # split body into multiple frame_max frames
279
+ len = [body_max, body.bytesize - pos].min
280
+ body_part = body.byteslice(pos, len)
281
+ write_bytes FrameBytes.body(id, body_part)
282
+ pos += len
283
+ end
284
+ @unconfirmed.push @confirm += 1 if @confirm
285
+ nil
286
+ end
152
287
 
153
- # Consume from a queue
154
- # worker_threads: 0 => blocking, messages are executed in the thread calling this method
155
- def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {},
156
- worker_threads: 1)
157
- write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
158
- tag, = expect(:basic_consume_ok)
159
- q = @consumers[tag] = ::Queue.new
160
- msgs = ::Queue.new
161
- Thread.new { recv_deliveries(tag, q, msgs) }
162
- if worker_threads.zero?
163
- while (msg = msgs.shift)
164
- yield msg
165
- end
166
- else
167
- threads = Array.new(worker_threads) do
168
- Thread.new do
169
- while (msg = msgs.shift)
170
- yield(msg)
288
+ # Publish a message and block until the message has confirmed it has received it
289
+ # @param (see #basic_publish)
290
+ # @option (see #basic_publish)
291
+ # @return [Boolean] True if the message was successfully published
292
+ # @raise (see #basic_publish)
293
+ def basic_publish_confirm(body, exchange, routing_key, **properties)
294
+ confirm_select(no_wait: true)
295
+ basic_publish(body, exchange, routing_key, **properties)
296
+ wait_for_confirms
297
+ end
298
+
299
+ # Consume messages from a queue
300
+ # @param queue [String] Name of the queue to subscribe to
301
+ # @param tag [String] Custom consumer tag, will be auto assigned by the broker if empty.
302
+ # Has to be uniqe among this channel's consumers only
303
+ # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
304
+ # @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
305
+ # @param arguments [Hash] Custom arguments for the consumer
306
+ # @param worker_threads [Integer] Number of threads processing messages,
307
+ # 0 means that the thread calling this method will process the messages and thus this method will block
308
+ # @yield [Message] Delivered message from the queue
309
+ # @return [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
310
+ # @return [nil] When `worker_threads` is 0 the method will return when the consumer is cancelled
311
+ def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, worker_threads: 1)
312
+ write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
313
+ tag, = expect(:basic_consume_ok)
314
+ q = @consumers[tag] = ::Queue.new
315
+ if worker_threads.zero?
316
+ loop do
317
+ yield (q.pop || break)
318
+ end
319
+ nil
320
+ else
321
+ threads = Array.new(worker_threads) do
322
+ Thread.new do
323
+ loop do
324
+ yield (q.pop || break)
325
+ end
326
+ end
171
327
  end
328
+ [tag, threads]
172
329
  end
173
330
  end
174
- [tag, threads]
175
- end
176
- end
177
331
 
178
- def basic_cancel(consumer_tag, no_wait: false)
179
- consumer = @consumers.fetch(consumer_tag)
180
- return if consumer.closed?
332
+ # Cancel/abort/stop a consumer
333
+ # @param consumer_tag [String] Tag of the consumer to cancel
334
+ # @param no_wait [Boolean] Will wait for a confirmation from the broker that the consumer is cancelled
335
+ # @return [nil]
336
+ def basic_cancel(consumer_tag, no_wait: false)
337
+ consumer = @consumers.fetch(consumer_tag)
338
+ return if consumer.closed?
339
+
340
+ write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
341
+ expect(:basic_cancel_ok) unless no_wait
342
+ consumer.close
343
+ nil
344
+ end
181
345
 
182
- write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
183
- expect(:basic_cancel_ok) unless no_wait
184
- consumer.close
185
- end
346
+ # Specify how many messages to prefetch for consumers with `no_ack: false`
347
+ # @param prefetch_count [Integer] Number of messages to maxium keep in flight
348
+ # @param prefetch_size [Integer] Number of bytes to maxium keep in flight
349
+ # @param global [Boolean] If true the limit will apply to channel rather than the consumer
350
+ # @return [nil]
351
+ def basic_qos(prefetch_count, prefetch_size: 0, global: false)
352
+ write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
353
+ expect :basic_qos_ok
354
+ nil
355
+ end
186
356
 
187
- def basic_qos(prefetch_count, prefetch_size: 0, global: false)
188
- write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
189
- expect :basic_qos_ok
190
- end
357
+ # Acknowledge a message
358
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
359
+ # @return [nil]
360
+ def basic_ack(delivery_tag, multiple: false)
361
+ write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
362
+ nil
363
+ end
191
364
 
192
- def basic_ack(delivery_tag, multiple: false)
193
- write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
194
- end
365
+ # Negatively acknowledge a message
366
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
367
+ # @param multiple [Boolean] Nack all messages up to this message
368
+ # @param requeue [Boolean] Requeue the message
369
+ # @return [nil]
370
+ def basic_nack(delivery_tag, multiple: false, requeue: false)
371
+ write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
372
+ nil
373
+ end
195
374
 
196
- def basic_nack(delivery_tag, multiple: false, requeue: false)
197
- write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
198
- end
375
+ # Reject a message
376
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
377
+ # @param requeue [Boolean] Requeue the message into the queue again
378
+ # @return [nil]
379
+ def basic_reject(delivery_tag, requeue: false)
380
+ write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
381
+ nil
382
+ end
199
383
 
200
- def basic_reject(delivery_tag, requeue: false)
201
- write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
202
- end
384
+ # Recover all the unacknowledge messages
385
+ # @param requeue [Boolean] If false the currently unack:ed messages will be deliviered to this consumer again,
386
+ # if true to any consumer
387
+ # @return [nil]
388
+ def basic_recover(requeue: false)
389
+ write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
390
+ expect :basic_recover_ok
391
+ nil
392
+ end
203
393
 
204
- def basic_recover(requeue: false)
205
- write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
206
- expect :basic_recover_ok
207
- end
394
+ # @!endgroup
395
+ # @!group Confirm
208
396
 
209
- def confirm_select(no_wait: false)
210
- return if @confirm
397
+ # Put the channel in confirm mode, each published message will then be confirmed by the broker
398
+ # @param no_wait [Boolean] If false the method will block until the broker has confirmed the request
399
+ # @return [nil]
400
+ def confirm_select(no_wait: false)
401
+ return if @confirm
211
402
 
212
- write_bytes FrameBytes.confirm_select(@id, no_wait)
213
- expect :confirm_select_ok unless no_wait
214
- @confirms = ::Queue.new
215
- @confirm = 0
216
- end
403
+ write_bytes FrameBytes.confirm_select(@id, no_wait)
404
+ expect :confirm_select_ok unless no_wait
405
+ @confirm = 0
406
+ nil
407
+ end
217
408
 
218
- def wait_for_confirm(id)
219
- raise ArgumentError, "Confirm id has to a positive number" unless id&.positive?
220
- return true if @last_confirmed >= id
409
+ # Block until all publishes messages are confirmed
410
+ # @return [Boolean] True if all message where positivly acknowledged, false if not
411
+ def wait_for_confirms
412
+ return true if @unconfirmed.empty?
221
413
 
222
- loop do
223
- ack, delivery_tag, multiple = @confirms.shift || break
224
- @last_confirmed = delivery_tag
225
- return ack if delivery_tag == id || (delivery_tag > id && multiple)
226
- end
227
- false
228
- end
414
+ case @unconfirmed_empty.pop
415
+ when true then true
416
+ when false then false
417
+ else raise Error::Closed.new(@id, *@closed)
418
+ end
419
+ end
229
420
 
230
- def tx_select
231
- write_bytes FrameBytes.tx_select(@id)
232
- expect :tx_select_ok
233
- end
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?
234
435
 
235
- def tx_commit
236
- write_bytes FrameBytes.tx_commit(@id)
237
- expect :tx_commit_ok
238
- end
436
+ @unconfirmed_empty.num_waiting.times do
437
+ @unconfirmed_empty << (ack_or_nack == :ack)
438
+ end
439
+ end
239
440
 
240
- def tx_rollback
241
- write_bytes FrameBytes.tx_rollback(@id)
242
- expect :tx_rollback_ok
243
- end
441
+ # @!endgroup
442
+ # @!group Transaction
244
443
 
245
- def reply(args)
246
- @replies.push(args)
247
- end
444
+ # Put the channel in transaction mode, make sure that you #tx_commit or #tx_rollback after publish
445
+ # @return [nil]
446
+ def tx_select
447
+ write_bytes FrameBytes.tx_select(@id)
448
+ expect :tx_select_ok
449
+ nil
450
+ end
248
451
 
249
- def confirm(args)
250
- @confirms.push(args)
251
- end
452
+ # Commmit a transaction, requires that the channel is in transaction mode
453
+ # @return [nil]
454
+ def tx_commit
455
+ write_bytes FrameBytes.tx_commit(@id)
456
+ expect :tx_commit_ok
457
+ nil
458
+ end
252
459
 
253
- def message_returned(reply_code, reply_text, exchange, routing_key)
254
- Thread.new do
255
- body_size, properties = expect(:header)
256
- body = String.new("", capacity: body_size)
257
- while body.bytesize < body_size
258
- body_part, = expect(:body)
259
- body += body_part
460
+ # Rollback a transaction, requires that the channel is in transaction mode
461
+ # @return [nil]
462
+ def tx_rollback
463
+ write_bytes FrameBytes.tx_rollback(@id)
464
+ expect :tx_rollback_ok
465
+ nil
260
466
  end
261
- msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
262
467
 
263
- if @on_return
264
- @on_return.call(msg)
265
- else
266
- puts "[WARN] Message returned: #{msg.inspect}"
468
+ # @!endgroup
469
+
470
+ # @api private
471
+ def reply(args)
472
+ @replies.push(args)
267
473
  end
268
- end
269
- end
270
474
 
271
- def on_return(&block)
272
- @on_return = block
273
- end
475
+ # @api private
476
+ def message_returned(reply_code, reply_text, exchange, routing_key)
477
+ @next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, nil, "")
478
+ end
479
+
480
+ # @api private
481
+ def message_delivered(consumer_tag, delivery_tag, redelivered, exchange, routing_key)
482
+ @next_msg = Message.new(self, delivery_tag, exchange, routing_key, nil, "", redelivered, consumer_tag)
483
+ end
274
484
 
275
- private
485
+ # @api private
486
+ def basic_get_empty
487
+ @basic_gets.push :basic_get_empty
488
+ end
276
489
 
277
- def recv_deliveries(consumer_tag, deliver_queue, msgs)
278
- loop do
279
- _, delivery_tag, redelivered, exchange, routing_key = deliver_queue.shift || raise(ClosedQueueError)
280
- body_size, properties = expect(:header)
281
- body = String.new("", capacity: body_size)
282
- while body.bytesize < body_size
283
- body_part, = expect(:body)
284
- body += body_part
490
+ # @api private
491
+ def header_delivered(body_size, properties)
492
+ @next_msg.properties = properties
493
+ if body_size.zero?
494
+ next_message_finished!
495
+ else
496
+ @next_body = StringIO.new(String.new(capacity: body_size))
497
+ @next_body_size = body_size
498
+ end
285
499
  end
286
- msgs.push Message.new(self, delivery_tag, exchange, routing_key, properties, body, redelivered, consumer_tag)
287
- end
288
- ensure
289
- msgs.close
290
- end
291
500
 
292
- def write_bytes(*bytes)
293
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if @closed
501
+ # @api private
502
+ def body_delivered(body_part)
503
+ @next_body.write(body_part)
504
+ return unless @next_body.pos == @next_body_size
294
505
 
295
- @connection.write_bytes(*bytes)
296
- end
506
+ @next_msg.body = @next_body.string
507
+ next_message_finished!
508
+ end
509
+
510
+ # @api private
511
+ def close_consumer(tag)
512
+ @consumers.fetch(tag).close
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
+ ensure
532
+ @next_msg = @next_body = @next_body_size = nil
533
+ end
534
+
535
+ def write_bytes(*bytes)
536
+ raise Error::Closed.new(@id, *@closed) if @closed
297
537
 
298
- def expect(expected_frame_type)
299
- loop do
300
- frame_type, *args = @replies.shift
301
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
302
- return args if frame_type == expected_frame_type
538
+ @connection.write_bytes(*bytes)
539
+ end
540
+
541
+ def expect(expected_frame_type)
542
+ frame_type, *args = @replies.pop
543
+ raise Error::Closed.new(@id, *@closed) if frame_type.nil?
544
+ raise Error::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
303
545
 
304
- @replies.push [frame_type, *args]
546
+ args
547
+ end
305
548
  end
306
549
  end
307
550
  end