amqp-client 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e6dd3fef130c286e97eadd9bb6d3d4de0fa67599361604cec3560879b9b4bdf
4
- data.tar.gz: c2ce00f897c68d3085427854b08cc98d3487b40bb5327ce38bb216c98357e366
3
+ metadata.gz: 89ea2743bb81c4615d1445ee6ce25a5234a95f9f859ca4926ff5437228af1e02
4
+ data.tar.gz: 4739cd429d9cf55a631c36c9100f78c6d2da65d4aa18535297d69b8ec0ca6e56
5
5
  SHA512:
6
- metadata.gz: 9b62e8ee74ec542be5617b08fbd0e36e5fc16294f99cd0ec61781c7c7425005c426e8358a74abf45a1a24ecb88451b94ef50ee7524403ac7097748b2e94b55b5
7
- data.tar.gz: 5c3e63369578923e20b441fbe9728b374e18150c75ed155291b802c42f80f668b704b39e80b144b90346a8d769d9b71f031b9fca712e6ac52c100a88e46380e3
6
+ metadata.gz: 12d2650fbf4be1f3d1449c4c035e8e12be88a64f3e4eeb2f4588558d294816e36510f53cb37f8217e2754ca3fb6b1acb021df25b63f34c7710f267de6186123c
7
+ data.tar.gz: 4b44974681cacc7956c73983cebd61315684c35adb5d98851469eaa7677e4a5149cc582cfeb3ec6561f1a036b0b80d7623197e8e1881400f6e00fff1305a4362
@@ -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,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.1] - 2021-09-06
4
+
5
+ - The API is fully documented! https://cloudamqp.github.io/amqp-client.rb/
6
+ - Fixed: Socket writing is now thread-safe
7
+ - Change: Block while waiting for basic_cancel by default
8
+ - Added: Can specify channel_max, heartbeat and frame_max as options to the Client/Connection
9
+ - Added: Reuse channel 1 to declare high level queues/exchanges
10
+ - Fixed: Only wait for exchange_delete confirmation if not no_wait is set
11
+ - Fixed: Don't raise if Connection#close detects a closed socket (expected)
12
+
3
13
  ## [1.0.0] - 2021-08-27
4
14
 
5
15
  - Verify TLS certificate matches hostname
data/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  An AMQP 0-9-1 Ruby client, trying to keep things as simple as possible.
4
4
 
