amqp-client 1.1.0 → 1.1.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: 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