amqp-client 1.1.0 → 1.1.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: ec3129a38420f19de4c0225aea1eaefc320463c6e032c54990a998f7654f8347
4
- data.tar.gz: 3c8da5fccb818730958f47fdaf78837524381b116a322adaf585ee02d0492b2d
3
+ metadata.gz: 0e248898d176ad9c3d785226ed37c0335052ef701ee16a16bff483089be65dd8
4
+ data.tar.gz: 89b58399edb11b30b05db86e9e4b0a617438e3ed52e63b3e76ae3a4c8cb361fc
5
5
  SHA512:
6
- metadata.gz: 24b592bb7fc50f29499e32ca6348f2aa25bac9837e694740346fa24e1fe6c45c9f9ab6497f066ca6491c6f09e558b29ac3a1d84f5b31a3b2b68bb8c28cdf76e7
7
- data.tar.gz: 6ad3e059d311894f4a023d75df89c72beb4508a2abc83083a47f4f7a07cc6f98edc157e06b5ed42eda55458612d89206de1cce4b0cc94fd598facad4433083e9
6
+ metadata.gz: 4500bec819ea9de9546f001eed682ac040ea3c2f93904dabb24afdbbda966d3352d8b1e33687a82c482ce835e7d650172dd906d9ef4ec2efc17f1dea31947314
7
+ data.tar.gz: 62ea49dc2782122146dd2613cd51b1c4132b3eef9d48c5573e9369b0af96042018f104a84cfe2aa2f173cf6d10f5ca3e427baa3ef61faed7029cd7328148a762
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.1] - 2021-09-15
4
+
5
+ - Added: Examples in the documentation
6
+ - Added: Faster Properties and Table encoding and decoding
7
+
3
8
  ## [1.1.0] - 2021-09-08
4
9
 
5
10
  - Fixed: Due to a race condition publishers could get stuck waiting for publish confirms
data/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A modern AMQP 0-9-1 Ruby client. Very fast (just as fast as the Java client, and >4x than other Ruby clients), fully thread-safe, blocking operations and straight-forward error handling.
4
4
 
5
+ It's small, only ~1800 lines of code, and without any dependencies. Other Ruby clients are about 4 times bigger. But without trading functionallity.
6
+
7
+ It's safe by default, messages are published as persistent, and is waiting for confirmation from the broker. That can of course be disabled if performance is a priority.
8
+
5
9
  ## Support
6
10
 
