amqp-client 1.1.0 → 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|