amqp-client 1.1.0 → 1.1.4
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 +4 -4
- data/.github/workflows/docs.yml +3 -0
- data/.github/workflows/main.yml +76 -6
- data/.rubocop_todo.yml +29 -12
- data/CHANGELOG.md +19 -0
- data/README.md +10 -3
- data/Rakefile +11 -4
- data/lib/amqp/client/channel.rb +1 -0
- data/lib/amqp/client/connection.rb +34 -16
- data/lib/amqp/client/{frames.rb → frame_bytes.rb} +1 -1
- data/lib/amqp/client/properties.rb +74 -37
- data/lib/amqp/client/table.rb +46 -25
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +15 -3
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e287ef0dee4c9ec581690733c12c132f74c595ceddead1ba5b95b765138e11c
|
4
|
+
data.tar.gz: e5462c8cd2a4f7232a51e17d9ec93f4fb2ea88a922bb9b5cc58242cced78d61e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 923fb64c7234fb6718f7dbc5eb33c1f8de03a9cf12d669539935cc6ef83f4ca387afc80c1e56fcc2ed167dc14381cda1d7b6a67673c6516818909cb6a84c287d
|
7
|
+
data.tar.gz: 14c4d9157cee844609eeb5beed70ee2560ba3442c5afb857fed3e0945c443910f7bd4a281e327d2cb5ee0f0896facf4c8e693e5d3ecf1152c07474e20f8a724f
|
data/.github/workflows/docs.yml
CHANGED
data/.github/workflows/main.yml
CHANGED
@@ -3,8 +3,9 @@ name: Ruby
|
|
3
3
|
on: [push,pull_request]
|
4
4
|
|
5
5
|
jobs:
|
6
|
-
|
6
|
+
tests:
|
7
7
|
runs-on: ubuntu-latest
|
8
|
+
timeout-minutes: 5
|
8
9
|
services:
|
9
10
|
rabbitmq:
|
10
11
|
image: rabbitmq:latest
|
@@ -19,16 +20,85 @@ jobs:
|
|
19
20
|
strategy:
|
20
21
|
fail-fast: false
|
21
22
|
matrix:
|
22
|
-
ruby: ['2.6', '2.7', '3.0']
|
23
|
+
ruby: ['2.6', '2.7', '3.0', '3.1']
|
24
|
+
include:
|
25
|
+
- { ruby: jruby, allow-failure: true }
|
26
|
+
- { ruby: truffleruby, allow-failure: true }
|
23
27
|
steps:
|
24
28
|
- uses: actions/checkout@v2
|
25
29
|
- name: Set up Ruby
|
26
30
|
uses: ruby/setup-ruby@v1
|
27
31
|
with:
|
32
|
+
bundler-cache: true
|
28
33
|
ruby-version: ${{ matrix.ruby }}
|
29
|
-
- name: Run
|
30
|
-
|
31
|
-
|
32
|
-
bundle exec rake
|
34
|
+
- name: Run tests (excluding TLS tests)
|
35
|
+
continue-on-error: ${{ matrix.allow-failure || false }}
|
36
|
+
run: bundle exec rake
|
33
37
|
env:
|
34
38
|
AMQP_PORT: ${{ job.services.rabbitmq.ports[5672] }}
|
39
|
+
tls:
|
40
|
+
runs-on: ubuntu-latest
|
41
|
+
timeout-minutes: 5
|
42
|
+
strategy:
|
43
|
+
fail-fast: false
|
44
|
+
matrix:
|
45
|
+
ruby: ['3.0', 'jruby', 'truffleruby']
|
46
|
+
steps:
|
47
|
+
- name: Install RabbitMQ
|
48
|
+
run: sudo apt-get update && sudo apt-get install -y rabbitmq-server
|
49
|
+
- name: Stop RabbitMQ
|
50
|
+
run: sudo systemctl stop rabbitmq-server
|
51
|
+
|
52
|
+
- name: Install github.com/FiloSottile/mkcert
|
53
|
+
run: brew install mkcert
|
54
|
+
- name: Create local CA
|
55
|
+
run: sudo CAROOT=/etc/rabbitmq $(brew --prefix)/bin/mkcert -install
|
56
|
+
- name: Create certificate
|
57
|
+
run: |
|
58
|
+
sudo $(brew --prefix)/bin/mkcert -key-file /etc/rabbitmq/localhost-key.pem -cert-file /etc/rabbitmq/localhost.pem localhost
|
59
|
+
sudo chmod +r /etc/rabbitmq/localhost-key.pem
|
60
|
+
- name: Create RabbitMQ config
|
61
|
+
run: |
|
62
|
+
sudo tee /etc/rabbitmq/rabbitmq.conf <<'EOF'
|
63
|
+
listeners.ssl.default = 5671
|
64
|
+
ssl_options.cacertfile = /etc/rabbitmq/rootCA.pem
|
65
|
+
ssl_options.certfile = /etc/rabbitmq/localhost.pem
|
66
|
+
ssl_options.keyfile = /etc/rabbitmq/localhost-key.pem
|
67
|
+
EOF
|
68
|
+
- name: Start RabbitMQ
|
69
|
+
run: sudo systemctl start rabbitmq-server
|
70
|
+
- name: Verify RabbitMQ started correctly
|
71
|
+
run: while true; do sudo rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
|
72
|
+
|
73
|
+
- uses: actions/checkout@v2
|
74
|
+
- name: Set up Ruby
|
75
|
+
uses: ruby/setup-ruby@v1
|
76
|
+
with:
|
77
|
+
bundler-cache: true
|
78
|
+
ruby-version: ${{ matrix.ruby }}
|
79
|
+
- name: Run TLS tests
|
80
|
+
run: bundle exec rake
|
81
|
+
env:
|
82
|
+
TESTOPTS: --name=/_tls$/
|
83
|
+
macos:
|
84
|
+
runs-on: macos-latest
|
85
|
+
timeout-minutes: 5
|
86
|
+
strategy:
|
87
|
+
fail-fast: false
|
88
|
+
matrix:
|
89
|
+
ruby: ['3.0']
|
90
|
+
steps:
|
91
|
+
- name: Install RabbitMQ
|
92
|
+
run: brew install rabbitmq
|
93
|
+
- name: Start RabbitMQ
|
94
|
+
run: brew services start rabbitmq
|
95
|
+
- uses: actions/checkout@v2
|
96
|
+
- name: Set up Ruby
|
97
|
+
uses: ruby/setup-ruby@v1
|
98
|
+
with:
|
99
|
+
bundler-cache: true
|
100
|
+
ruby-version: ${{ matrix.ruby }}
|
101
|
+
- name: Verify RabbitMQ started correctly
|
102
|
+
run: while true; do rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
|
103
|
+
- name: Run tests (excluding TLS tests)
|
104
|
+
run: bundle exec rake
|
data/.rubocop_todo.yml
CHANGED
@@ -1,15 +1,27 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2021-
|
3
|
+
# on 2021-10-15 13:44:24 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:
|
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: 132
|
15
|
+
|
16
|
+
# Offense count: 1
|
17
|
+
Lint/RescueException:
|
18
|
+
Exclude:
|
19
|
+
- 'lib/amqp/client/connection.rb'
|
20
|
+
|
21
|
+
# Offense count: 32
|
10
22
|
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
|
11
23
|
Metrics/AbcSize:
|
12
|
-
Max:
|
24
|
+
Max: 175
|
13
25
|
|
14
26
|
# Offense count: 1
|
15
27
|
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
@@ -22,27 +34,32 @@ Metrics/BlockLength:
|
|
22
34
|
Metrics/BlockNesting:
|
23
35
|
Max: 4
|
24
36
|
|
25
|
-
# Offense count:
|
37
|
+
# Offense count: 6
|
26
38
|
# Configuration parameters: CountComments, CountAsOne.
|
27
39
|
Metrics/ClassLength:
|
28
|
-
Max:
|
40
|
+
Max: 497
|
29
41
|
|
30
|
-
# Offense count:
|
42
|
+
# Offense count: 10
|
31
43
|
# Configuration parameters: IgnoredMethods.
|
32
44
|
Metrics/CyclomaticComplexity:
|
33
|
-
Max:
|
45
|
+
Max: 46
|
34
46
|
|
35
|
-
# Offense count:
|
47
|
+
# Offense count: 67
|
36
48
|
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
37
49
|
Metrics/MethodLength:
|
38
|
-
Max:
|
50
|
+
Max: 169
|
39
51
|
|
40
52
|
# Offense count: 2
|
41
53
|
# Configuration parameters: CountComments, CountAsOne.
|
42
54
|
Metrics/ModuleLength:
|
43
|
-
Max:
|
55
|
+
Max: 486
|
56
|
+
|
57
|
+
# Offense count: 1
|
58
|
+
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
59
|
+
Metrics/ParameterLists:
|
60
|
+
Max: 13
|
44
61
|
|
45
|
-
# Offense count:
|
62
|
+
# Offense count: 5
|
46
63
|
# Configuration parameters: IgnoredMethods.
|
47
64
|
Metrics/PerceivedComplexity:
|
48
|
-
Max:
|
65
|
+
Max: 23
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.1.4] - 2021-12-27
|
4
|
+
|
5
|
+
- Fixed: Ruby 3.1.0 compability, StringIO have to be required manually
|
6
|
+
|
7
|
+
## [1.1.3] - 2021-11-04
|
8
|
+
|
9
|
+
- Fixed: Reraise SystemcallError in connect so that reconnect works
|
10
|
+
- Fixed: Keepalive support in OS X
|
11
|
+
- Added: Make keepalive settings configurable (eg. amqp://?keepalive=60:10:3)
|
12
|
+
|
13
|
+
## [1.1.2] - 2021-10-15
|
14
|
+
|
15
|
+
- Added: Support for JRuby and TruffleRuby
|
16
|
+
|
17
|
+
## [1.1.1] - 2021-09-15
|
18
|
+
|
19
|
+
- Added: Examples in the documentation
|
20
|
+
- Added: Faster Properties and Table encoding and decoding
|
21
|
+
|
3
22
|
## [1.1.0] - 2021-09-08
|
4
23
|
|
5
24
|
- 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.
|
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
|
41
|
+
msg = ch.basic_get(q.queue_name)
|
38
42
|
|
39
43
|
# Print the message's body to STDOUT
|
40
44
|
puts msg.body
|
@@ -76,9 +80,12 @@ amqp.publish(Zlib.gzip("an event"), "amq.topic", "my.event", content_encoding: '
|
|
76
80
|
|
77
81
|
All maintained Ruby versions are supported.
|
78
82
|
|
83
|
+
- 3.1
|
79
84
|
- 3.0
|
80
85
|
- 2.7
|
81
86
|
- 2.6
|
87
|
+
- jruby
|
88
|
+
- truffleruby
|
82
89
|
|
83
90
|
## Installation
|
84
91
|
|
@@ -104,7 +111,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
104
111
|
|
105
112
|
## Contributing
|
106
113
|
|
107
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/cloudamqp/amqp-client.rb
|
114
|
+
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
115
|
|
109
116
|
## License
|
110
117
|
|
data/Rakefile
CHANGED
@@ -4,9 +4,16 @@ require "bundler/gem_tasks"
|
|
4
4
|
require "rake/testtask"
|
5
5
|
|
6
6
|
Rake::TestTask.new(:test) do |t|
|
7
|
-
t.
|
8
|
-
t.
|
9
|
-
t.
|
7
|
+
t.description = "Run all but TLS tests"
|
8
|
+
t.options = "--exclude=/_tls$/"
|
9
|
+
t.pattern = "test/**/*_test.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
namespace :test do
|
13
|
+
Rake::TestTask.new(:all) do |t|
|
14
|
+
t.description = "Run all tests"
|
15
|
+
t.pattern = "test/**/*_test.rb"
|
16
|
+
end
|
10
17
|
end
|
11
18
|
|
12
19
|
require "rubocop/rake_task"
|
@@ -15,4 +22,4 @@ RuboCop::RakeTask.new do |task|
|
|
15
22
|
task.requires << "rubocop-minitest"
|
16
23
|
end
|
17
24
|
|
18
|
-
task default:
|
25
|
+
task default: [:test, *(:rubocop if RUBY_ENGINE == "ruby")]
|
data/lib/amqp/client/channel.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "socket"
|
4
4
|
require "uri"
|
5
5
|
require "openssl"
|
6
|
-
require_relative "./
|
6
|
+
require_relative "./frame_bytes"
|
7
7
|
require_relative "./channel"
|
8
8
|
require_relative "./errors"
|
9
9
|
|
@@ -24,6 +24,7 @@ module AMQP
|
|
24
24
|
# the smallest of the client's and the broker's values will be used
|
25
25
|
# @option options [Integer] channel_max (2048) Maxium number of channels the client will be allowed to have open.
|
26
26
|
# Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
|
27
|
+
# @option options [String] keepalive (60:10:3) TCP keepalive setting, 60s idle, 10s interval between probes, 3 probes
|
27
28
|
# @return [Connection]
|
28
29
|
def initialize(uri = "", read_loop_thread: true, **options)
|
29
30
|
uri = URI.parse(uri)
|
@@ -133,9 +134,13 @@ module AMQP
|
|
133
134
|
warn "AMQP-Client blocked by broker: #{blocked}" if blocked
|
134
135
|
@write_lock.synchronize do
|
135
136
|
warn "AMQP-Client unblocked by broker" if blocked
|
136
|
-
|
137
|
+
if RUBY_ENGINE == "truffleruby"
|
138
|
+
bytes.each { |b| @socket.write b }
|
139
|
+
else
|
140
|
+
@socket.write(*bytes)
|
141
|
+
end
|
137
142
|
end
|
138
|
-
rescue
|
143
|
+
rescue *READ_EXCEPTIONS => e
|
139
144
|
raise Error::ConnectionClosed.new(*@closed) if @closed
|
140
145
|
|
141
146
|
raise Error, "Could not write to socket, #{e.message}"
|
@@ -152,12 +157,12 @@ module AMQP
|
|
152
157
|
frame_start = String.new(capacity: 7)
|
153
158
|
frame_buffer = String.new(capacity: frame_max)
|
154
159
|
loop do
|
155
|
-
socket.read(7, frame_start)
|
160
|
+
socket.read(7, frame_start) || raise(IOError)
|
156
161
|
type, channel_id, frame_size = frame_start.unpack("C S> L>")
|
157
162
|
frame_max >= frame_size || raise(Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}")
|
158
163
|
|
159
164
|
# read the frame content
|
160
|
-
socket.read(frame_size, frame_buffer)
|
165
|
+
socket.read(frame_size, frame_buffer) || raise(IOError)
|
161
166
|
|
162
167
|
# make sure that the frame end is correct
|
163
168
|
frame_end = socket.readchar.ord
|
@@ -167,7 +172,7 @@ module AMQP
|
|
167
172
|
parse_frame(type, channel_id, frame_buffer) || return
|
168
173
|
end
|
169
174
|
nil
|
170
|
-
rescue
|
175
|
+
rescue *READ_EXCEPTIONS => e
|
171
176
|
@closed ||= [400, "read error: #{e.message}"]
|
172
177
|
nil # ignore read errors
|
173
178
|
ensure
|
@@ -177,13 +182,16 @@ module AMQP
|
|
177
182
|
@write_lock.synchronize do
|
178
183
|
@socket.close
|
179
184
|
end
|
180
|
-
rescue
|
185
|
+
rescue *READ_EXCEPTIONS
|
181
186
|
nil
|
182
187
|
end
|
183
188
|
end
|
184
189
|
|
185
190
|
private
|
186
191
|
|
192
|
+
READ_EXCEPTIONS = [IOError, OpenSSL::OpenSSLError, SystemCallError,
|
193
|
+
RUBY_ENGINE == "jruby" ? java.lang.NullPointerException : nil].compact.freeze
|
194
|
+
|
187
195
|
def parse_frame(type, channel_id, buf)
|
188
196
|
case type
|
189
197
|
when 1 # method frame
|
@@ -350,7 +358,7 @@ module AMQP
|
|
350
358
|
end
|
351
359
|
when 2 # header
|
352
360
|
body_size = buf.unpack1("@4 Q>")
|
353
|
-
properties = Properties.decode(buf
|
361
|
+
properties = Properties.decode(buf, 12)
|
354
362
|
@channels[channel_id].header_delivered body_size, properties
|
355
363
|
when 3 # body
|
356
364
|
@channels[channel_id].body_delivered buf
|
@@ -376,7 +384,8 @@ module AMQP
|
|
376
384
|
def open_socket(host, port, tls, options)
|
377
385
|
connect_timeout = options.fetch(:connect_timeout, 30).to_i
|
378
386
|
socket = Socket.tcp host, port, connect_timeout: connect_timeout
|
379
|
-
|
387
|
+
keepalive = options.fetch(:keepalive, "").split(":", 3).map!(&:to_i)
|
388
|
+
enable_tcp_keepalive(socket, *keepalive)
|
380
389
|
if tls
|
381
390
|
cert_store = OpenSSL::X509::Store.new
|
382
391
|
cert_store.set_default_paths
|
@@ -391,6 +400,8 @@ module AMQP
|
|
391
400
|
socket.post_connection_check(host) || raise(Error, "TLS certificate hostname doesn't match requested")
|
392
401
|
end
|
393
402
|
socket
|
403
|
+
rescue SystemCallError, OpenSSL::OpenSSLError => e
|
404
|
+
raise Error, "Could not open a socket: #{e.message}"
|
394
405
|
end
|
395
406
|
|
396
407
|
# Negotiate a connection
|
@@ -402,7 +413,7 @@ module AMQP
|
|
402
413
|
loop do
|
403
414
|
begin
|
404
415
|
socket.readpartial(4096, buf)
|
405
|
-
rescue
|
416
|
+
rescue *READ_EXCEPTIONS => e
|
406
417
|
raise Error, "Could not establish AMQP connection: #{e.message}"
|
407
418
|
end
|
408
419
|
|
@@ -444,10 +455,10 @@ module AMQP
|
|
444
455
|
else raise Error, "Unexpected frame type: #{type}"
|
445
456
|
end
|
446
457
|
end
|
447
|
-
rescue
|
458
|
+
rescue Exception => e
|
448
459
|
begin
|
449
460
|
socket.close
|
450
|
-
rescue
|
461
|
+
rescue *READ_EXCEPTIONS
|
451
462
|
nil
|
452
463
|
end
|
453
464
|
raise e
|
@@ -455,11 +466,18 @@ module AMQP
|
|
455
466
|
|
456
467
|
# Enable TCP keepalive, which is prefered to heartbeats
|
457
468
|
# @return [void]
|
458
|
-
def enable_tcp_keepalive(socket)
|
469
|
+
def enable_tcp_keepalive(socket, idle = 60, interval = 10, count = 3)
|
459
470
|
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
460
|
-
|
461
|
-
|
462
|
-
|
471
|
+
if Socket.const_defined?(:TCP_KEEPIDLE) # linux/bsd
|
472
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPIDLE, idle)
|
473
|
+
elsif RUBY_PLATFORM.include? "darwin" # os x
|
474
|
+
# https://www.quickhack.net/nom/blog/2018-01-19-enable-tcp-keepalive-of-macos-and-linux-in-ruby.html
|
475
|
+
socket.setsockopt(Socket::IPPROTO_TCP, 0x10, idle)
|
476
|
+
else # windows
|
477
|
+
return
|
478
|
+
end
|
479
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, interval)
|
480
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, count)
|
463
481
|
rescue StandardError => e
|
464
482
|
warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
|
465
483
|
end
|
@@ -6,8 +6,10 @@ module AMQP
|
|
6
6
|
class Client
|
7
7
|
# Encode/decode AMQP Properties
|
8
8
|
class Properties
|
9
|
-
|
10
|
-
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
10
|
+
def initialize(content_type: nil, content_encoding: nil, headers: nil, delivery_mode: nil,
|
11
|
+
priority: nil, correlation_id: nil, reply_to: nil, expiration: nil,
|
12
|
+
message_id: nil, timestamp: nil, type: nil, user_id: nil, app_id: nil)
|
11
13
|
@content_type = content_type
|
12
14
|
@content_encoding = content_encoding
|
13
15
|
@headers = headers
|
@@ -22,6 +24,37 @@ module AMQP
|
|
22
24
|
@user_id = user_id
|
23
25
|
@app_id = app_id
|
24
26
|
end
|
27
|
+
# rubocop:enable Metrics/ParameterLists
|
28
|
+
|
29
|
+
# Properties as a Hash
|
30
|
+
# @return [Hash] Properties
|
31
|
+
def to_h
|
32
|
+
{
|
33
|
+
content_type: content_type,
|
34
|
+
content_encoding: content_encoding,
|
35
|
+
headers: headers,
|
36
|
+
delivery_mode: delivery_mode,
|
37
|
+
priority: priority,
|
38
|
+
correlation_id: correlation_id,
|
39
|
+
reply_to: reply_to,
|
40
|
+
expiration: expiration,
|
41
|
+
message_id: message_id,
|
42
|
+
timestamp: timestamp,
|
43
|
+
type: type,
|
44
|
+
user_id: user_id,
|
45
|
+
app_id: app_id
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
alias to_hash to_h
|
50
|
+
|
51
|
+
# Returns true if two Property objects holds the same information
|
52
|
+
# @return [Boolean]
|
53
|
+
def ==(other)
|
54
|
+
return false unless other.is_a? self.class
|
55
|
+
|
56
|
+
instance_variables.all? { |v| instance_variable_get(v) == other.instance_variable_get(v) }
|
57
|
+
end
|
25
58
|
|
26
59
|
# Content type of the message body
|
27
60
|
# @return [String, nil]
|
@@ -29,50 +62,54 @@ module AMQP
|
|
29
62
|
# Content encoding of the body
|
30
63
|
# @return [String, nil]
|
31
64
|
attr_accessor :content_encoding
|
32
|
-
#
|
65
|
+
# Headers, for applications and header exchange routing
|
33
66
|
# @return [Hash<String, Object>, nil]
|
34
67
|
attr_accessor :headers
|
35
|
-
#
|
36
|
-
# @
|
68
|
+
# Message persistent level
|
69
|
+
# @note The exchange and queue have to durable as well for the message to be persistent
|
70
|
+
# @return [1] Transient message
|
71
|
+
# @return [2] Persistent message
|
72
|
+
# @return [nil] Not specified (implicitly transient)
|
37
73
|
attr_accessor :delivery_mode
|
38
74
|
# A priority of the message (between 0 and 255)
|
39
75
|
# @return [Integer, nil]
|
40
76
|
attr_accessor :priority
|
41
|
-
#
|
42
|
-
# @return [
|
77
|
+
# Message correlation id, commonly used to correlate RPC requests and responses
|
78
|
+
# @return [String, nil]
|
43
79
|
attr_accessor :correlation_id
|
44
80
|
# Queue to reply RPC responses to
|
45
81
|
# @return [String, nil]
|
46
82
|
attr_accessor :reply_to
|
47
83
|
# Number of seconds the message will stay in the queue
|
48
|
-
# @return [
|
49
|
-
# @return [String]
|
50
|
-
# @return [nil]
|
84
|
+
# @return [String, nil]
|
51
85
|
attr_accessor :expiration
|
86
|
+
# Application message identifier
|
52
87
|
# @return [String, nil]
|
53
88
|
attr_accessor :message_id
|
54
|
-
#
|
89
|
+
# Message timestamp, often indicates when the message was originally generated
|
55
90
|
# @return [Date, nil]
|
56
91
|
attr_accessor :timestamp
|
57
|
-
#
|
92
|
+
# Message type name
|
58
93
|
# @return [String, nil]
|
59
94
|
attr_accessor :type
|
60
|
-
#
|
95
|
+
# The user that published the message
|
61
96
|
# @return [String, nil]
|
62
97
|
attr_accessor :user_id
|
63
|
-
#
|
98
|
+
# Name of application that generated the message
|
64
99
|
# @return [String, nil]
|
65
100
|
attr_accessor :app_id
|
66
101
|
|
67
102
|
# Encode properties into a byte array
|
103
|
+
# @param properties [Hash]
|
68
104
|
# @return [String] byte array
|
69
|
-
def encode
|
105
|
+
def self.encode(properties)
|
106
|
+
return "\x00\x00" if properties.empty?
|
107
|
+
|
70
108
|
flags = 0
|
71
109
|
arr = [flags]
|
72
|
-
fmt =
|
73
|
-
fmt.pos = 2
|
110
|
+
fmt = String.new("S>", capacity: 37)
|
74
111
|
|
75
|
-
if content_type
|
112
|
+
if (content_type = properties[:content_type])
|
76
113
|
content_type.is_a?(String) || raise(ArgumentError, "content_type must be a string")
|
77
114
|
|
78
115
|
flags |= (1 << 15)
|
@@ -80,7 +117,7 @@ module AMQP
|
|
80
117
|
fmt << "Ca*"
|
81
118
|
end
|
82
119
|
|
83
|
-
if content_encoding
|
120
|
+
if (content_encoding = properties[:content_encoding])
|
84
121
|
content_encoding.is_a?(String) || raise(ArgumentError, "content_encoding must be a string")
|
85
122
|
|
86
123
|
flags |= (1 << 14)
|
@@ -88,7 +125,7 @@ module AMQP
|
|
88
125
|
fmt << "Ca*"
|
89
126
|
end
|
90
127
|
|
91
|
-
if headers
|
128
|
+
if (headers = properties[:headers])
|
92
129
|
headers.is_a?(Hash) || raise(ArgumentError, "headers must be a hash")
|
93
130
|
|
94
131
|
flags |= (1 << 13)
|
@@ -97,31 +134,31 @@ module AMQP
|
|
97
134
|
fmt << "L>a*"
|
98
135
|
end
|
99
136
|
|
100
|
-
if delivery_mode
|
137
|
+
if (delivery_mode = properties[:delivery_mode])
|
101
138
|
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
139
|
|
104
140
|
flags |= (1 << 12)
|
105
141
|
arr << delivery_mode
|
106
142
|
fmt << "C"
|
107
143
|
end
|
108
144
|
|
109
|
-
if priority
|
145
|
+
if (priority = properties[:priority])
|
110
146
|
priority.is_a?(Integer) || raise(ArgumentError, "priority must be an int")
|
147
|
+
|
111
148
|
flags |= (1 << 11)
|
112
149
|
arr << priority
|
113
150
|
fmt << "C"
|
114
151
|
end
|
115
152
|
|
116
|
-
if correlation_id
|
117
|
-
|
153
|
+
if (correlation_id = properties[:correlation_id])
|
154
|
+
correlation_id.is_a?(String) || raise(ArgumentError, "correlation_id must be a string")
|
118
155
|
|
119
156
|
flags |= (1 << 10)
|
120
157
|
arr << correlation_id.bytesize << correlation_id
|
121
158
|
fmt << "Ca*"
|
122
159
|
end
|
123
160
|
|
124
|
-
if reply_to
|
161
|
+
if (reply_to = properties[:reply_to])
|
125
162
|
reply_to.is_a?(String) || raise(ArgumentError, "reply_to must be a string")
|
126
163
|
|
127
164
|
flags |= (1 << 9)
|
@@ -129,8 +166,8 @@ module AMQP
|
|
129
166
|
fmt << "Ca*"
|
130
167
|
end
|
131
168
|
|
132
|
-
if expiration
|
133
|
-
|
169
|
+
if (expiration = properties[:expiration])
|
170
|
+
expiration = expiration.to_s if expiration.is_a?(Integer)
|
134
171
|
expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string or integer")
|
135
172
|
|
136
173
|
flags |= (1 << 8)
|
@@ -138,7 +175,7 @@ module AMQP
|
|
138
175
|
fmt << "Ca*"
|
139
176
|
end
|
140
177
|
|
141
|
-
if message_id
|
178
|
+
if (message_id = properties[:message_id])
|
142
179
|
message_id.is_a?(String) || raise(ArgumentError, "message_id must be a string")
|
143
180
|
|
144
181
|
flags |= (1 << 7)
|
@@ -146,7 +183,7 @@ module AMQP
|
|
146
183
|
fmt << "Ca*"
|
147
184
|
end
|
148
185
|
|
149
|
-
if timestamp
|
186
|
+
if (timestamp = properties[:timestamp])
|
150
187
|
timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
|
151
188
|
|
152
189
|
flags |= (1 << 6)
|
@@ -154,7 +191,7 @@ module AMQP
|
|
154
191
|
fmt << "Q>"
|
155
192
|
end
|
156
193
|
|
157
|
-
if type
|
194
|
+
if (type = properties[:type])
|
158
195
|
type.is_a?(String) || raise(ArgumentError, "type must be a string")
|
159
196
|
|
160
197
|
flags |= (1 << 5)
|
@@ -162,7 +199,7 @@ module AMQP
|
|
162
199
|
fmt << "Ca*"
|
163
200
|
end
|
164
201
|
|
165
|
-
if user_id
|
202
|
+
if (user_id = properties[:user_id])
|
166
203
|
user_id.is_a?(String) || raise(ArgumentError, "user_id must be a string")
|
167
204
|
|
168
205
|
flags |= (1 << 4)
|
@@ -170,7 +207,7 @@ module AMQP
|
|
170
207
|
fmt << "Ca*"
|
171
208
|
end
|
172
209
|
|
173
|
-
if app_id
|
210
|
+
if (app_id = properties[:app_id])
|
174
211
|
app_id.is_a?(String) || raise(ArgumentError, "app_id must be a string")
|
175
212
|
|
176
213
|
flags |= (1 << 3)
|
@@ -179,15 +216,15 @@ module AMQP
|
|
179
216
|
end
|
180
217
|
|
181
218
|
arr[0] = flags
|
182
|
-
arr.pack(fmt
|
219
|
+
arr.pack(fmt)
|
183
220
|
end
|
184
221
|
|
185
222
|
# Decode a byte array
|
186
223
|
# @return [Properties]
|
187
|
-
def self.decode(bytes)
|
224
|
+
def self.decode(bytes, pos = 0)
|
188
225
|
p = new
|
189
|
-
flags = bytes.unpack1("S>")
|
190
|
-
pos
|
226
|
+
flags = bytes.byteslice(pos, 2).unpack1("S>")
|
227
|
+
pos += 2
|
191
228
|
if (flags & 0x8000).positive?
|
192
229
|
len = bytes.getbyte(pos)
|
193
230
|
pos += 1
|
data/lib/amqp/client/table.rb
CHANGED
@@ -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
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
18
|
+
arr.push(key.bytesize, key)
|
19
|
+
fmt << "Ca*"
|
20
|
+
encode_field(value, arr, fmt)
|
16
21
|
end
|
17
|
-
|
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
|
-
|
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
|
-
|
49
|
+
arr.push("l", value)
|
50
|
+
fmt << "aq>"
|
45
51
|
else
|
46
|
-
|
52
|
+
arr.push("I", value)
|
53
|
+
fmt << "al>"
|
47
54
|
end
|
48
55
|
when Float
|
49
|
-
|
56
|
+
arr.push("d", value)
|
57
|
+
fmt << "aG"
|
50
58
|
when String
|
51
|
-
|
59
|
+
arr.push("S", value.bytesize, value)
|
60
|
+
fmt << "aL>a*"
|
52
61
|
when Time
|
53
|
-
|
62
|
+
arr.push("T", value.to_i)
|
63
|
+
fmt << "aQ>"
|
54
64
|
when Array
|
55
|
-
|
56
|
-
|
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
|
-
|
73
|
+
arr.push("F", bytes.bytesize, bytes)
|
74
|
+
fmt << "aL>a*"
|
60
75
|
when true
|
61
|
-
|
76
|
+
arr.push("t", 1)
|
77
|
+
fmt << "aC"
|
62
78
|
when false
|
63
|
-
|
79
|
+
arr.push("t", 0)
|
80
|
+
fmt << "aC"
|
64
81
|
when nil
|
65
|
-
|
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
|
90
|
+
# @return [Array<Integer, Object>] Bytes read and the parsed value
|
72
91
|
# @api private
|
73
|
-
def self.decode_field(bytes)
|
74
|
-
type = bytes[
|
75
|
-
pos
|
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 <
|
89
|
-
length, value = decode_field(bytes
|
109
|
+
while pos < array_end
|
110
|
+
length, value = decode_field(bytes, pos)
|
90
111
|
pos += length + 1
|
91
112
|
a << value
|
92
113
|
end
|
data/lib/amqp/client/version.rb
CHANGED
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)
|
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.
|
4
|
+
version: 1.1.4
|
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-
|
11
|
+
date: 2021-12-27 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/
|
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
|
@@ -66,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
66
|
- !ruby/object:Gem::Version
|
67
67
|
version: '0'
|
68
68
|
requirements: []
|
69
|
-
rubygems_version: 3.
|
69
|
+
rubygems_version: 3.3.3
|
70
70
|
signing_key:
|
71
71
|
specification_version: 4
|
72
72
|
summary: AMQP 0-9-1 client
|