5
+ ## Documentation
6
+
7
+ [API reference](https://cloudamqp.github.io/amqp-client.rb/)
8
+
5
9
  ## Installation
6
10
 
7
11
  Add this line to your application's Gemfile:
@@ -3,338 +3,543 @@
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
- @basic_gets = ::Queue.new
20
- end
21
-
22
- def inspect
23
- "#<#{self.class} @id=#{@id} @open=#{@open} @closed=#{@closed} confirm_selected=#{!@confirm.nil?}"\
24
- " consumer_count=#{@consumers.size} replies_count=#{@replies.size} unconfirmed_count=#{@unconfirmed.size}>"
25
- end
26
-
27
- attr_reader :id
28
-
29
- def open
30
- return self if @open
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
31
28
 
32
- @open = true
33
- write_bytes FrameBytes.channel_open(@id)
34
- expect(:channel_open_ok)
35
- self
36
- end
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
37
35
 
38
- def close(reason = "", code = 200)
39
- return if @closed
36
+ # Channel ID
37
+ # @return [Integer]
38
+ attr_reader :id
40
39
 
41
- write_bytes FrameBytes.channel_close(@id, reason, code)
42
- @closed = [code, reason]
43
- expect :channel_close_ok
44
- @replies.close
45
- @basic_gets.close
46
- @unconfirmed_empty.close
47
- @consumers.each_value(&:close)
48
- end
40
+ # Open the channel (called from Connection)
41
+ # @return [Channel] self
42
+ # @api private
43
+ def open
44
+ return self if @open
49
45
 
50
- # Called when channel is closed by server
51
- def closed!(code, reason, classid, methodid)
52
- @closed = [code, reason, classid, methodid]
53
- @replies.close
54
- @basic_gets.close
55
- @unconfirmed_empty.close
56
- @consumers.each_value(&:close)
57
- end
46
+ @open = true
47
+ write_bytes FrameBytes.channel_open(@id)
48
+ expect(:channel_open_ok)
49
+ self
50
+ end
58
51
 
59
- def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
60
- write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, arguments)
61
- expect :exchange_declare_ok
62
- 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 = [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
63
66
 
64
- def exchange_delete(name, if_unused: false, no_wait: false)
65
- write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
66
- expect :exchange_delete_ok
67
- end
67
+ # Called when channel is closed by server
68
+ # @return [nil]
69
+ # @api private
70
+ def closed!(code, reason, classid, methodid)
71
+ @closed = [code, reason, classid, methodid]
72
+ @replies.close
73
+ @basic_gets.close
74
+ @unconfirmed_empty.close
75
+ @consumers.each_value(&:close)
76
+ nil
77
+ end
68
78
 
69
- def exchange_bind(destination, source, binding_key, arguments: {})
70
- write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
71
- expect :exchange_bind_ok
72
- end
79
+ # Handle returned messages in this block. If not set the message will just be logged to STDERR
80
+ # @yield [ReturnMessage] Messages returned by the server when a publish has failed
81
+ # @return nil
82
+ def on_return(&block)
83
+ @on_return = block
84
+ nil
85
+ end
73
86
 
74
- def exchange_unbind(destination, source, binding_key, arguments: {})
75
- write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
76
- expect :exchange_unbind_ok
77
- end
87
+ # @!group Exchange
88
+
89
+ # Declare an exchange
90
+ # @param name [String] Name of the exchange
91
+ # @param type [String] Type of exchange (amq.direct, amq.fanout, amq.topic, amq.headers, etc.)
92
+ # @param passive [Boolean] If true raise an exception if the exchange doesn't already exists
93
+ # @param durable [Boolean] If true the exchange will persist between broker restarts,
94
+ # also a requirement for persistent messages
95
+ # @param auto_delete [Boolean] If true the exchange will be deleted when the last queue/exchange is unbound
96
+ # @param internal [Boolean] If true the exchange can't be published to directly
97
+ # @param arguments [Hash] Custom arguments
98
+ # @return [nil]
99
+ def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
100
+ write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, arguments)
101
+ expect :exchange_declare_ok
102
+ nil
103
+ end
78
104
 
79
- QueueOk = Struct.new(:queue_name, :message_count, :consumer_count)
105
+ # Delete an exchange
106
+ # @param name [String] Name of the exchange
107
+ # @param if_unused [Boolean] If true raise an exception if queues/exchanges is bound to this exchange
108
+ # @param no_wait [Boolean] If true don't wait for a server confirmation
109
+ # @return [nil]
110
+ def exchange_delete(name, if_unused: false, no_wait: false)
111
+ write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
112
+ expect :exchange_delete_ok unless no_wait
113
+ nil
114
+ end
80
115
 
81
- def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
82
- durable = false if name.empty?
83
- exclusive = true if name.empty?
84
- auto_delete = true if name.empty?
116
+ # Bind an exchange to another exchange
117
+ # @param destination [String] Name of the exchange to bind
118
+ # @param source [String] Name of the exchange to bind to
119
+ # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
120
+ # @param arguments [Hash] Message headers to match on, but only when bound to header exchanges
121
+ # @return [nil]
122
+ def exchange_bind(destination, source, binding_key, arguments: {})
123
+ write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
124
+ expect :exchange_bind_ok
125
+ nil
126
+ end
85
127
 
