amqp-client 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +1 -1
- data/.github/workflows/docs.yml +1 -1
- data/.github/workflows/main.yml +34 -9
- data/.github/workflows/release.yml +1 -1
- data/.rubocop.yml +5 -3
- data/CHANGELOG.md +12 -1
- data/CODEOWNERS +1 -0
- data/README.md +63 -4
- data/Rakefile +155 -4
- data/amqp-client.gemspec +3 -3
- data/lib/amqp/client/channel.rb +4 -4
- data/lib/amqp/client/connection.rb +81 -16
- data/lib/amqp/client/frame_bytes.rb +24 -1
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +1 -2
- metadata +7 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0abeedd581cdc09eac6afaf690fb8a50fbaef0146e4c22be43d7cad90d8d9b6
|
4
|
+
data.tar.gz: 16161d756ce7d68ba24debc60eda941243bb951cd17262f1c148ef1039595e88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 272c099f3158572304b30bbf67227343ce5456b90e5b4a08ccaf9c2da0f9b3a42ffb02913431b7d98cc0b7f1d5d36ef2c61bd492cdabf4411b0d2f941c616f04
|
7
|
+
data.tar.gz: a2000478882d0e630d88fb67e09675b087981498ac9a5b7f7a80f69a092c082415530156136cca1fa2e1bf88874468437fd3dbc600ee354110fb0c6af59e1373
|
data/.github/workflows/docs.yml
CHANGED
data/.github/workflows/main.yml
CHANGED
@@ -19,21 +19,22 @@ jobs:
|
|
19
19
|
matrix:
|
20
20
|
sudo: [true]
|
21
21
|
ruby:
|
22
|
-
- "2.6"
|
23
|
-
- "2.7"
|
24
|
-
- "3.0"
|
25
|
-
- "3.1"
|
26
22
|
- "3.2"
|
27
23
|
- "3.3"
|
24
|
+
- "3.4"
|
28
25
|
include:
|
29
|
-
- { ruby: jruby, allow-failure:
|
30
|
-
- { ruby: truffleruby, allow-failure:
|
26
|
+
- { ruby: jruby, allow-failure: false, sudo: false }
|
27
|
+
- { ruby: truffleruby, allow-failure: false, sudo: false }
|
31
28
|
steps:
|
29
|
+
- name: Configure dpkg to skip building man pages
|
30
|
+
run: |
|
31
|
+
echo 'path-exclude /usr/share/doc/*' | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc
|
32
|
+
echo 'path-exclude /usr/share/man/*' | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc
|
32
33
|
- name: Install RabbitMQ
|
33
34
|
run: sudo apt-get update && sudo apt-get install -y rabbitmq-server
|
34
35
|
- name: Verify RabbitMQ started correctly
|
35
36
|
run: while true; do sudo rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
|
36
|
-
- uses: actions/checkout@
|
37
|
+
- uses: actions/checkout@v5
|
37
38
|
- uses: ruby/setup-ruby@v1
|
38
39
|
with:
|
39
40
|
bundler-cache: true
|
@@ -55,6 +56,10 @@ jobs:
|
|
55
56
|
- "jruby"
|
56
57
|
- "truffleruby"
|
57
58
|
steps:
|
59
|
+
- name: Configure dpkg to skip building man pages
|
60
|
+
run: |
|
61
|
+
echo 'path-exclude /usr/share/doc/*' | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc
|
62
|
+
echo 'path-exclude /usr/share/man/*' | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc
|
58
63
|
- name: Install RabbitMQ
|
59
64
|
run: sudo apt-get update && sudo apt-get install -y rabbitmq-server
|
60
65
|
- name: Stop RabbitMQ
|
@@ -81,12 +86,20 @@ jobs:
|
|
81
86
|
run: sudo systemctl start rabbitmq-server
|
82
87
|
- name: Verify RabbitMQ started correctly
|
83
88
|
run: while true; do sudo rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
|
84
|
-
- uses: actions/checkout@
|
89
|
+
- uses: actions/checkout@v5
|
85
90
|
- uses: ruby/setup-ruby@v1
|
86
91
|
with:
|
87
92
|
bundler-cache: true
|
88
93
|
ruby-version: ${{ matrix.ruby }}
|
94
|
+
- name: Run TLS tests (JRuby)
|
95
|
+
if: ${{ matrix.ruby == 'jruby' }}
|
96
|
+
run: bundle exec rake
|
97
|
+
env:
|
98
|
+
JAVA_OPTS: "-Djava.net.preferIPv4Stack=true"
|
99
|
+
TEST_AMQP_HOST: "localhost"
|
100
|
+
TESTOPTS: --name=/_tls$/
|
89
101
|
- name: Run TLS tests
|
102
|
+
if: ${{ matrix.ruby != 'jruby' }}
|
90
103
|
run: bundle exec rake
|
91
104
|
env:
|
92
105
|
TESTOPTS: --name=/_tls$/
|
@@ -104,7 +117,7 @@ jobs:
|
|
104
117
|
run: brew install rabbitmq
|
105
118
|
- name: Start RabbitMQ
|
106
119
|
run: brew services start rabbitmq
|
107
|
-
- uses: actions/checkout@
|
120
|
+
- uses: actions/checkout@v5
|
108
121
|
- uses: ruby/setup-ruby@v1
|
109
122
|
with:
|
110
123
|
bundler-cache: true
|
@@ -113,3 +126,15 @@ jobs:
|
|
113
126
|
run: while true; do rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
|
114
127
|
- name: Run tests (excluding TLS tests)
|
115
128
|
run: bundle exec rake
|
129
|
+
|
130
|
+
lint:
|
131
|
+
runs-on: ubuntu-latest
|
132
|
+
timeout-minutes: 10
|
133
|
+
steps:
|
134
|
+
- uses: actions/checkout@v5
|
135
|
+
- uses: ruby/setup-ruby@v1
|
136
|
+
with:
|
137
|
+
bundler-cache: true
|
138
|
+
ruby-version: ruby
|
139
|
+
- name: Run RuboCop
|
140
|
+
run: bundle exec rake rubocop
|
data/.rubocop.yml
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
2
|
+
plugins:
|
3
|
+
- rubocop-minitest
|
2
4
|
|
3
5
|
AllCops:
|
4
6
|
NewCops: disable
|
5
|
-
TargetRubyVersion: 2
|
7
|
+
TargetRubyVersion: 3.2
|
6
8
|
SuggestExtensions: false
|
7
9
|
|
8
10
|
Style/StringLiterals:
|
@@ -18,11 +20,11 @@ Layout/LineLength:
|
|
18
20
|
|
19
21
|
Naming/FileName:
|
20
22
|
Exclude:
|
21
|
-
-
|
23
|
+
- "lib/amqp-client.rb"
|
22
24
|
|
23
25
|
Metrics/PerceivedComplexity:
|
24
26
|
Exclude:
|
25
|
-
-
|
27
|
+
- "lib/amqp/client/properties.rb"
|
26
28
|
|
27
29
|
Metrics/ParameterLists:
|
28
30
|
Max: 8
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.2.0] - 2025-09-10
|
4
|
+
|
5
|
+
- Fixed: `Connection#channel` wasn't thread-safe
|
6
|
+
- Added: Support for heartbeats
|
7
|
+
|
8
|
+
## [1.1.7] - 2024-05-12
|
9
|
+
|
10
|
+
- Support for Connection.update-secret
|
11
|
+
- Allow sub-second connect_timeout
|
12
|
+
- Fixed: undefinied variable if message was returned and no on_return block was set
|
13
|
+
|
3
14
|
## [1.1.6] - 2024-03-26
|
4
15
|
|
5
16
|
- Fixed: Channel#wait_for_confirms now waits for all confirms, in a thread safe way
|
@@ -42,7 +53,7 @@
|
|
42
53
|
|
43
54
|
## [1.0.1] - 2021-09-06
|
44
55
|
|
45
|
-
- The API is fully documented! https://cloudamqp.github.io/amqp-client.rb
|
56
|
+
- The API is fully documented! <https://cloudamqp.github.io/amqp-client.rb/>
|
46
57
|
- Fixed: Socket writing is now thread-safe
|
47
58
|
- Change: Block while waiting for basic_cancel by default
|
48
59
|
- Added: Can specify channel_max, heartbeat and frame_max as options to the Client/Connection
|
data/CODEOWNERS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* @carlhoerberg @spuun @dentarg @baelter @walro
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@ It's safe by default, messages are published as persistent, and is waiting for c
|
|
8
8
|
|
9
9
|
## Support
|
10
10
|
|
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.
|
11
|
+
The library is fully supported by [CloudAMQP](https://www.cloudamqp.com), the largest LavinMQ and 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.
|
12
12
|
|
13
13
|
## Documentation
|
14
14
|
|
@@ -113,17 +113,76 @@ gem 'amqp-client'
|
|
113
113
|
|
114
114
|
And then execute:
|
115
115
|
|
116
|
-
|
116
|
+
bundle install
|
117
117
|
|
118
118
|
Or install it yourself as:
|
119
119
|
|
120
|
-
|
120
|
+
gem install amqp-client
|
121
121
|
|
122
122
|
## Development
|
123
123
|
|
124
124
|
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.
|
125
125
|
|
126
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
126
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
127
|
+
|
128
|
+
### Release Process
|
129
|
+
|
130
|
+
The gem uses rake tasks to automate the release process. Make sure your working directory is clean before starting a release.
|
131
|
+
|
132
|
+
#### Quick Release (Patch Version)
|
133
|
+
|
134
|
+
```bash
|
135
|
+
rake release:full
|
136
|
+
```
|
137
|
+
|
138
|
+
This will:
|
139
|
+
|
140
|
+
1. Run tests and RuboCop to ensure code quality
|
141
|
+
2. Bump the patch version (e.g., 1.2.0 → 1.2.1)
|
142
|
+
3. Update the CHANGELOG.md with the new version and current date
|
143
|
+
4. Create a git commit and tag for the release
|
144
|
+
5. Build and push the gem to RubyGems
|
145
|
+
6. Push commits and tags to the remote repository
|
146
|
+
|
147
|
+
#### Custom Version Bump
|
148
|
+
|
149
|
+
For minor or major version bumps:
|
150
|
+
|
151
|
+
```bash
|
152
|
+
# Minor version bump (e.g., 1.2.0 → 1.3.0)
|
153
|
+
rake release:full[minor]
|
154
|
+
|
155
|
+
# Major version bump (e.g., 1.2.0 → 2.0.0)
|
156
|
+
rake release:full[major]
|
157
|
+
```
|
158
|
+
|
159
|
+
#### Individual Release Steps
|
160
|
+
|
161
|
+
You can also run individual steps if needed:
|
162
|
+
|
163
|
+
```bash
|
164
|
+
# Bump version only
|
165
|
+
rake release:bump[patch] # or [minor] or [major]
|
166
|
+
|
167
|
+
# Update changelog with current version
|
168
|
+
rake release:changelog
|
169
|
+
|
170
|
+
# Create git tag
|
171
|
+
rake release:tag
|
172
|
+
|
173
|
+
# Build and push to RubyGems
|
174
|
+
rake release:push
|
175
|
+
```
|
176
|
+
|
177
|
+
#### Manual Release Steps
|
178
|
+
|
179
|
+
If you prefer manual control:
|
180
|
+
|
181
|
+
1. Update the version number in `lib/amqp/client/version.rb`
|
182
|
+
2. Update the CHANGELOG.md with the new version and release notes
|
183
|
+
3. Commit your changes: `git add . && git commit -m "Release X.Y.Z"`
|
184
|
+
4. Create and push a tag: `git tag vX.Y.Z && git push origin vX.Y.Z`
|
185
|
+
5. Build and push: `rake build && gem push amqp-client-X.Y.Z.gem`
|
127
186
|
|
128
187
|
## Contributing
|
129
188
|
|
data/Rakefile
CHANGED
@@ -18,12 +18,163 @@ end
|
|
18
18
|
|
19
19
|
require "rubocop/rake_task"
|
20
20
|
|
21
|
-
RuboCop::RakeTask.new
|
22
|
-
task.requires << "rubocop-minitest"
|
23
|
-
end
|
21
|
+
RuboCop::RakeTask.new
|
24
22
|
|
25
23
|
require "yard"
|
26
24
|
|
27
25
|
YARD::Rake::YardocTask.new
|
28
26
|
|
29
|
-
|
27
|
+
# Release helper methods
|
28
|
+
def current_version
|
29
|
+
version_file = "lib/amqp/client/version.rb"
|
30
|
+
content = File.read(version_file)
|
31
|
+
content.match(/VERSION = "(.+)"/)[1]
|
32
|
+
end
|
33
|
+
|
34
|
+
def bump_version(version_type)
|
35
|
+
unless %w[major minor patch].include?(version_type)
|
36
|
+
puts "Invalid version type. Use: major, minor, or patch"
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
version_file = "lib/amqp/client/version.rb"
|
41
|
+
content = File.read(version_file)
|
42
|
+
|
43
|
+
current_version = content.match(/VERSION = "(.+)"/)[1]
|
44
|
+
major, minor, patch = current_version.split(".").map(&:to_i)
|
45
|
+
|
46
|
+
case version_type
|
47
|
+
when "major"
|
48
|
+
major += 1
|
49
|
+
minor = 0
|
50
|
+
patch = 0
|
51
|
+
when "minor"
|
52
|
+
minor += 1
|
53
|
+
patch = 0
|
54
|
+
when "patch"
|
55
|
+
patch += 1
|
56
|
+
end
|
57
|
+
|
58
|
+
new_version = "#{major}.#{minor}.#{patch}"
|
59
|
+
new_content = content.gsub(/VERSION = ".+"/, %(VERSION = "#{new_version}"))
|
60
|
+
|
61
|
+
File.write(version_file, new_content)
|
62
|
+
puts "Bumped version from #{current_version} to #{new_version}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def update_changelog
|
66
|
+
version = current_version
|
67
|
+
date = Time.now.strftime("%Y-%m-%d")
|
68
|
+
|
69
|
+
changelog = File.read("CHANGELOG.md")
|
70
|
+
|
71
|
+
if changelog.include?("## [#{version}]")
|
72
|
+
puts "Version #{version} already exists in CHANGELOG.md"
|
73
|
+
else
|
74
|
+
updated_changelog = changelog.sub(
|
75
|
+
"## [Unreleased]",
|
76
|
+
"## [Unreleased]\n\n## [#{version}] - #{date}"
|
77
|
+
)
|
78
|
+
|
79
|
+
File.write("CHANGELOG.md", updated_changelog)
|
80
|
+
puts "Updated CHANGELOG.md with version #{version}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_git_tag
|
85
|
+
version = current_version
|
86
|
+
|
87
|
+
system("git add .")
|
88
|
+
system("git commit -m 'Release #{version}'")
|
89
|
+
|
90
|
+
# Check if tag already exists and remove it if it does
|
91
|
+
if system("git tag -l v#{version} | grep -q v#{version}")
|
92
|
+
puts "Tag v#{version} already exists, removing it..."
|
93
|
+
system("git tag -d v#{version}")
|
94
|
+
end
|
95
|
+
|
96
|
+
system("git tag v#{version}")
|
97
|
+
|
98
|
+
puts "Created git tag v#{version}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def push_gem_to_rubygems
|
102
|
+
version = current_version
|
103
|
+
|
104
|
+
# Look for gem file in both current directory and pkg directory
|
105
|
+
gem_file = "amqp-client-#{version}.gem"
|
106
|
+
pkg_gem_file = "pkg/amqp-client-#{version}.gem"
|
107
|
+
|
108
|
+
if File.exist?(pkg_gem_file)
|
109
|
+
system("gem push #{pkg_gem_file}")
|
110
|
+
puts "Pushed #{pkg_gem_file} to RubyGems"
|
111
|
+
elsif File.exist?(gem_file)
|
112
|
+
system("gem push #{gem_file}")
|
113
|
+
puts "Pushed #{gem_file} to RubyGems"
|
114
|
+
else
|
115
|
+
puts "Gem file #{gem_file} not found in current directory or pkg/. Make sure to build first."
|
116
|
+
exit 1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def full_release_process(version_type)
|
121
|
+
puts "Starting release process..."
|
122
|
+
|
123
|
+
# Ensure working directory is clean
|
124
|
+
unless system("git diff --quiet && git diff --cached --quiet")
|
125
|
+
puts "Working directory is not clean. Please commit or stash changes first."
|
126
|
+
exit 1
|
127
|
+
end
|
128
|
+
|
129
|
+
# Bump version
|
130
|
+
Rake::Task["release:bump"].invoke(version_type)
|
131
|
+
Rake::Task["release:bump"].reenable
|
132
|
+
|
133
|
+
# Update changelog
|
134
|
+
Rake::Task["release:changelog"].invoke
|
135
|
+
Rake::Task["release:changelog"].reenable
|
136
|
+
|
137
|
+
# Create tag and push
|
138
|
+
Rake::Task["release:tag"].invoke
|
139
|
+
Rake::Task["release:tag"].reenable
|
140
|
+
|
141
|
+
# Build and push gem
|
142
|
+
Rake::Task["release:push"].invoke
|
143
|
+
Rake::Task["release:push"].reenable
|
144
|
+
|
145
|
+
# Push to git
|
146
|
+
system("git push origin")
|
147
|
+
system("git push origin --tags")
|
148
|
+
|
149
|
+
version = current_version
|
150
|
+
puts "Successfully released version #{version}!"
|
151
|
+
end
|
152
|
+
|
153
|
+
namespace :release do
|
154
|
+
desc "Bump version (usage: rake release:bump[major|minor|patch])"
|
155
|
+
task :bump, [:type] do |_t, args|
|
156
|
+
bump_version(args[:type] || "patch")
|
157
|
+
end
|
158
|
+
|
159
|
+
desc "Update changelog with current version"
|
160
|
+
task :changelog do
|
161
|
+
update_changelog
|
162
|
+
end
|
163
|
+
|
164
|
+
desc "Create git tag for current version"
|
165
|
+
task :tag do
|
166
|
+
create_git_tag
|
167
|
+
end
|
168
|
+
|
169
|
+
desc "Build and push gem to RubyGems"
|
170
|
+
task push: :build do
|
171
|
+
push_gem_to_rubygems
|
172
|
+
end
|
173
|
+
|
174
|
+
desc "Full release process (bump version, update changelog, tag, build and push)"
|
175
|
+
task :full, [:type] => %i[test rubocop] do |_t, args|
|
176
|
+
full_release_process(args[:type] || "patch")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
task default: [:test, *(:rubocop if ENV["CI"] != "true")]
|
data/amqp-client.gemspec
CHANGED
@@ -5,14 +5,14 @@ require_relative "lib/amqp/client/version"
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "amqp-client"
|
7
7
|
spec.version = AMQP::Client::VERSION
|
8
|
-
spec.authors = ["
|
9
|
-
spec.email = ["
|
8
|
+
spec.authors = ["CloudAMQP"]
|
9
|
+
spec.email = ["team@cloudamqp.com"]
|
10
10
|
|
11
11
|
spec.summary = "AMQP 0-9-1 client"
|
12
12
|
spec.description = "Modern AMQP 0-9-1 Ruby client"
|
13
13
|
spec.homepage = "https://github.com/cloudamqp/amqp-client.rb"
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
16
16
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
18
18
|
spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
|
data/lib/amqp/client/channel.rb
CHANGED
@@ -251,7 +251,7 @@ module AMQP
|
|
251
251
|
# @option properties [Hash<String, Object>] headers Custom headers
|
252
252
|
# @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
|
253
253
|
# @option properties [Integer] priority A priority of the message (between 0 and 255)
|
254
|
-
# @option properties [
|
254
|
+
# @option properties [String] correlation_id A correlation id, most often used used for RPC communication
|
255
255
|
# @option properties [String] reply_to Queue to reply RPC responses to
|
256
256
|
# @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
|
257
257
|
# @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
|
@@ -345,8 +345,8 @@ module AMQP
|
|
345
345
|
end
|
346
346
|
|
347
347
|
# Specify how many messages to prefetch for consumers with `no_ack: false`
|
348
|
-
# @param prefetch_count [Integer] Number of messages to
|
349
|
-
# @param prefetch_size [Integer] Number of bytes to
|
348
|
+
# @param prefetch_count [Integer] Number of messages to maximum keep in flight
|
349
|
+
# @param prefetch_size [Integer] Number of bytes to maximum keep in flight
|
350
350
|
# @param global [Boolean] If true the limit will apply to channel rather than the consumer
|
351
351
|
# @return [nil]
|
352
352
|
def basic_qos(prefetch_count, prefetch_size: 0, global: false)
|
@@ -522,7 +522,7 @@ module AMQP
|
|
522
522
|
if @on_return
|
523
523
|
Thread.new { @on_return.call(next_msg) }
|
524
524
|
else
|
525
|
-
warn "AMQP-Client message returned: #{
|
525
|
+
warn "AMQP-Client message returned: #{next_msg.inspect}"
|
526
526
|
end
|
527
527
|
elsif next_msg.consumer_tag.nil?
|
528
528
|
@basic_gets.push next_msg
|
@@ -18,7 +18,7 @@ module AMQP
|
|
18
18
|
# @option options [Boolean] connection_name (PROGRAM_NAME) Set a name for the connection to be able to identify
|
19
19
|
# the client from the broker
|
20
20
|
# @option options [Boolean] verify_peer (true) Verify broker's TLS certificate, set to false for self-signed certs
|
21
|
-
# @option options [
|
21
|
+
# @option options [Float] connect_timeout (30) TCP connection timeout
|
22
22
|
# @option options [Integer] heartbeat (0) Heartbeat timeout, defaults to 0 and relies on TCP keepalive instead
|
23
23
|
# @option options [Integer] frame_max (131_072) Maximum frame size,
|
24
24
|
# the smallest of the client's and the broker's values will be used
|
@@ -44,6 +44,7 @@ module AMQP
|
|
44
44
|
@frame_max = frame_max
|
45
45
|
@heartbeat = heartbeat
|
46
46
|
@channels = {}
|
47
|
+
@channels_lock = Mutex.new
|
47
48
|
@closed = nil
|
48
49
|
@replies = ::Queue.new
|
49
50
|
@write_lock = Mutex.new
|
@@ -51,6 +52,9 @@ module AMQP
|
|
51
52
|
@on_blocked = ->(reason) { warn "AMQP-Client blocked by broker: #{reason}" }
|
52
53
|
@on_unblocked = -> { warn "AMQP-Client unblocked by broker" }
|
53
54
|
|
55
|
+
# Only used with heartbeats
|
56
|
+
@last_activity_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
57
|
+
|
54
58
|
Thread.new { read_loop } if read_loop_thread
|
55
59
|
end
|
56
60
|
|
@@ -89,15 +93,17 @@ module AMQP
|
|
89
93
|
raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
|
90
94
|
raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
|
91
95
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
96
|
+
ch = @channels_lock.synchronize do
|
97
|
+
if id
|
98
|
+
@channels[id] ||= Channel.new(self, id)
|
99
|
+
else
|
100
|
+
1.upto(@channel_max) do |i|
|
101
|
+
break id = i unless @channels.key? i
|
102
|
+
end
|
103
|
+
raise Error, "Max channels reached" if id.nil?
|
99
104
|
|
100
|
-
|
105
|
+
@channels[id] = Channel.new(self, id)
|
106
|
+
end
|
101
107
|
end
|
102
108
|
ch.open
|
103
109
|
end
|
@@ -132,6 +138,16 @@ module AMQP
|
|
132
138
|
nil
|
133
139
|
end
|
134
140
|
|
141
|
+
# Update authentication secret, for example when an OAuth backend is used
|
142
|
+
# @param secret [String] The new secret
|
143
|
+
# @param reason [String] A reason to update it
|
144
|
+
# @return [nil]
|
145
|
+
def update_secret(secret, reason)
|
146
|
+
write_bytes FrameBytes.update_secret(secret, reason)
|
147
|
+
expect(:update_secret_ok)
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
135
151
|
# True if the connection is closed
|
136
152
|
# @return [Boolean]
|
137
153
|
def closed?
|
@@ -165,6 +181,7 @@ module AMQP
|
|
165
181
|
def write_bytes(*bytes)
|
166
182
|
@write_lock.synchronize do
|
167
183
|
@socket.write(*bytes)
|
184
|
+
update_last_activity
|
168
185
|
end
|
169
186
|
rescue *READ_EXCEPTIONS => e
|
170
187
|
raise Error::ConnectionClosed.new(*@closed) if @closed
|
@@ -196,6 +213,7 @@ module AMQP
|
|
196
213
|
|
197
214
|
# parse the frame, will return false if a close frame was received
|
198
215
|
parse_frame(type, channel_id, frame_buffer) || return
|
216
|
+
update_last_activity
|
199
217
|
end
|
200
218
|
nil
|
201
219
|
rescue *READ_EXCEPTIONS => e
|
@@ -222,7 +240,7 @@ module AMQP
|
|
222
240
|
READ_EXCEPTIONS = [IOError, OpenSSL::OpenSSLError, SystemCallError,
|
223
241
|
RUBY_ENGINE == "jruby" ? java.lang.NullPointerException : nil].compact.freeze
|
224
242
|
|
225
|
-
def parse_frame(type, channel_id, buf) # rubocop:disable Metrics/MethodLength
|
243
|
+
def parse_frame(type, channel_id, buf) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
226
244
|
channel = @channels[channel_id]
|
227
245
|
case type
|
228
246
|
when 1 # method frame
|
@@ -255,6 +273,8 @@ module AMQP
|
|
255
273
|
when 61 # connection#unblocked
|
256
274
|
@blocked = nil
|
257
275
|
@on_unblocked.call
|
276
|
+
when 71 # connection#update_secret_ok
|
277
|
+
@replies.push [:update_secret_ok]
|
258
278
|
else raise Error::UnsupportedMethodFrame, class_id, method_id
|
259
279
|
end
|
260
280
|
when 20 # channel
|
@@ -265,11 +285,11 @@ module AMQP
|
|
265
285
|
reply_code, reply_text_len = buf.unpack("@4 S> C")
|
266
286
|
reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
|
267
287
|
classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
|
268
|
-
channel = @channels.delete(channel_id)
|
288
|
+
channel = @channels_lock.synchronize { @channels.delete(channel_id) }
|
269
289
|
channel.closed!(:channel, reply_code, reply_text, classid, methodid)
|
270
290
|
write_bytes FrameBytes.channel_close_ok(channel_id)
|
271
291
|
when 41 # channel#close-ok
|
272
|
-
channel = @channels.delete(channel_id)
|
292
|
+
channel = @channels_lock.synchronize { @channels.delete(channel_id) }
|
273
293
|
channel.reply [:channel_close_ok]
|
274
294
|
else raise Error::UnsupportedMethodFrame, class_id, method_id
|
275
295
|
end
|
@@ -394,17 +414,61 @@ module AMQP
|
|
394
414
|
channel.header_delivered body_size, properties
|
395
415
|
when 3 # body
|
396
416
|
channel.body_delivered buf
|
417
|
+
when 8 # heartbeat
|
418
|
+
handle_server_heartbeat(channel_id)
|
397
419
|
else raise Error::UnsupportedFrameType, type
|
398
420
|
end
|
399
421
|
true
|
400
422
|
end
|
401
423
|
|
424
|
+
def update_last_activity
|
425
|
+
return unless @heartbeat&.positive?
|
426
|
+
|
427
|
+
@last_activity_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
428
|
+
end
|
429
|
+
|
430
|
+
def handle_server_heartbeat(channel_id)
|
431
|
+
return if channel_id.zero?
|
432
|
+
|
433
|
+
raise Error::ConnectionClosed.new(501, "Heartbeat frame received on non-zero channel #{channel_id}")
|
434
|
+
end
|
435
|
+
|
436
|
+
# Start the heartbeat background thread (called from connection#tune)
|
437
|
+
def start_heartbeats(period)
|
438
|
+
Thread.new do
|
439
|
+
Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
|
440
|
+
interval = period / 2.0
|
441
|
+
loop do
|
442
|
+
sleep interval
|
443
|
+
break if @closed
|
444
|
+
next if @socket.nil?
|
445
|
+
|
446
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
447
|
+
# If we haven't sent anything recently, send a heartbeat
|
448
|
+
next unless now - @last_activity_time >= interval
|
449
|
+
|
450
|
+
begin
|
451
|
+
send_heartbeat
|
452
|
+
rescue Error => e
|
453
|
+
warn "AMQP-Client heartbeat send failed: #{e.inspect}"
|
454
|
+
break
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
def send_heartbeat
|
461
|
+
write_bytes FrameBytes.heartbeat
|
462
|
+
end
|
463
|
+
|
402
464
|
def expect(expected_frame_type)
|
403
465
|
frame_type, args = @replies.pop
|
404
466
|
if frame_type.nil?
|
405
467
|
return if expected_frame_type == :close_ok
|
406
468
|
|
407
|
-
raise
|
469
|
+
raise Error::ConnectionClosed.new(*@closed) if @closed
|
470
|
+
|
471
|
+
raise Error, "Connection closed while waiting for #{expected_frame_type}"
|
408
472
|
end
|
409
473
|
frame_type == expected_frame_type || raise(Error::UnexpectedFrame.new(expected_frame_type, frame_type))
|
410
474
|
args
|
@@ -414,7 +478,7 @@ module AMQP
|
|
414
478
|
# @return [Socket]
|
415
479
|
# @return [OpenSSL::SSL::SSLSocket]
|
416
480
|
def open_socket(host, port, tls, options)
|
417
|
-
connect_timeout = options.fetch(:connect_timeout, 30).
|
481
|
+
connect_timeout = options.fetch(:connect_timeout, 30).to_f
|
418
482
|
socket = Socket.tcp host, port, connect_timeout: connect_timeout
|
419
483
|
keepalive = options.fetch(:keepalive, "").split(":", 3).map!(&:to_i)
|
420
484
|
enable_tcp_keepalive(socket, *keepalive)
|
@@ -442,7 +506,7 @@ module AMQP
|
|
442
506
|
channel_max, frame_max, heartbeat = nil
|
443
507
|
socket.write "AMQP\x00\x00\x09\x01"
|
444
508
|
buf = String.new(capacity: 4096)
|
445
|
-
loop do
|
509
|
+
loop do # rubocop:disable Metrics/BlockLength
|
446
510
|
begin
|
447
511
|
socket.readpartial(4096, buf)
|
448
512
|
rescue *READ_EXCEPTIONS => e
|
@@ -471,6 +535,7 @@ module AMQP
|
|
471
535
|
channel_max = [channel_max, options.fetch(:channel_max, 2048).to_i].min
|
472
536
|
frame_max = [frame_max, options.fetch(:frame_max, 131_072).to_i].min
|
473
537
|
heartbeat = [heartbeat, options.fetch(:heartbeat, 0).to_i].min
|
538
|
+
start_heartbeats(heartbeat) if heartbeat.positive?
|
474
539
|
socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
|
475
540
|
socket.write FrameBytes.connection_open(vhost)
|
476
541
|
when 41 # connection#open-ok
|
@@ -496,7 +561,7 @@ module AMQP
|
|
496
561
|
raise e
|
497
562
|
end
|
498
563
|
|
499
|
-
# Enable TCP keepalive, which is
|
564
|
+
# Enable TCP keepalive, which is preferred to heartbeats
|
500
565
|
# @return [void]
|
501
566
|
def enable_tcp_keepalive(socket, idle = 60, interval = 10, count = 3)
|
502
567
|
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
@@ -9,7 +9,7 @@ module AMQP
|
|
9
9
|
# Each frame type implemented as a method
|
10
10
|
# Having a class for each frame type is more expensive in terms of CPU and memory
|
11
11
|
# @api private
|
12
|
-
module FrameBytes
|
12
|
+
module FrameBytes # rubocop:disable Metrics/ModuleLength
|
13
13
|
def self.connection_start_ok(response, properties)
|
14
14
|
prop_tbl = Table.encode(properties)
|
15
15
|
[
|
@@ -81,6 +81,20 @@ module AMQP
|
|
81
81
|
].pack("C S> L> S> S> C")
|
82
82
|
end
|
83
83
|
|
84
|
+
def self.update_secret(secret, reason)
|
85
|
+
frame_size = 4 + 4 + secret.bytesize + 1 + reason.bytesize
|
86
|
+
[
|
87
|
+
1, # type: method
|
88
|
+
0, # channel id
|
89
|
+
frame_size, # frame size
|
90
|
+
10, # class: connection
|
91
|
+
70, # method: close-ok
|
92
|
+
secret.bytesize, secret,
|
93
|
+
reason.bytesize, reason,
|
94
|
+
206 # frame end
|
95
|
+
].pack("C S> L> S> S> L>a* Ca* C")
|
96
|
+
end
|
97
|
+
|
84
98
|
def self.channel_open(id)
|
85
99
|
[
|
86
100
|
1, # type: method
|
@@ -528,6 +542,15 @@ module AMQP
|
|
528
542
|
206 # frame end
|
529
543
|
].pack("C S> L> S> S> C")
|
530
544
|
end
|
545
|
+
|
546
|
+
def self.heartbeat
|
547
|
+
[
|
548
|
+
8, # type: heartbeat
|
549
|
+
0, # channel id
|
550
|
+
0, # frame size
|
551
|
+
206 # frame end
|
552
|
+
].pack("C S> L> C")
|
553
|
+
end
|
531
554
|
end
|
532
555
|
end
|
533
556
|
end
|
data/lib/amqp/client/version.rb
CHANGED
data/lib/amqp/client.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "set"
|
4
3
|
require_relative "client/version"
|
5
4
|
require_relative "client/connection"
|
6
5
|
require_relative "client/exchange"
|
@@ -52,7 +51,7 @@ module AMQP
|
|
52
51
|
def start
|
53
52
|
@stopped = false
|
54
53
|
Thread.new(connect(read_loop_thread: false)) do |conn|
|
55
|
-
Thread.abort_on_exception = true # Raising an unhandled exception is a bug
|
54
|
+
Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
|
56
55
|
loop do
|
57
56
|
break if @stopped
|
58
57
|
|
metadata
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: amqp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
8
|
-
autorequire:
|
7
|
+
- CloudAMQP
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
12
|
description: Modern AMQP 0-9-1 Ruby client
|
14
13
|
email:
|
15
|
-
-
|
14
|
+
- team@cloudamqp.com
|
16
15
|
executables: []
|
17
16
|
extensions: []
|
18
17
|
extra_rdoc_files: []
|
@@ -26,6 +25,7 @@ files:
|
|
26
25
|
- ".rubocop_todo.yml"
|
27
26
|
- ".yardopts"
|
28
27
|
- CHANGELOG.md
|
28
|
+
- CODEOWNERS
|
29
29
|
- Gemfile
|
30
30
|
- LICENSE.txt
|
31
31
|
- README.md
|
@@ -52,7 +52,6 @@ metadata:
|
|
52
52
|
homepage_uri: https://github.com/cloudamqp/amqp-client.rb
|
53
53
|
source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
|
54
54
|
changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
|
55
|
-
post_install_message:
|
56
55
|
rdoc_options: []
|
57
56
|
require_paths:
|
58
57
|
- lib
|
@@ -60,15 +59,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
59
|
requirements:
|
61
60
|
- - ">="
|
62
61
|
- !ruby/object:Gem::Version
|
63
|
-
version: 2.
|
62
|
+
version: 3.2.0
|
64
63
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
64
|
requirements:
|
66
65
|
- - ">="
|
67
66
|
- !ruby/object:Gem::Version
|
68
67
|
version: '0'
|
69
68
|
requirements: []
|
70
|
-
rubygems_version: 3.
|
71
|
-
signing_key:
|
69
|
+
rubygems_version: 3.6.9
|
72
70
|
specification_version: 4
|
73
71
|
summary: AMQP 0-9-1 client
|
74
72
|
test_files: []
|