amqp-client 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +19 -2
- data/.gitignore +1 -0
- data/.rubocop_todo.yml +39 -33
- data/CHANGELOG.md +5 -0
- data/README.md +31 -0
- data/lib/amqp/client.rb +145 -77
- data/lib/amqp/client/channel.rb +239 -32
- data/lib/amqp/client/connection.rb +293 -42
- data/lib/amqp/client/errors.rb +7 -2
- data/lib/amqp/client/frames.rb +361 -16
- data/lib/amqp/client/message.rb +11 -1
- data/lib/amqp/client/properties.rb +201 -0
- data/lib/amqp/client/table.rb +123 -0
- data/lib/amqp/client/version.rb +1 -1
- metadata +4 -3
- data/Gemfile.lock +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 337f82d8ebaf862fd8fd1acd17f9b71be2aaee6de02f0c7c59786b222d5c5c0c
|
4
|
+
data.tar.gz: 040adffbda90616864a845517a1904efa5c1dbb25e30e63febe3f34b3cf4f752
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5700b885356d0c36060ffb4a9ad819bc26672a3d89fef6283cb5c4789ae3ddeafeaa57917f3001947fbdc38954eabfb138b8a98ed4a187a66912e5fb47bb3200
|
7
|
+
data.tar.gz: 034c2aa88d93bf45063d41b87e92cb20545deadf7c98930a7b6626e448a84036d234274c8c1066a85e6fd35a1274dfb392128a037f6fac7a330aee0af9907a74
|
data/.github/workflows/main.yml
CHANGED
@@ -5,14 +5,31 @@ on: [push,pull_request]
|
|
5
5
|
jobs:
|
6
6
|
build:
|
7
7
|
runs-on: ubuntu-latest
|
8
|
+
services:
|
9
|
+
rabbitmq:
|
10
|
+
image: rabbitmq:latest
|
11
|
+
ports:
|
12
|
+
- 5672/tcp
|
13
|
+
# needed because the rabbitmq container does not provide a healthcheck
|
14
|
+
options: >-
|
15
|
+
--health-cmd "rabbitmqctl node_health_check"
|
16
|
+
--health-interval 10s
|
17
|
+
--health-timeout 5s
|
18
|
+
--health-retries 5
|
19
|
+
strategy:
|
20
|
+
fail-fast: false
|
21
|
+
matrix:
|
22
|
+
ruby: ['2.7', '3.0']
|
8
23
|
steps:
|
9
24
|
- uses: actions/checkout@v2
|
10
25
|
- name: Set up Ruby
|
11
26
|
uses: ruby/setup-ruby@v1
|
12
27
|
with:
|
13
|
-
ruby-version:
|
28
|
+
ruby-version: ${{ matrix.ruby }}
|
14
29
|
- name: Run the default task
|
15
30
|
run: |
|
16
|
-
gem install bundler -v 2.2.15
|
17
31
|
bundle install
|
18
32
|
bundle exec rake
|
33
|
+
env:
|
34
|
+
AMQP_PORT: ${{ job.services.rabbitmq.ports[5672] }}
|
35
|
+
|
data/.gitignore
CHANGED
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2021-
|
3
|
+
# on 2021-06-01 23:54:39 UTC using RuboCop version 1.15.0.
|
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
|
@@ -11,64 +11,62 @@
|
|
11
11
|
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
12
12
|
# URISchemes: http, https
|
13
13
|
Layout/LineLength:
|
14
|
-
Max:
|
14
|
+
Max: 123
|
15
15
|
|
16
|
-
# Offense count:
|
17
|
-
Lint/ShadowedException:
|
18
|
-
Exclude:
|
19
|
-
- 'lib/amqp/client.rb'
|
20
|
-
- 'lib/amqp/client/connection.rb'
|
21
|
-
|
22
|
-
# Offense count: 2
|
23
|
-
# Cop supports --auto-correct.
|
24
|
-
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods.
|
25
|
-
Lint/UnusedMethodArgument:
|
26
|
-
Exclude:
|
27
|
-
- 'lib/amqp/client/channel.rb'
|
28
|
-
|
29
|
-
# Offense count: 6
|
16
|
+
# Offense count: 15
|
30
17
|
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
|
31
18
|
Metrics/AbcSize:
|
32
|
-
Max:
|
19
|
+
Max: 142
|
33
20
|
|
34
21
|
# Offense count: 1
|
35
22
|
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
36
23
|
# IgnoredMethods: refine
|
37
24
|
Metrics/BlockLength:
|
38
|
-
Max:
|
25
|
+
Max: 37
|
39
26
|
|
40
|
-
# Offense count:
|
27
|
+
# Offense count: 2
|
28
|
+
# Configuration parameters: CountBlocks.
|
29
|
+
Metrics/BlockNesting:
|
30
|
+
Max: 4
|
31
|
+
|
32
|
+
# Offense count: 2
|
41
33
|
# Configuration parameters: CountComments, CountAsOne.
|
42
34
|
Metrics/ClassLength:
|
43
|
-
Max:
|
35
|
+
Max: 191
|
44
36
|
|
45
|
-
# Offense count:
|
37
|
+
# Offense count: 9
|
46
38
|
# Configuration parameters: IgnoredMethods.
|
47
39
|
Metrics/CyclomaticComplexity:
|
48
|
-
Max:
|
40
|
+
Max: 30
|
49
41
|
|
50
|
-
# Offense count:
|
42
|
+
# Offense count: 26
|
51
43
|
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
52
44
|
Metrics/MethodLength:
|
53
|
-
Max:
|
45
|
+
Max: 124
|
54
46
|
|
55
|
-
# Offense count:
|
47
|
+
# Offense count: 3
|
48
|
+
# Configuration parameters: CountComments, CountAsOne.
|
49
|
+
Metrics/ModuleLength:
|
50
|
+
Max: 300
|
51
|
+
|
52
|
+
# Offense count: 5
|
56
53
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
57
54
|
Metrics/ParameterLists:
|
58
|
-
Max:
|
55
|
+
Max: 7
|
59
56
|
|
60
|
-
# Offense count:
|
57
|
+
# Offense count: 7
|
61
58
|
# Configuration parameters: IgnoredMethods.
|
62
59
|
Metrics/PerceivedComplexity:
|
63
|
-
Max:
|
60
|
+
Max: 18
|
64
61
|
|
65
62
|
# Offense count: 1
|
66
63
|
# Cop supports --auto-correct.
|
67
|
-
# Configuration parameters: EnforcedStyle.
|
68
|
-
# SupportedStyles:
|
69
|
-
Style/
|
64
|
+
# Configuration parameters: EnforcedStyle, IgnoredMethods.
|
65
|
+
# SupportedStyles: predicate, comparison
|
66
|
+
Style/NumericPredicate:
|
70
67
|
Exclude:
|
71
|
-
- '
|
68
|
+
- 'spec/**/*'
|
69
|
+
- 'lib/amqp/client/channel.rb'
|
72
70
|
|
73
71
|
# Offense count: 1
|
74
72
|
# Cop supports --auto-correct.
|
@@ -78,9 +76,17 @@ Style/RescueStandardError:
|
|
78
76
|
Exclude:
|
79
77
|
- 'lib/amqp/client.rb'
|
80
78
|
|
79
|
+
# Offense count: 1
|
80
|
+
# Cop supports --auto-correct.
|
81
|
+
# Configuration parameters: EnforcedStyle.
|
82
|
+
# SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only
|
83
|
+
Style/YodaCondition:
|
84
|
+
Exclude:
|
85
|
+
- 'lib/amqp/client/channel.rb'
|
86
|
+
|
81
87
|
# Offense count: 1
|
82
88
|
# Cop supports --auto-correct.
|
83
89
|
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
84
90
|
# URISchemes: http, https
|
85
91
|
Layout/LineLength:
|
86
|
-
Max:
|
92
|
+
Max: 123
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -20,6 +20,8 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
+
Low level API
|
24
|
+
|
23
25
|
```ruby
|
24
26
|
require "amqp-client"
|
25
27
|
|
@@ -32,6 +34,35 @@ msg = ch.basic_get q[:queue_name]
|
|
32
34
|
puts msg.body
|
33
35
|
```
|
34
36
|
|
37
|
+
High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to be extreme throughput, be expect 100% delivery guarantees (messages might be deliviered twice, in the unlikely event of a connection loss between message publish and message confirmed by the server).
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
amqp = AMQP::Client.new("amqp://localhost")
|
41
|
+
amqp.start
|
42
|
+
|
43
|
+
# Declares a durable queue
|
44
|
+
q = amqp.queue("myqueue")
|
45
|
+
|
46
|
+
# Bind the queue to any exchange, with any binding key
|
47
|
+
q.bind("amq.topic", "my.events.*")
|
48
|
+
|
49
|
+
# The message will be reprocessed if the client lost connection to the server
|
50
|
+
# between the message arrived and the message was supposed to be ack:ed.
|
51
|
+
q.subscribe(prefetch: 20) do |msg|
|
52
|
+
process(JSON.parse(msg.body))
|
53
|
+
msg.ack
|
54
|
+
rescue
|
55
|
+
msg.reject(requeue: false)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Publish directly to the queue
|
59
|
+
q.publish { foo: "bar" }.to_json, content_type: "application/json"
|
60
|
+
|
61
|
+
# Publish to any exchange
|
62
|
+
amqp.publish("my message", "amq.topic", "topic.foo", headers: { foo: 'bar' })
|
63
|
+
amqp.publish(Zlib.gzip("an event"), "amq.topic", "my.event", content_encoding: 'gzip')
|
64
|
+
```
|
65
|
+
|
35
66
|
## Development
|
36
67
|
|
37
68
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/amqp/client.rb
CHANGED
@@ -1,101 +1,169 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "uri"
|
5
|
-
require "openssl"
|
3
|
+
require "set"
|
6
4
|
require_relative "client/version"
|
7
|
-
require_relative "client/errors"
|
8
|
-
require_relative "client/frames"
|
9
5
|
require_relative "client/connection"
|
10
|
-
require_relative "client/channel"
|
11
6
|
|
12
7
|
module AMQP
|
13
8
|
# AMQP 0-9-1 Client
|
14
9
|
class Client
|
15
|
-
def initialize(uri)
|
16
|
-
@uri =
|
17
|
-
@
|
18
|
-
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@vhost = URI.decode_www_form_component(@uri.path[1..-1] || "/")
|
23
|
-
@options = URI.decode_www_form(@uri.query || "").to_h
|
10
|
+
def initialize(uri, **options)
|
11
|
+
@uri = uri
|
12
|
+
@options = options
|
13
|
+
|
14
|
+
@queues = {}
|
15
|
+
@subscriptions = Set.new
|
16
|
+
@connq = SizedQueue.new(1)
|
24
17
|
end
|
25
18
|
|
26
|
-
def connect
|
27
|
-
|
28
|
-
enable_tcp_keepalive(socket)
|
29
|
-
if @tls
|
30
|
-
context = OpenSSL::SSL::SSLContext.new
|
31
|
-
context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless @options["verify_peer"] == "none"
|
32
|
-
socket = OpenSSL::SSL::SSLSocket.new(socket, context)
|
33
|
-
socket.sync_close = true # closing the TLS socket also closes the TCP socket
|
34
|
-
end
|
35
|
-
channel_max, frame_max, heartbeat = establish(socket)
|
36
|
-
Connection.new(socket, channel_max, frame_max, heartbeat)
|
19
|
+
def connect(read_loop_thread: true)
|
20
|
+
Connection.connect(@uri, **@options.merge(read_loop_thread: read_loop_thread))
|
37
21
|
end
|
38
22
|
|
39
|
-
|
23
|
+
def start
|
24
|
+
@stopped = false
|
25
|
+
Thread.new do
|
26
|
+
loop do
|
27
|
+
break if @stopped
|
40
28
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
29
|
+
conn = connect(read_loop_thread: false)
|
30
|
+
Thread.new do
|
31
|
+
# restore connection in another thread, read_loop have to run
|
32
|
+
conn.channel(1) # reserve channel 1 for publishes
|
33
|
+
@subscriptions.each { |args| subscribe(*args) }
|
34
|
+
@connq << conn
|
35
|
+
end
|
36
|
+
conn.read_loop # blocks until connection is closed, then reconnect
|
37
|
+
rescue => e
|
38
|
+
warn "AMQP-Client reconnect error: #{e.inspect}"
|
39
|
+
sleep @options[:reconnect_interval] || 1
|
50
40
|
end
|
41
|
+
end
|
42
|
+
self
|
43
|
+
end
|
51
44
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
when 30 # connection#tune
|
67
|
-
channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
|
68
|
-
channel_max = [channel_max, 2048].min
|
69
|
-
frame_max = [frame_max, 4096].min
|
70
|
-
heartbeat = [heartbeat, 0].min
|
71
|
-
socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
|
72
|
-
socket.write FrameBytes.connection_open(@vhost)
|
73
|
-
when 41 # connection#open-ok
|
74
|
-
return [channel_max, frame_max, heartbeat]
|
75
|
-
when 50 # connection#close
|
76
|
-
code, text_len = buf.unpack("@11 S> C")
|
77
|
-
text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
|
78
|
-
socket.write FrameBytes.connection_close_ok
|
79
|
-
raise Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
|
80
|
-
else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
|
81
|
-
end
|
82
|
-
else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
|
45
|
+
def stop
|
46
|
+
@stopped = true
|
47
|
+
conn = @connq.pop
|
48
|
+
conn.close
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def queue(name, arguments: {})
|
53
|
+
raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
|
54
|
+
|
55
|
+
@queues.fetch(name) do
|
56
|
+
with_connection do |conn|
|
57
|
+
conn.with_channel do |ch| # use a temp channel in case the declaration fails
|
58
|
+
ch.queue_declare(name, arguments: arguments)
|
83
59
|
end
|
84
|
-
else raise Error, "Unexpected frame type: #{type}"
|
85
60
|
end
|
61
|
+
@queues[name] = Queue.new(self, name)
|
86
62
|
end
|
87
|
-
rescue StandardError
|
88
|
-
socket.close
|
89
|
-
raise
|
90
63
|
end
|
91
64
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
65
|
+
def subscribe(queue_name, no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
|
66
|
+
@subscriptions.add? [queue_name, no_ack, prefetch, arguments, blk]
|
67
|
+
|
68
|
+
with_connection do |conn|
|
69
|
+
ch = conn.channel
|
70
|
+
ch.basic_qos(prefetch)
|
71
|
+
ch.basic_consume(queue_name, no_ack: no_ack, worker_threads: worker_threads, arguments: arguments) do |msg|
|
72
|
+
blk.call(msg)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def publish(body, exchange, routing_key, **properties)
|
78
|
+
with_connection do |conn|
|
79
|
+
# Use channel 1 for publishes
|
80
|
+
conn.channel(1).basic_publish_confirm(body, exchange, routing_key, **properties)
|
81
|
+
rescue
|
82
|
+
conn.channel(1) # reopen channel 1 if it raised
|
83
|
+
raise
|
84
|
+
end
|
97
85
|
rescue => e
|
98
|
-
warn "
|
86
|
+
warn "AMQP-Client error publishing, retrying (#{e.inspect})"
|
87
|
+
retry
|
88
|
+
end
|
89
|
+
|
90
|
+
def bind(queue, exchange, routing_key, **headers)
|
91
|
+
with_connection do |conn|
|
92
|
+
conn.channel(1).queue_bind(queue, exchange, routing_key, **headers)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def unbind(queue, exchange, routing_key, **headers)
|
97
|
+
with_connection do |conn|
|
98
|
+
conn.channel(1).queue_unbind(queue, exchange, routing_key, **headers)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def purge(queue)
|
103
|
+
with_connection do |conn|
|
104
|
+
conn.channel(1).queue_purge(queue)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def delete_queue(queue)
|
109
|
+
with_connection do |conn|
|
110
|
+
conn.channel(1).queue_delete(queue)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Queue abstraction
|
115
|
+
class Queue
|
116
|
+
def initialize(client, name)
|
117
|
+
@client = client
|
118
|
+
@name = name
|
119
|
+
end
|
120
|
+
|
121
|
+
def publish(body, **properties)
|
122
|
+
@client.publish(body, "", @name, **properties)
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
def subscribe(prefetch: 1, arguments: {}, &blk)
|
127
|
+
@client.subscribe(@name, prefetch: prefetch, arguments: arguments, &blk)
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def bind(exchange, routing_key, **headers)
|
132
|
+
@client.bind(@name, exchange, routing_key, **headers)
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
def unbind(exchange, routing_key, **headers)
|
137
|
+
@client.unbind(@name, exchange, routing_key, **headers)
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def purge
|
142
|
+
@client.purge(@name)
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
def delete
|
147
|
+
@client.delete_queue(@name)
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def with_connection
|
155
|
+
conn = nil
|
156
|
+
loop do
|
157
|
+
conn = @connq.pop
|
158
|
+
next if conn.closed?
|
159
|
+
|
160
|
+
break
|
161
|
+
end
|
162
|
+
begin
|
163
|
+
yield conn
|
164
|
+
ensure
|
165
|
+
@connq << conn unless conn.closed?
|
166
|
+
end
|
99
167
|
end
|
100
168
|
end
|
101
169
|
end
|