86
- write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
87
- name, message_count, consumer_count = expect(:queue_declare_ok)
128
+ # Unbind an exchange from another exchange
129
+ # @param destination [String] Name of the exchange to unbind
130
+ # @param source [String] Name of the exchange to unbind from
131
+ # @param binding_key [String] Binding key which the queue is bound to the exchange with
132
+ # @param arguments [Hash] Arguments matching the binding that's being removed
133
+ # @return [nil]
134
+ def exchange_unbind(destination, source, binding_key, arguments: {})
135
+ write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
136
+ expect :exchange_unbind_ok
137
+ nil
138
+ end
88
139
 
89
- QueueOk.new(name, message_count, consumer_count)
90
- end
140
+ # @!endgroup
141
+ # @!group Queue
142
+
143
+ # Response when declaring a Queue
144
+ # @!attribute queue_name
145
+ # @return [String] The name of the queue
146
+ # @!attribute message_count
147
+ # @return [Integer] Number of messages in the queue at the time of declaration
148
+ # @!attribute consumer_count
149
+ # @return [Integer] Number of consumers subscribed to the queue at the time of declaration
150
+ QueueOk = Struct.new(:queue_name, :message_count, :consumer_count)
151
+
152
+ # Create a queue (operation is idempotent)
153
+ # @param name [String] Name of the queue, can be empty, but will then be generated by the broker
154
+ # @param passive [Boolean] If true an exception will be raised if the queue doesn't already exists
155
+ # @param durable [Boolean] If true the queue will survive broker restarts,
156
+ # messages in the queue will only survive if they are published as persistent
157
+ # @param exclusive [Boolean] If true the queue will be deleted when the channel is closed
158
+ # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
159
+ # (it won't be deleted until at least one consumer has consumed from it)
160
+ # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
161
+ # @return [QueueOk] The QueueOk struct got `queue_name`, `message_count` and `consumer_count` properties
162
+ def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
163
+ durable = false if name.empty?
164
+ exclusive = true if name.empty?
165
+ auto_delete = true if name.empty?
166
+
167
+ write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
168
+ name, message_count, consumer_count = expect(:queue_declare_ok)
169
+
170
+ QueueOk.new(name, message_count, consumer_count)
171
+ end
91
172
 
92
- def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
93
- write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
94
- message_count, = expect :queue_delete
95
- message_count
96
- end
173
+ # Delete a queue
174
+ # @param name [String] Name of the queue
175
+ # @param if_unused [Boolean] Only delete if the queue doesn't have consumers, raises a ChannelClosed error otherwise
176
+ # @param if_empty [Boolean] Only delete if the queue is empty, raises a ChannelClosed error otherwise
177
+ # @param no_wait [Boolean] Don't wait for a server confirmation if true
178
+ # @return [Integer] Number of messages in queue when deleted
179
+ # @return [nil] If no_wait was set true
180
+ def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
181
+ write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
182
+ message_count, = expect :queue_delete unless no_wait
183
+ message_count
184
+ end
97
185
 
98
- def queue_bind(name, exchange, binding_key, arguments: {})
99
- write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
100
- expect :queue_bind_ok
101
- end
186
+ # Bind a queue to an exchange
187
+ # @param name [String] Name of the queue to bind
188
+ # @param exchange [String] Name of the exchange to bind to
189
+ # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
190
+ # @param arguments [Hash] Message headers to match on, but only when bound to header exchanges
191
+ # @return [nil]
192
+ def queue_bind(name, exchange, binding_key, arguments: {})
193
+ write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
194
+ expect :queue_bind_ok
195
+ nil
196
+ end
102
197
 
103
- def queue_purge(name, no_wait: false)
104
- write_bytes FrameBytes.queue_purge(@id, name, no_wait)
105
- expect :queue_purge_ok unless no_wait
106
- end
198
+ # Purge a queue
199
+ # @param name [String] Name of the queue
200
+ # @param no_wait [Boolean] Don't wait for a server confirmation if true
201
+ # @return [nil]
202
+ def queue_purge(name, no_wait: false)
203
+ write_bytes FrameBytes.queue_purge(@id, name, no_wait)
204
+ expect :queue_purge_ok unless no_wait
205
+ nil
206
+ end
107
207
 