7
11
  The library is fully supported by [CloudAMQP](https://www.cloudamqp.com), the largest RabbitMQ hosting provider in the world. Open [an issue](https://github.com/cloudamqp/amqp-client.rb/issues) or [email our support](mailto:support@cloudamqp.com) if you have problems or questions.
@@ -31,10 +35,10 @@ ch = conn.channel
31
35
  q = ch.queue_declare
32
36
 
33
37
  # Publish a message to said queue
34
- ch.basic_publish "Hello World!", "", q.queue_name
38
+ ch.basic_publish_confirm "Hello World!", "", q.queue_name, persistent: true
35
39
 
36
40
  # Poll the queue for a message
37
- msg = ch.basic_get q.queue_name
41
+ msg = ch.basic_get(q.queue_name)
38
42
 
39
43
  # Print the message's body to STDOUT
40
44
  puts msg.body
@@ -104,7 +108,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
104
108
 
105
109
  ## Contributing
106
110
 
107
- Bug reports and pull requests are welcome on GitHub at https://github.com/cloudamqp/amqp-client.rb
111
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/cloudamqp/amqp-client.rb](https://github.com/cloudamqp/amqp-client.rb/)
108
112
 
109
113
  ## License
110
114
 
@@ -3,7 +3,7 @@
3
3
  require "socket"
4
4
  require "uri"
5
5
  require "openssl"
6
- require_relative "./frames"
6
+ require_relative "./frame_bytes"
7
7
  require_relative "./channel"
8
8
  require_relative "./errors"
9
9
 
@@ -350,7 +350,7 @@ module AMQP
350
350
  end
351
351
  when 2 # header
352
352
  body_size = buf.unpack1("@4 Q>")
353
- properties = Properties.decode(buf.byteslice(12, buf.bytesize - 12))
353
+ properties = Properties.decode(buf, 12)
354
354
  @channels[channel_id].header_delivered body_size, properties
355
355
  when 3 # body
356
356
  @channels[channel_id].body_delivered buf
@@ -332,7 +332,7 @@ module AMQP
332
332
  end
333
333
 
334
334
  def self.header(id, body_size, properties)
335
- props = Properties.new(**properties).encode
335
+ props = Properties.encode(properties)
336
336
  frame_size = 2 + 2 + 8 + props.bytesize
337
337
  [
338
338
  2, # type: header
@@ -23,56 +23,90 @@ module AMQP
23
23
  @app_id = app_id
24
24
  end
25
25
 
26
+ # Properties as a Hash
27
+ # @return [Hash] Properties
28
+ def to_h
29
+ {
30
+ content_type: content_type,
31
+ content_encoding: content_encoding,
32
+ headers: headers,
33
+ delivery_mode: delivery_mode,
34
+ priority: priority,
35
+ correlation_id: correlation_id,
36
+ reply_to: reply_to,
37
+ expiration: expiration,
38
+ message_id: message_id,
39
+ timestamp: timestamp,
40
+ type: type,
41
+ user_id: user_id,
42
+ app_id: app_id
43
+ }
44
+ end
45
+
46
+ alias to_hash to_h
47
+
48
+ # Returns true if two Property objects holds the same information
49
+ # @return [Boolean]
50
+ def ==(other)
51
+ return false unless other.is_a? self.class
52
+
53
+ instance_variables.all? { |v| instance_variable_get(v) == other.instance_variable_get(v) }
54
+ end
55
+
26
56
  # Content type of the message body
27
57
  # @return [String, nil]
28
58
  attr_accessor :content_type
29
59
  # Content encoding of the body
30
60
  # @return [String, nil]
31
61
  attr_accessor :content_encoding
32
- # Custom headers
62
+ # Headers, for applications and header exchange routing
33
63
  # @return [Hash<String, Object>, nil]
34
64
  attr_accessor :headers
35
- # 2 for persisted message, transient messages for all other values
36
- # @return [Integer, nil]
65
+ # Message persistent level
66
+ # @note The exchange and queue have to durable as well for the message to be persistent
67
+ # @return [1] Transient message
68
+ # @return [2] Persistent message
69
+ # @return [nil] Not specified (implicitly transient)
37
70
  attr_accessor :delivery_mode
38
71
  # A priority of the message (between 0 and 255)
39
72
  # @return [Integer, nil]
40
73
  attr_accessor :priority
41
- # A correlation id, most often used used for RPC communication
42
- # @return [Integer, nil]
74
+ # Message correlation id, commonly used to correlate RPC requests and responses
75
+ # @return [String, nil]
43
76
  attr_accessor :correlation_id
44
77
  # Queue to reply RPC responses to
45
78
  # @return [String, nil]
46
79
  attr_accessor :reply_to
47
80
  # Number of seconds the message will stay in the queue
48
- # @return [Integer]
49
- # @return [String]
50
- # @return [nil]
81
+ # @return [String, nil]
51
82
  attr_accessor :expiration
83
+ # Application message identifier
52
84
  # @return [String, nil]
53
85
  attr_accessor :message_id
54
- # User-definable, but often used for the time the message was originally generated
86
+ # Message timestamp, often indicates when the message was originally generated
55
87
  # @return [Date, nil]
56
88
  attr_accessor :timestamp
57
- # User-definable, but can can indicate what kind of message this is
89
+ # Message type name
58
90
  # @return [String, nil]
59
91
  attr_accessor :type
60
- # User-definable, but can be used to verify that this is the user that published the message
92
+ # The user that published the message
61
93
  # @return [String, nil]
62
94
  attr_accessor :user_id
63
- # User-definable, but often indicates which app that generated the message
95
+ # Name of application that generated the message
64
96
  # @return [String, nil]
65
97
  attr_accessor :app_id
66
98
 
67
99
  # Encode properties into a byte array
100
+ # @param properties [Hash]
68
101
  # @return [String] byte array
69
- def encode
102
+ def self.encode(properties)
103
+ return "\x00\x00" if properties.empty?
104
+
70
105
  flags = 0
71
106
  arr = [flags]
72
- fmt = StringIO.new(String.new("S>", capacity: 35))
73
- fmt.pos = 2
107
+ fmt = String.new("S>", capacity: 37)
74
108
 
75
- if content_type
109
+ if (content_type = properties[:content_type])
76
110
  content_type.is_a?(String) || raise(ArgumentError, "content_type must be a string")
77
111
 
78
112
  flags |= (1 << 15)
@@ -80,7 +114,7 @@ module AMQP
80
114
  fmt << "Ca*"
81
115
  end
82
116
 
83
- if content_encoding
117
+ if (content_encoding = properties[:content_encoding])
84
118
  content_encoding.is_a?(String) || raise(ArgumentError, "content_encoding must be a string")
85
119
 
86
120
  flags |= (1 << 14)
@@ -88,7 +122,7 @@ module AMQP
88
122
  fmt << "Ca*"
89
123
  end
90
124
 
91
- if headers
125
+ if (headers = properties[:headers])
92
126
  headers.is_a?(Hash) || raise(ArgumentError, "headers must be a hash")
93
127
 
94
128
  flags |= (1 << 13)
@@ -97,31 +131,31 @@ module AMQP
97
131
  fmt << "L>a*"
98
132
  end
99
133
 
100
- if delivery_mode
134
+ if (delivery_mode = properties[:delivery_mode])
101
135
  delivery_mode.is_a?(Integer) || raise(ArgumentError, "delivery_mode must be an int")
102
- delivery_mode.between?(0, 2) || raise(ArgumentError, "delivery_mode must be be between 0 and 2")
103
136
 
104
137
  flags |= (1 << 12)
105
138
  arr << delivery_mode
106
139
  fmt << "C"
107
140
  end
108
141
 
109
- if priority
142
+ if (priority = properties[:priority])
110
143
  priority.is_a?(Integer) || raise(ArgumentError, "priority must be an int")
144
+
111
145
  flags |= (1 << 11)
112
146
  arr << priority
113
147
  fmt << "C"
114
148
  end
115
149
 
116
- if correlation_id
117
- priority.is_a?(String) || raise(ArgumentError, "correlation_id must be a string")
150
+ if (correlation_id = properties[:correlation_id])
151
+ correlation_id.is_a?(String) || raise(ArgumentError, "correlation_id must be a string")
118
152
 
119
153
  flags |= (1 << 10)
120
154
  arr << correlation_id.bytesize << correlation_id
121
155
  fmt << "Ca*"
122
156
  end
123
157
 
124
- if reply_to
158
+ if (reply_to = properties[:reply_to])
125
159
  reply_to.is_a?(String) || raise(ArgumentError, "reply_to must be a string")
126
160
 
127
161
  flags |= (1 << 9)
@@ -129,8 +163,8 @@ module AMQP
129
163
  fmt << "Ca*"
130
164
  end
131
165
 
132
- if expiration
133
- self.expiration = expiration.to_s if expiration.is_a?(Integer)
166
+ if (expiration = properties[:expiration])
167
+ expiration = expiration.to_s if expiration.is_a?(Integer)
134
168
  expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string or integer")
135
169
 
136
170
  flags |= (1 << 8)
@@ -138,7 +172,7 @@ module AMQP
138
172
  fmt << "Ca*"
139
173
  end
140
174
 
141
- if message_id
175
+ if (message_id = properties[:message_id])
142
176
  message_id.is_a?(String) || raise(ArgumentError, "message_id must be a string")
143
177
 
144
178
  flags |= (1 << 7)
@@ -146,7 +180,7 @@ module AMQP
146
180
  fmt << "Ca*"
147
181
  end
148
182
 
149
- if timestamp
183
+ if (timestamp = properties[:timestamp])
150
184
  timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
151
185
 
152
186
  flags |= (1 << 6)
@@ -154,7 +188,7 @@ module AMQP
154
188
  fmt << "Q>"
155
189
  end
156
190
 
157
- if type
191
+ if (type = properties[:type])
158
192
  type.is_a?(String) || raise(ArgumentError, "type must be a string")
159
193
 
160
194
  flags |= (1 << 5)
@@ -162,7 +196,7 @@ module AMQP
162
196
  fmt << "Ca*"
163
197
  end
164
198
 
165
- if user_id
199
+ if (user_id = properties[:user_id])
166
200
  user_id.is_a?(String) || raise(ArgumentError, "user_id must be a string")
167
201
 
168
202
  flags |= (1 << 4)
@@ -170,7 +204,7 @@ module AMQP
170
204
  fmt << "Ca*"
171
205
  end
172
206
 
173
- if app_id
207
+ if (app_id = properties[:app_id])
174
208
  app_id.is_a?(String) || raise(ArgumentError, "app_id must be a string")
175
209
 
176
210
  flags |= (1 << 3)
@@ -179,15 +213,15 @@ module AMQP
179
213
  end
180
214
 
181
215
  arr[0] = flags
182
- arr.pack(fmt.string)
216
+ arr.pack(fmt)
183
217
  end
184
218
 
185
219
  # Decode a byte array
186
220
  # @return [Properties]
187
- def self.decode(bytes)
221
+ def self.decode(bytes, pos = 0)
188
222
  p = new
189
- flags = bytes.unpack1("S>")
190
- pos = 2
223
+ flags = bytes.byteslice(pos, 2).unpack1("S>")
224
+ pos += 2
191
225
  if (flags & 0x8000).positive?
192
226
  len = bytes.getbyte(pos)
193
227
  pos += 1
@@ -6,15 +6,20 @@ module AMQP
6
6
  # @api private
7
7
  module Table
8
8
  # Encodes a hash into a byte array
9
+ # @param hash [Hash]
9
10
  # @return [String] Byte array
10
11
  def self.encode(hash)
11
- tbl = StringIO.new
12
- hash.each do |k, v|
12
+ return "" if hash.empty?
13
+
14
+ arr = []
15
+ fmt = String.new
16
+ hash.each do |k, value|
13
17
  key = k.to_s
14
- tbl.write [key.bytesize, key].pack("Ca*")
15
- tbl.write encode_field(v)
18
+ arr.push(key.bytesize, key)
19
+ fmt << "Ca*"
20
+ encode_field(value, arr, fmt)
16
21
  end
17
- tbl.string
22
+ arr.pack(fmt)
18
23
  end
19
24
 
20
25
  # Decodes an AMQP table into a hash
@@ -27,8 +32,7 @@ module AMQP
27
32
  pos += 1
28
33
  key = bytes.byteslice(pos, key_len).force_encoding("utf-8")
29
34
  pos += key_len
30
- rest = bytes.byteslice(pos, bytes.bytesize - pos)
31
- len, value = decode_field(rest)
35
+ len, value = decode_field(bytes, pos)
32
36
  pos += len + 1
33
37
  hash[key] = value
34
38
  end
@@ -36,43 +40,58 @@ module AMQP
36
40
  end
37
41
 
38
42
  # Encoding a single value in a table
43
+ # @return [nil]
39
44
  # @api private
40
- def self.encode_field(value)
45
+ def self.encode_field(value, arr, fmt)
41
46
  case value
42
47
  when Integer
43
48
  if value > 2**31
44
- ["l", value].pack("a q>")
49
+ arr.push("l", value)
50
+ fmt << "aq>"
45
51
  else
46
- ["I", value].pack("a l>")
52
+ arr.push("I", value)
53
+ fmt << "al>"
47
54
  end
48
55
  when Float
49
- ["d", value].pack("a G")
56
+ arr.push("d", value)
57
+ fmt << "aG"
50
58
  when String
51
- ["S", value.bytesize, value].pack("a L> a*")
59
+ arr.push("S", value.bytesize, value)
60
+ fmt << "aL>a*"
52
61
  when Time
53
- ["T", value.to_i].pack("a Q>")
62
+ arr.push("T", value.to_i)
63
+ fmt << "aQ>"
54
64
  when Array
55
- bytes = value.map { |e| encode_field(e) }.join
56
- ["A", bytes.bytesize, bytes].pack("a L> a*")
65
+ value_arr = []
66
+ value_fmt = String.new
67
+ value.each { |e| encode_field(e, value_arr, value_fmt) }
68
+ bytes = value_arr.pack(value_fmt)
69
+ arr.push("A", bytes.bytesize, bytes)
70
+ fmt << "aL>a*"
57
71
  when Hash
58
72
  bytes = Table.encode(value)
59
- ["F", bytes.bytesize, bytes].pack("a L> a*")
73
+ arr.push("F", bytes.bytesize, bytes)
74
+ fmt << "aL>a*"
60
75
  when true
61
- ["t", 1].pack("a C")
76
+ arr.push("t", 1)
77
+ fmt << "aC"
62
78
  when false
63
- ["t", 0].pack("a C")
79
+ arr.push("t", 0)
80
+ fmt << "aC"
64
81
  when nil
65
- ["V"].pack("a")
82
+ arr << "V"
83
+ fmt << "a"
66
84
  else raise ArgumentError, "unsupported table field type: #{value.class}"
67
85
  end
86
+ nil
68
87
  end
69
88
 
70
89
  # Decodes a single value
71
- # @return [Array<Integer, Object>] Bytes read and the parsed object
90
+ # @return [Array<Integer, Object>] Bytes read and the parsed value
72
91
  # @api private
73
- def self.decode_field(bytes)
74
- type = bytes[0]
75
- pos = 1
92
+ def self.decode_field(bytes, pos)
93
+ type = bytes[pos]
94
+ pos += 1
76
95
  case type
77
96
  when "S"
78
97
  len = bytes.byteslice(pos, 4).unpack1("L>")
@@ -84,9 +103,11 @@ module AMQP
84
103
  [4 + len, decode(bytes.byteslice(pos, len))]
85
104
  when "A"
86
105
  len = bytes.byteslice(pos, 4).unpack1("L>")
106
+ pos += 4
107
+ array_end = pos + len
87
108
  a = []
88
- while pos < len
89
- length, value = decode_field(bytes.byteslice(pos, -1))
109
+ while pos < array_end
110
+ length, value = decode_field(bytes, pos)
90
111
  pos += length + 1
91
112
  a << value
92
113
  end
@@ -3,6 +3,6 @@
3
3
  module AMQP
4
4
  class Client
5
5
  # Version of the client library
6
- VERSION = "1.1.0"
6
+ VERSION = "1.1.1"
7
7
  end
8
8
  end
data/lib/amqp/client.rb CHANGED
@@ -38,12 +38,18 @@ module AMQP
38
38
  # Establishes and returns a new AMQP connection
39
39
  # @see Connection#initialize
40
40
  # @return [Connection]
41
+ # @example
42
+ # connection = AMQP::Client.new("amqps://server.rmq.cloudamqp.com", connection_name: "My connection").connect
41
43
  def connect(read_loop_thread: true)
42
44
  Connection.new(@uri, read_loop_thread: read_loop_thread, **@options)
43
45
  end
44
46
 
45
47
  # Opens an AMQP connection using the high level API, will try to reconnect if successfully connected at first
46
48
  # @return [self]
49
+ # @example
50
+ # amqp = AMQP::Client.new("amqps://server.rmq.cloudamqp.com")
51
+ # amqp.start
52
+ # amqp.queue("foobar")
47
53
  def start
48
54
  @stopped = false
49
55
  Thread.new(connect(read_loop_thread: false)) do |conn|
@@ -95,6 +101,10 @@ module AMQP
95
101
  # (it won't be deleted until at least one consumer has consumed from it)
96
102
  # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
97
103
  # @return [Queue]
104
+ # @example
105
+ # amqp = AMQP::Client.new.start
106
+ # q = amqp.queue("foobar")
107
+ # q.publish("body")
98
108
  def queue(name, durable: true, auto_delete: false, arguments: {})
99
109
  raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
100
110
 
@@ -108,6 +118,10 @@ module AMQP
108
118
 
109
119
  # Declare an exchange and return a high level Exchange object
110
120
  # @return [Exchange]
121
+ # @example
122
+ # amqp = AMQP::Client.new.start
123
+ # x = amqp.exchange("my.hash.exchange", "x-consistent-hash")
124
+ # x.publish("body", "routing-key")
111
125
  def exchange(name, type, durable: true, auto_delete: false, internal: false, arguments: {})
112
126
  @exchanges.fetch(name) do
113
127
  with_connection do |conn|
@@ -172,9 +186,7 @@ module AMQP
172
186
  with_connection do |conn|
173
187
  ch = conn.channel
174
188
  ch.basic_qos(prefetch)
175
- ch.basic_consume(queue, no_ack: no_ack, worker_threads: worker_threads, arguments: arguments) do |msg|
176
- blk.call(msg)
177
- end
189
+ ch.basic_consume(queue, no_ack: no_ack, worker_threads: worker_threads, arguments: arguments, &blk)
178
190
  end
179
191
  end
180
192
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amqp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Hörberg
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-08 00:00:00.000000000 Z
11
+ date: 2021-09-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Work in progress
14
14
  email:
@@ -37,7 +37,7 @@ files:
37
37
  - lib/amqp/client/connection.rb
38
38
  - lib/amqp/client/errors.rb
39
39
  - lib/amqp/client/exchange.rb
40
- - lib/amqp/client/frames.rb
40
+ - lib/amqp/client/frame_bytes.rb
41
41
  - lib/amqp/client/message.rb
42
42
  - lib/amqp/client/properties.rb
43
43
  - lib/amqp/client/queue.rb