amqp-client 1.0.0 → 1.0.1

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: 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