108
- def queue_unbind(name, exchange, binding_key, arguments: {})
109
- write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
110
- expect :queue_unbind_ok
111
- end
208
+ # Unbind a queue from an exchange
209
+ # @param name [String] Name of the queue to unbind
210
+ # @param exchange [String] Name of the exchange to unbind from
211
+ # @param binding_key [String] Binding key which the queue is bound to the exchange with
212
+ # @param arguments [Hash] Arguments matching the binding that's being removed
213
+ # @return [nil]
214
+ def queue_unbind(name, exchange, binding_key, arguments: {})
215
+ write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
216
+ expect :queue_unbind_ok
217
+ end
112
218
 
113
- def basic_get(queue_name, no_ack: true)
114
- write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
115
- case (msg = @basic_gets.pop)
116
- when Message then msg
117
- when :basic_get_empty then nil
118
- when nil then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
119
- end
120
- end
219
+ # @!endgroup
220
+ # @!group Basic
221
+
222
+ # Get a message from a queue (by polling)
223
+ # @param queue_name [String]
224
+ # @param no_ack [Boolean] When false the message have to be manually acknowledged
225
+ # @return [Message] If the queue had a message
226
+ # @return [nil] If the queue doesn't have any messages
227
+ def basic_get(queue_name, no_ack: true)
228
+ write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
229
+ case (msg = @basic_gets.pop)
230
+ when Message then msg
231
+ when :basic_get_empty then nil
232
+ when nil then raise Error::ChannelClosed.new(@id, *@closed)
233
+ end
234
+ end
121
235
 
122
- def basic_publish(body, exchange, routing_key, **properties)
123
- frame_max = @connection.frame_max - 8
124
- id = @id
125
- mandatory = properties.delete(:mandatory) || false
126
- case properties.delete(:persistent)
127
- when true then properties[:delivery_mode] = 2
128
- when false then properties[:delivery_mode] = 1
129
- end
236
+ # Publishes a message to an exchange
237
+ # @param body [String] The body, can be a string or a byte array
238
+ # @param exchange [String] Name of the exchange to publish to
239
+ # @param routing_key [String] The routing key that the exchange might use to route the message to a queue
240
+ # @param properties [Properties]
241
+ # @option properties [String] content_type Content type of the message body
242
+ # @option properties [String] content_encoding Content encoding of the body
243
+ # @option properties [Hash<String, Object>] headers Custom headers
244
+ # @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
245
+ # @option properties [Integer] priority A priority of the message (between 0 and 255)
246
+ # @option properties [Integer] correlation_id A correlation id, most often used used for RPC communication
247
+ # @option properties [String] reply_to Queue to reply RPC responses to
248
+ # @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
249
+ # @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
250
+ # @option properties [Date] timestamp Often used for the time the message was originally generated
251
+ # @option properties [String] type Can indicate what kind of message this is
252
+ # @option properties [String] user_id Can be used to verify that this is the user that published the message
253
+ # @option properties [String] app_id Can be used to indicates which app that generated the message
254
+ # @return [nil]
255
+ def basic_publish(body, exchange, routing_key, **properties)
256
+ frame_max = @connection.frame_max - 8
257
+ id = @id
258
+ mandatory = properties.delete(:mandatory) || false
259
+ case properties.delete(:persistent)
260
+ when true then properties[:delivery_mode] = 2
261
+ when false then properties[:delivery_mode] = 1
262
+ end
130
263
 
131
- if body.bytesize.between?(1, frame_max)
132
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
133
- FrameBytes.header(id, body.bytesize, properties),
134
- FrameBytes.body(id, body)
135
- @unconfirmed.push @confirm += 1 if @confirm
136
- return
137
- end
264
+ if body.bytesize.between?(1, frame_max)
265
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
266
+ FrameBytes.header(id, body.bytesize, properties),
267
+ FrameBytes.body(id, body)
268
+ @unconfirmed.push @confirm += 1 if @confirm
269
+ return
270
+ end
138
271
 
