amqp-client 0.2.2 → 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: bce748ffe46469f11bfecc13b7f7b7967cacdffb06b535dd6aff7377070c8c66
4
- data.tar.gz: db388d3c3ddcd86df118621f33d486b01e78bb10a4d4696aba829dab86a14936
3
+ metadata.gz: 89ea2743bb81c4615d1445ee6ce25a5234a95f9f859ca4926ff5437228af1e02
4
+ data.tar.gz: 4739cd429d9cf55a631c36c9100f78c6d2da65d4aa18535297d69b8ec0ca6e56
5
5
  SHA512:
6
- metadata.gz: cbcd67199658ecaddb418eeb540e3ad2685df8f5b9b050631ad5baa40dc93f3f74ddd04d17750d677388f5546653ec408ae8ffea02ab65eedacded2a1dc851aa
7
- data.tar.gz: e0825a87830cdf054d43a2cabe4e7dd55bded23203b2e8999b41ba0601ef8593460580283a2ad34622ad4b59198a4c537ee9b9c0c413dc76495858c629af795f
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,44 @@
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
+
13
+ ## [1.0.0] - 2021-08-27
14
+
15
+ - Verify TLS certificate matches hostname
16
+ - TLS thread-safety
17
+ - Assemble Messages in the (single threaded) read_loop thread
18
+ - Give read_loop_thread higher priority so that channel errors crop up faster
19
+ - One less Thread required per Consumer
20
+ - Read exactly one frame at a time, not trying to split/assemble frames over socket reads
21
+ - Heafty speedup for message assembling with StringIO
22
+ - Channel#queue_declare returns a struct for nicer API (still backward compatible)
23
+ - AMQP::Client#publish_and_forget for fast, non confirmed publishes
24
+ - Allow Properties#timestamp to be an integer (in addition to Time)
25
+ - Bug fix allow Properties#expiration to be an Integer
26
+ - Consistent use of named parameters
27
+ - High level Exchange API
28
+ - Don't try to reconnect if first connect fails
29
+ - Bug fix: Close all channels when connection is closed by server
30
+ - Raise error if run out of channels
31
+ - Improved retry in high level client
32
+ - Bug fix: Support channel_max 0
33
+
34
+ ## [0.3.0] - 2021-08-20
35
+
36
+ - Channel#wait_for_confirms is a smarter way of waiting for publish confirms
37
+ - Default connection_name to $PROGRAM_NAME
38
+
39
+ ## [0.2.3] - 2021-08-19
40
+
41
+ - Improved TLS/AMQPS support
3
42
 
4
43
  ## [0.2.2] - 2021-08-19
5
44
 
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 server).
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 server
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,542 @@
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 = [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 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
47
78
 
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
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
52
86
 
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
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
57
104
 
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
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
62
115
 
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
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
67
127
 
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
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
81
139
 
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
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
87
172
 
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
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
92
185
 
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
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
97
197
 
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
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
102
207
 
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
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
123
218
 
124
- def basic_publish(body, exchange, routing_key, **properties)
125
- frame_max = @connection.frame_max - 8
126
- id = @id
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
127
235
 
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
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
134
263
 
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
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
146
271
 
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
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
152
284
 
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)
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
293
+
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?
310
+ loop do
311
+ yield (q.pop || break)
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
171
321
  end
322
+ [tag, threads]
172
323
  end
173
324
  end
174
- [tag, threads]
175
- end
176
- end
177
325
 
178
- def basic_cancel(consumer_tag, no_wait: false)
179
- consumer = @consumers.fetch(consumer_tag)
180
- 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
181
339
 
182
- write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
183
- expect(:basic_cancel_ok) unless no_wait
184
- consumer.close
185
- 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
186
350
 
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
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
191
358
 
192
- def basic_ack(delivery_tag, multiple: false)
193
- write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
194
- 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
195
368
 
196
- def basic_nack(delivery_tag, multiple: false, requeue: false)
197
- write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
198
- 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
199
377
 
200
- def basic_reject(delivery_tag, requeue: false)
201
- write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
202
- 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
203
387
 
204
- def basic_recover(requeue: false)
205
- write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
206
- expect :basic_recover_ok
207
- end
388
+ # @!endgroup
389
+ # @!group Confirm
208
390
 
209
- def confirm_select(no_wait: false)
210
- 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
211
396
 
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
397
+ write_bytes FrameBytes.confirm_select(@id, no_wait)
398
+ expect :confirm_select_ok unless no_wait
399
+ @confirm = 0
400
+ nil
401
+ end
217
402
 
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
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?
221
407
 
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
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
- def tx_select
231
- write_bytes FrameBytes.tx_select(@id)
232
- expect :tx_select_ok
233
- end
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?
234
429
 
235
- def tx_commit
236
- write_bytes FrameBytes.tx_commit(@id)
237
- expect :tx_commit_ok
238
- end
430
+ @unconfirmed_empty.num_waiting.times do
431
+ @unconfirmed_empty << (ack_or_nack == :ack)
432
+ end
433
+ end
239
434
 
240
- def tx_rollback
241
- write_bytes FrameBytes.tx_rollback(@id)
242
- expect :tx_rollback_ok
243
- end
435
+ # @!endgroup
436
+ # @!group Transaction
244
437
 
245
- def reply(args)
246
- @replies.push(args)
247
- 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
248
445
 
249
- def confirm(args)
250
- @confirms.push(args)
251
- 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
252
453
 
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
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
260
460
  end
261
- msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
262
461
 
263
- if @on_return
264
- @on_return.call(msg)
265
- else
266
- puts "[WARN] Message returned: #{msg.inspect}"
462
+ # @!endgroup
463
+
464
+ # @api private
465
+ def reply(args)
466
+ @replies.push(args)
267
467
  end
268
- end
269
- end
270
468
 
271
- def on_return(&block)
272
- @on_return = block
273
- 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
473
+
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
274
478
 
275
- private
479
+ # @api private
480
+ def basic_get_empty
481
+ @basic_gets.push :basic_get_empty
482
+ end
276
483
 
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
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
285
493
  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
494
 
292
- def write_bytes(*bytes)
293
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if @closed
495
+ # @api private
496
+ def body_delivered(body_part)
497
+ @next_body.write(body_part)
498
+ return unless @next_body.pos == @next_body_size
294
499
 
295
- @connection.write_bytes(*bytes)
296
- end
500
+ @next_msg.body = @next_body.string
501
+ next_message_finished!
502
+ end
503
+
504
+ # @api private
505
+ def close_consumer(tag)
506
+ @consumers.fetch(tag).close
507
+ end
508
+
509
+ private
510
+
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
297
531
 
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
532
+ @connection.write_bytes(*bytes)
533
+ end
534
+
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
303
539
 
304
- @replies.push [frame_type, *args]
540
+ args
541
+ end
305
542
  end
306
543
  end
307
544
  end