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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec3129a38420f19de4c0225aea1eaefc320463c6e032c54990a998f7654f8347
4
- data.tar.gz: 3c8da5fccb818730958f47fdaf78837524381b116a322adaf585ee02d0492b2d
3
+ metadata.gz: 0e287ef0dee4c9ec581690733c12c132f74c595ceddead1ba5b95b765138e11c
4
+ data.tar.gz: e5462c8cd2a4f7232a51e17d9ec93f4fb2ea88a922bb9b5cc58242cced78d61e
5
5
  SHA512:
6
- metadata.gz: 24b592bb7fc50f29499e32ca6348f2aa25bac9837e694740346fa24e1fe6c45c9f9ab6497f066ca6491c6f09e558b29ac3a1d84f5b31a3b2b68bb8c28cdf76e7
7
- data.tar.gz: 6ad3e059d311894f4a023d75df89c72beb4508a2abc83083a47f4f7a07cc6f98edc157e06b5ed42eda55458612d89206de1cce4b0cc94fd598facad4433083e9
6
+ metadata.gz: 923fb64c7234fb6718f7dbc5eb33c1f8de03a9cf12d669539935cc6ef83f4ca387afc80c1e56fcc2ed167dc14381cda1d7b6a67673c6516818909cb6a84c287d
7
+ data.tar.gz: 14c4d9157cee844609eeb5beed70ee2560ba3442c5afb857fed3e0945c443910f7bd4a281e327d2cb5ee0f0896facf4c8e693e5d3ecf1152c07474e20f8a724f
@@ -4,6 +4,9 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
+ paths:
8
+ - 'src/**'
9
+ - 'README.md'
7
10
 
8
11
  jobs:
9
12
  docs:
@@ -3,8 +3,9 @@ name: Ruby
3
3
  on: [push,pull_request]
4
4
 
5
5
  jobs:
6
- build:
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 the default task
30
- run: |
31
- bundle install
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-09-06 19:39:47 UTC using RuboCop version 1.19.1.
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: 18
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: 179
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: 5
37
+ # Offense count: 6
26
38
  # Configuration parameters: CountComments, CountAsOne.
27
39
  Metrics/ClassLength:
28
- Max: 400
40
+ Max: 497
29
41
 
30
- # Offense count: 9
42
+ # Offense count: 10
31
43
  # Configuration parameters: IgnoredMethods.
32
44
  Metrics/CyclomaticComplexity:
33
- Max: 44
45
+ Max: 46
34
46
 
35
- # Offense count: 40
47
+ # Offense count: 67
36
48
  # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
37
49
  Metrics/MethodLength:
38
- Max: 170
50
+ Max: 169
39
51
 
40
52
  # Offense count: 2
41
53
  # Configuration parameters: CountComments, CountAsOne.
42
54
  Metrics/ModuleLength:
43
- Max: 500
55
+ Max: 486
56
+
57
+ # Offense count: 1
58
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
59
+ Metrics/ParameterLists:
60
+ Max: 13
44
61
 
45
- # Offense count: 4
62
+ # Offense count: 5
46
63
  # Configuration parameters: IgnoredMethods.
47
64
  Metrics/PerceivedComplexity:
48
- Max: 22
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.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
@@ -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.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb"]
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: %i[test rubocop]
25
+ task default: [:test, *(:rubocop if RUBY_ENGINE == "ruby")]
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "./message"
4
+ require "stringio"
4
5
 
5
6
  module AMQP
6
7
  class Client
@@ -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
 
@@ -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
- @socket.write(*bytes)
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 IOError, OpenSSL::OpenSSLError, SystemCallError => e
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 IOError, OpenSSL::OpenSSLError, SystemCallError => e
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 IOError, OpenSSL::OpenSSLError, SystemCallError
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.byteslice(12, buf.bytesize - 12))
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
- enable_tcp_keepalive(socket)
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 IOError, OpenSSL::OpenSSLError, SystemCallError => e
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 StandardError => e
458
+ rescue Exception => e
448
459
  begin
449
460
  socket.close
450
- rescue IOError, OpenSSL::OpenSSLError, SystemCallError
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
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
461
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
462
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
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
@@ -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
@@ -6,8 +6,10 @@ module AMQP
6
6
  class Client
7
7
  # Encode/decode AMQP Properties
8
8
  class Properties
9
- def initialize(content_type: nil, content_encoding: nil, headers: nil, delivery_mode: nil, priority: nil, correlation_id: nil,
10
- reply_to: nil, expiration: nil, message_id: nil, timestamp: nil, type: nil, user_id: nil, app_id: nil)
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
- # Custom headers
65
+ # Headers, for applications and header exchange routing
33
66
  # @return [Hash<String, Object>, nil]
34
67
  attr_accessor :headers
35
- # 2 for persisted message, transient messages for all other values
36
- # @return [Integer, nil]
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
- # A correlation id, most often used used for RPC communication
42
- # @return [Integer, nil]
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 [Integer]
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
- # User-definable, but often used for the time the message was originally generated
89
+ # Message timestamp, often indicates when the message was originally generated
55
90
  # @return [Date, nil]
56
91
  attr_accessor :timestamp
57
- # User-definable, but can can indicate what kind of message this is
92
+ # Message type name
58
93
  # @return [String, nil]
59
94
  attr_accessor :type
60
- # User-definable, but can be used to verify that this is the user that published the message
95
+ # The user that published the message
61
96
  # @return [String, nil]
62
97
  attr_accessor :user_id
63
- # User-definable, but often indicates which app that generated the message
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 = StringIO.new(String.new("S>", capacity: 35))
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
- priority.is_a?(String) || raise(ArgumentError, "correlation_id must be a string")
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
- self.expiration = expiration.to_s if expiration.is_a?(Integer)
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.string)
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 = 2
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
@@ -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.4"
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.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-09-08 00:00:00.000000000 Z
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/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
@@ -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.2.22
69
+ rubygems_version: 3.3.3
70
70
  signing_key:
71
71
  specification_version: 4
72
72
  summary: AMQP 0-9-1 client