139
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
140
- FrameBytes.header(id, body.bytesize, properties)
141
- pos = 0
142
- while pos < body.bytesize # split body into multiple frame_max frames
143
- len = [frame_max, body.bytesize - pos].min
144
- body_part = body.byteslice(pos, len)
145
- write_bytes FrameBytes.body(id, body_part)
146
- pos += len
147
- end
148
- @unconfirmed.push @confirm += 1 if @confirm
149
- nil
150
- end
272
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
273
+ FrameBytes.header(id, body.bytesize, properties)
274
+ pos = 0
275
+ while pos < body.bytesize # split body into multiple frame_max frames
276
+ len = [frame_max, body.bytesize - pos].min
277
+ body_part = body.byteslice(pos, len)
278
+ write_bytes FrameBytes.body(id, body_part)
279
+ pos += len
280
+ end
281
+ @unconfirmed.push @confirm += 1 if @confirm
282
+ nil
283
+ end
151
284
 
152
- def basic_publish_confirm(body, exchange, routing_key, **properties)
153
- confirm_select(no_wait: true)
154
- basic_publish(body, exchange, routing_key, **properties)
155
- wait_for_confirms
156
- end
285
+ # Publish a message and block until the message has confirmed it has received it
286
+ # @param (see #basic_publish)
287
+ # @return [Boolean] True if the message was successfully published
288
+ def basic_publish_confirm(body, exchange, routing_key, **properties)
289
+ confirm_select(no_wait: true)
290
+ basic_publish(body, exchange, routing_key, **properties)
291
+ wait_for_confirms
292
+ end
157
293
 
158
- # Consume from a queue
159
- # worker_threads: 0 => blocking, messages are executed in the thread calling this method
160
- def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, 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
- if worker_threads.zero?
165
- loop do
166
- yield (q.pop || break)
167
- end
168
- else
169
- threads = Array.new(worker_threads) do
170
- Thread.new do
294
+ # Consume messages from a queue
295
+ # @param queue [String] Name of the queue to subscribe to
296
+ # @param tag [String] Custom consumer tag, will be auto assigned by the server if empty
297
+ # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
298
+ # @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
299
+ # @param arguments [Hash] Custom arguments to the consumer
300
+ # @param worker_threads [Integer] Number of threads processing messages,
301
+ # 0 means that the thread calling this method will be blocked
302
+ # @yield [Message] Delivered message from the queue
303
+ # @return [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
304
+ # @return [nil] When `worker_threads` is 0 the method will return when the consumer is cancelled
305
+ def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, worker_threads: 1)
306
+ write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
307
+ tag, = expect(:basic_consume_ok)
308
+ q = @consumers[tag] = ::Queue.new
309
+ if worker_threads.zero?
171
310
  loop do
172
311
  yield (q.pop || break)
173
312
  end
313
+ nil
314
+ else
315
+ threads = Array.new(worker_threads) do
316
+ Thread.new do
317
+ loop do
318
+ yield (q.pop || break)
319
+ end
320
+ end
321
+ end
322
+ [tag, threads]
174
323
  end
175
324
  end
176
- [tag, threads]
177
- end
178
- end
179
325
 
180
- def basic_cancel(consumer_tag, no_wait: false)
181
- consumer = @consumers.fetch(consumer_tag)
182
- return if consumer.closed?
326
+ # Cancel/abort/stop a consumer
327
+ # @param consumer_tag [String] Tag of the consumer to cancel
328
+ # @param no_wait [Boolean] Will wait for a confirmation from the server that the consumer is cancelled
329
+ # @return [nil]
330
+ def basic_cancel(consumer_tag, no_wait: false)
331
+ consumer = @consumers.fetch(consumer_tag)
332
+ return if consumer.closed?
333
+
334
+ write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
335
+ expect(:basic_cancel_ok) unless no_wait
336
+ consumer.close
337
+ nil
338
+ end
183
339
 
