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 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