184
- write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
185
- expect(:basic_cancel_ok) unless no_wait
186
- consumer.close
187
- end
340
+ # Specify how many messages to prefetch for consumers with `no_ack: false`
341
+ # @param prefetch_count [Integer] Number of messages to maxium keep in flight
342
+ # @param prefetch_size [Integer] Number of bytes to maxium keep in flight
343
+ # @param global [Boolean] If true the limit will apply to channel rather than the consumer
344
+ # @return [nil]
345
+ def basic_qos(prefetch_count, prefetch_size: 0, global: false)
346
+ write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
347
+ expect :basic_qos_ok
348
+ nil
349
+ end
188
350
 
189
- def basic_qos(prefetch_count, prefetch_size: 0, global: false)
190
- write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
191
- expect :basic_qos_ok
192
- end
351
+ # Acknowledge a message
352
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
353
+ # @return [nil]
354
+ def basic_ack(delivery_tag, multiple: false)
355
+ write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
356
+ nil
357
+ end
193
358
 
194
- def basic_ack(delivery_tag, multiple: false)
195
- write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
196
- end
359
+ # Negatively acknowledge a message
360
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
361
+ # @param multiple [Boolean] Nack all messages up to this message
362
+ # @param requeue [Boolean] Requeue the message
363
+ # @return [nil]
364
+ def basic_nack(delivery_tag, multiple: false, requeue: false)
365
+ write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
366
+ nil
367
+ end
197
368
 
198
- def basic_nack(delivery_tag, multiple: false, requeue: false)
199
- write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
200
- end
369
+ # Reject a message
370
+ # @param delivery_tag [Integer] The delivery tag of the message to acknowledge
371
+ # @param requeue [Boolean] Requeue the message into the queue again
372
+ # @return [nil]
373
+ def basic_reject(delivery_tag, requeue: false)
374
+ write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
375
+ nil
376
+ end
201
377
 
202
- def basic_reject(delivery_tag, requeue: false)
203
- write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
204
- end
378
+ # Recover all the unacknowledge messages
379
+ # @param requeue [Boolean] If false the currently unack:ed messages will be deliviered to this consumer again,
380
+ # if false to any consumer
381
+ # @return [nil]
382
+ def basic_recover(requeue: false)
383
+ write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
384
+ expect :basic_recover_ok
385
+ nil
386
+ end
205
387
 
206
- def basic_recover(requeue: false)
207
- write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
208
- expect :basic_recover_ok
209
- end
388
+ # @!endgroup
389
+ # @!group Confirm
210
390
 
211
- def confirm_select(no_wait: false)
212
- return if @confirm
391
+ # Put the channel in confirm mode, each published message will then be confirmed by the server
392
+ # @param no_wait [Boolean] If false the method will block until the server has confirmed the request
393
+ # @return [nil]
394
+ def confirm_select(no_wait: false)
395
+ return if @confirm
213
396
 
214
- write_bytes FrameBytes.confirm_select(@id, no_wait)
215
- expect :confirm_select_ok unless no_wait
216
- @confirm = 0
217
- end
397
+ write_bytes FrameBytes.confirm_select(@id, no_wait)
398
+ expect :confirm_select_ok unless no_wait
399
+ @confirm = 0
400
+ nil
401
+ end
218
402
 
219
- # Block until all publishes messages are confirmed
220
- def wait_for_confirms
221
- return true if @unconfirmed.empty?
403
+ # Block until all publishes messages are confirmed
404
+ # @return [Boolean] True if all message where positivly acknowledged, false if not
405
+ def wait_for_confirms
406
+ return true if @unconfirmed.empty?
222
407
 
223
- case @unconfirmed_empty.pop
224
- when true then true
225
- when false then false
226
- else raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
227
- end
228
- end
408
+ case @unconfirmed_empty.pop
409
+ when true then true
410
+ when false then false
411
+ else raise Error::ChannelClosed.new(@id, *@closed)
412
+ end
413
+ end
229
414
 
230
- # Called by Connection when received ack/nack from server
231
- def confirm(args)
232
- ack_or_nack, delivery_tag, multiple = *args
233
- loop do
234
- tag = @unconfirmed.pop(true)
235
- break if tag == delivery_tag
236
- next if multiple && tag < delivery_tag
237
-
238
- @unconfirmed << tag # requeue
239
- rescue ThreadError
240
- break
241
- end
242
- return unless @unconfirmed.empty?
415
+ # Called by Connection when received ack/nack from server
416
+ # @api private
417
+ def confirm(args)
418
+ ack_or_nack, delivery_tag, multiple = *args
419
+ loop do
420
+ tag = @unconfirmed.pop(true)
421
+ break if tag == delivery_tag
422
+ next if multiple && tag < delivery_tag
423
+
424
+ @unconfirmed << tag # requeue
425
+ rescue ThreadError
426
+ break
427
+ end
428
+ return unless @unconfirmed.empty?
243
429
 
244
- @unconfirmed_empty.num_waiting.times do
245
- @unconfirmed_empty << (ack_or_nack == :ack)
246
- end
247
- end
430
+ @unconfirmed_empty.num_waiting.times do
431
+ @unconfirmed_empty << (ack_or_nack == :ack)
432
+ end
433
+ end
248
434
 
249
- def tx_select
250
- write_bytes FrameBytes.tx_select(@id)
251
- expect :tx_select_ok
252
- end
435
+ # @!endgroup
436
+ # @!group Transaction
253
437
 
254
- def tx_commit
255
- write_bytes FrameBytes.tx_commit(@id)
256
- expect :tx_commit_ok
257
- end
438
+ # Put the channel in transaction mode, make sure that you #tx_commit or #tx_rollback after publish
439
+ # @return [nil]
440
+ def tx_select
441
+ write_bytes FrameBytes.tx_select(@id)
442
+ expect :tx_select_ok
443
+ nil
444
+ end
258
445
 
259
- def tx_rollback
260
- write_bytes FrameBytes.tx_rollback(@id)
261
- expect :tx_rollback_ok
262
- end
446
+ # Commmit a transaction, requires that the channel is in transaction mode
447
+ # @return [nil]
448
+ def tx_commit
449
+ write_bytes FrameBytes.tx_commit(@id)
450
+ expect :tx_commit_ok
451
+ nil
452
+ end
263
453
 
264
- def on_return(&block)
265
- @on_return = block
266
- end
454
+ # Rollback a transaction, requires that the channel is in transaction mode
455
+ # @return [nil]
456
+ def tx_rollback
457
+ write_bytes FrameBytes.tx_rollback(@id)
458
+ expect :tx_rollback_ok
459
+ nil
460
+ end
267
461
 
268
- def reply(args)
269
- @replies.push(args)
270
- end
462
+ # @!endgroup
271
463
 
272
- def message_returned(reply_code, reply_text, exchange, routing_key)
273
- @next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, nil, "")
274
- end
464
+ # @api private
465
+ def reply(args)
466
+ @replies.push(args)
467
+ end
275
468
 
276
- def message_delivered(consumer_tag, delivery_tag, redelivered, exchange, routing_key)
277
- @next_msg = Message.new(self, delivery_tag, exchange, routing_key, nil, "", redelivered, consumer_tag)
278
- end
469
+ # @api private
470
+ def message_returned(reply_code, reply_text, exchange, routing_key)
471
+ @next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, nil, "")
472
+ end
279
473
 
280
- def basic_get_empty
281
- @basic_gets.push :basic_get_empty
282
- end
474
+ # @api private
475
+ def message_delivered(consumer_tag, delivery_tag, redelivered, exchange, routing_key)
476
+ @next_msg = Message.new(self, delivery_tag, exchange, routing_key, nil, "", redelivered, consumer_tag)
477
+ end
283
478
 
284
- def header_delivered(body_size, properties)
285
- @next_msg.properties = properties
286
- if body_size.zero?
287
- next_message_finished!
288
- else
289
- @next_body = StringIO.new(String.new(capacity: body_size))
290
- @next_body_size = body_size
291
- end
292
- end
479
+ # @api private
480
+ def basic_get_empty
481
+ @basic_gets.push :basic_get_empty
482
+ end
293
483
 
294
- def body_delivered(body_part)
295
- @next_body.write(body_part)
296
- return unless @next_body.pos == @next_body_size
484
+ # @api private
485
+ def header_delivered(body_size, properties)
486
+ @next_msg.properties = properties
487
+ if body_size.zero?
488
+ next_message_finished!
489
+ else
490
+ @next_body = StringIO.new(String.new(capacity: body_size))
491
+ @next_body_size = body_size
492
+ end
493
+ end
297
494
 
298
- @next_msg.body = @next_body.string
299
- next_message_finished!
300
- end
495
+ # @api private
496
+ def body_delivered(body_part)
497
+ @next_body.write(body_part)
498
+ return unless @next_body.pos == @next_body_size
301
499
 
302
- def close_consumer(tag)
303
- @consumers.fetch(tag).close
304
- end
500
+ @next_msg.body = @next_body.string
501
+ next_message_finished!
502
+ end
305
503
 
306
- private
307
-
308
- def next_message_finished!
309
- next_msg = @next_msg
310
- if next_msg.is_a? ReturnMessage
311
- if @on_return
312
- Thread.new { @on_return.call(next_msg) }
313
- else
314
- warn "AMQP-Client message returned: #{msg.inspect}"
315
- end
316
- elsif next_msg.consumer_tag.nil?
317
- @basic_gets.push next_msg
318
- else
319
- Thread.pass until (consumer = @consumers[next_msg.consumer_tag])
320
- consumer.push next_msg
321
- end
322
- ensure
323
- @next_msg = @next_body = @next_body_size = nil
324
- end
504
+ # @api private
505
+ def close_consumer(tag)
506
+ @consumers.fetch(tag).close
507
+ end
325
508
 
326
- def write_bytes(*bytes)
327
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if @closed
509
+ private
328
510
 
329
- @connection.write_bytes(*bytes)
330
- end
511
+ def next_message_finished!
512
+ next_msg = @next_msg
513
+ if next_msg.is_a? ReturnMessage
514
+ if @on_return
515
+ Thread.new { @on_return.call(next_msg) }
516
+ else
517
+ warn "AMQP-Client message returned: #{msg.inspect}"
518
+ end
519
+ elsif next_msg.consumer_tag.nil?
520
+ @basic_gets.push next_msg
521
+ else
522
+ Thread.pass until (consumer = @consumers[next_msg.consumer_tag])
523
+ consumer.push next_msg
524
+ end
525
+ ensure
526
+ @next_msg = @next_body = @next_body_size = nil
527
+ end
528
+
529
+ def write_bytes(*bytes)
530
+ raise Error::ChannelClosed.new(@id, *@closed) if @closed
531
+
532
+ @connection.write_bytes(*bytes)
533
+ end
331
534
 
332
- def expect(expected_frame_type)
333
- frame_type, *args = @replies.pop
334
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
335
- raise AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
535
+ def expect(expected_frame_type)
536
+ frame_type, *args = @replies.pop
537
+ raise Error::ChannelClosed.new(@id, *@closed) if frame_type.nil?
538
+ raise Error::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
336
539
 
337
- args
540
+ args
541
+ end
542
+ end
338
543
  end
339
544
  end
340
545
  end