amqp-client 1.1.7 → 1.2.1

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: 7b2125fb98c3f0172a05bce1cbf8aa0b6530df76685522c4d25772fa1edd2dc4
4
- data.tar.gz: 204891c1da627b813f37b160029ab55ecf58c91481949b02dc5bf3b163288d0c
3
+ metadata.gz: 8cfb2dd354eeb33334cf6848b168f2a0caa486b40cefcce924910a16a77ffa4f
4
+ data.tar.gz: 7a7896809e27539ab3395b833038c5b328298c9cf6a25aeff7f706e05657e995
5
5
  SHA512:
6
- metadata.gz: 290ce1a3d301e1119056e39eeb309b55d7fa0752e7bc4b012777ccab7c506f62eef255bffc7b62258bf8a56a22ea46e19f42c00ad91f4b8441b7ecf414c29c8d
7
- data.tar.gz: 22070957cc3c58d77f7d6c15476943a9f23ab34496dfc473618c4625c4f8531eddcfc8fb92086bf566aaf548d650045c65617ac68d73a49ec07c4ad4f971030a
6
+ metadata.gz: c8d0c1797b3b8a81fbe33a154bb255c452f09d925fdeafdfc03fdbb3342b116d7875d6166f7e9b948232f09d69c16179e0270c752b3afdd955d4ad6fca3b6cd1
7
+ data.tar.gz: 47c518dee777f61435e748045fb7cfebf808ac5ed9cd53813081b222a9abc81b82f85f436482b25dba1dcd10a96c1d1f9bbf4b5266fc90ece48307b61f03ccfb
@@ -25,7 +25,7 @@ jobs:
25
25
 
26
26
  steps:
27
27
  - name: Checkout repository
28
- uses: actions/checkout@v4
28
+ uses: actions/checkout@v5
29
29
 
30
30
  # Initializes the CodeQL tools for scanning.
31
31
  - name: Initialize CodeQL
@@ -12,7 +12,7 @@ jobs:
12
12
  docs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
15
+ - uses: actions/checkout@v5
16
16
  - name: Setup Ruby
17
17
  uses: ruby/setup-ruby@v1
18
18
  with:
@@ -19,26 +19,34 @@ 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: true, sudo: false }
30
- - { ruby: truffleruby, allow-failure: true, sudo: false }
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@v4
37
+ - uses: actions/checkout@v5
37
38
  - uses: ruby/setup-ruby@v1
38
39
  with:
39
40
  bundler-cache: true
40
41
  ruby-version: ${{ matrix.ruby }}
42
+ - name: Run tests (excluding TLS tests) (JRuby)
43
+ if: ${{ matrix.ruby == 'jruby' }}
44
+ continue-on-error: ${{ matrix.allow-failure || false }}
45
+ run: bundle exec rake
46
+ env:
47
+ JAVA_OPTS: "-Djava.net.preferIPv4Stack=true"
41
48
  - name: Run tests (excluding TLS tests)
49
+ if: ${{ matrix.ruby != 'jruby' }}
42
50
  continue-on-error: ${{ matrix.allow-failure || false }}
43
51
  run: bundle exec rake
44
52
  env:
@@ -55,6 +63,10 @@ jobs:
55
63
  - "jruby"
56
64
  - "truffleruby"
57
65
  steps:
66
+ - name: Configure dpkg to skip building man pages
67
+ run: |
68
+ echo 'path-exclude /usr/share/doc/*' | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc
69
+ echo 'path-exclude /usr/share/man/*' | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc
58
70
  - name: Install RabbitMQ
59
71
  run: sudo apt-get update && sudo apt-get install -y rabbitmq-server
60
72
  - name: Stop RabbitMQ
@@ -81,12 +93,20 @@ jobs:
81
93
  run: sudo systemctl start rabbitmq-server
82
94
  - name: Verify RabbitMQ started correctly
83
95
  run: while true; do sudo rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
84
- - uses: actions/checkout@v4
96
+ - uses: actions/checkout@v5
85
97
  - uses: ruby/setup-ruby@v1
86
98
  with:
87
99
  bundler-cache: true
88
100
  ruby-version: ${{ matrix.ruby }}
101
+ - name: Run TLS tests (JRuby)
102
+ if: ${{ matrix.ruby == 'jruby' }}
103
+ run: bundle exec rake
104
+ env:
105
+ JAVA_OPTS: "-Djava.net.preferIPv4Stack=true"
106
+ TEST_AMQP_HOST: "localhost"
107
+ TESTOPTS: --name=/_tls$/
89
108
  - name: Run TLS tests
109
+ if: ${{ matrix.ruby != 'jruby' }}
90
110
  run: bundle exec rake
91
111
  env:
92
112
  TESTOPTS: --name=/_tls$/
@@ -104,7 +124,7 @@ jobs:
104
124
  run: brew install rabbitmq
105
125
  - name: Start RabbitMQ
106
126
  run: brew services start rabbitmq
107
- - uses: actions/checkout@v4
127
+ - uses: actions/checkout@v5
108
128
  - uses: ruby/setup-ruby@v1
109
129
  with:
110
130
  bundler-cache: true
@@ -113,3 +133,15 @@ jobs:
113
133
  run: while true; do rabbitmq-diagnostics status 2>/dev/null && break; echo -n .; sleep 2; done
114
134
  - name: Run tests (excluding TLS tests)
115
135
  run: bundle exec rake
136
+
137
+ lint:
138
+ runs-on: ubuntu-latest
139
+ timeout-minutes: 10
140
+ steps:
141
+ - uses: actions/checkout@v5
142
+ - uses: ruby/setup-ruby@v1
143
+ with:
144
+ bundler-cache: true
145
+ ruby-version: ruby
146
+ - name: Run RuboCop
147
+ run: bundle exec rake rubocop
@@ -12,8 +12,9 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  permissions:
14
14
  id-token: write # for trusted publishing
15
+ contents: write # for creating releases
15
16
  steps:
16
- - uses: actions/checkout@v4
17
+ - uses: actions/checkout@v5
17
18
  - uses: ruby/setup-ruby@v1
18
19
  with:
19
20
  bundler-cache: true
@@ -24,3 +25,30 @@ jobs:
24
25
  - run: gem build *.gemspec
25
26
  - run: gem install *.gem
26
27
  - run: gem push *.gem
28
+
29
+ # create GitHub release
30
+ - name: Extract release notes
31
+ id: extract_release_notes
32
+ run: |
33
+ # Extract version from tag (remove 'v' prefix)
34
+ VERSION=${GITHUB_REF#refs/tags/v}
35
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
36
+
37
+ # Extract changelog section for this version with better handling
38
+ if grep -q "## \[$VERSION\]" CHANGELOG.md; then
39
+ awk "/^## \[$VERSION\]/ {flag=1; next} /^## \[/ && flag {exit} flag && /\S/ {print}" CHANGELOG.md > release_notes.md
40
+ echo "Release notes extracted for version $VERSION:"
41
+ cat release_notes.md
42
+ else
43
+ echo "No changelog entry found for version $VERSION" > release_notes.md
44
+ echo "Warning: No changelog entry found for version $VERSION"
45
+ fi
46
+
47
+ - name: Create GitHub Release
48
+ uses: softprops/action-gh-release@v2
49
+ with:
50
+ name: Release ${{ steps.extract_release_notes.outputs.version }}
51
+ body_path: release_notes.md
52
+ draft: false
53
+ prerelease: false
54
+ files: '*.gem'
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.6
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
- - 'lib/amqp-client.rb'
23
+ - "lib/amqp-client.rb"
22
24
 
23
25
  Metrics/PerceivedComplexity:
24
26
  Exclude:
25
- - 'lib/amqp/client/properties.rb'
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.1] - 2025-09-15
4
+
5
+ - Added: Convenience methods for creating exchange types: `fanout()`, `direct()`, `topic()`, and `headers()`
6
+ - Added: Support for binding with high level objects (Exchange and Queue objects can now be passed as binding sources)
7
+ - Fixed: Bug where a client without any connection could not be closed properly
8
+
9
+ ## [1.2.0] - 2025-09-10
10
+
11
+ - Fixed: `Connection#channel` wasn't thread-safe
12
+ - Added: Support for heartbeats
13
+
3
14
  ## [1.1.7] - 2024-05-12
4
15
 
5
16
  - Support for Connection.update-secret
@@ -48,7 +59,7 @@
48
59
 
49
60
  ## [1.0.1] - 2021-09-06
50
61
 
51
- - The API is fully documented! https://cloudamqp.github.io/amqp-client.rb/
62
+ - The API is fully documented! <https://cloudamqp.github.io/amqp-client.rb/>
52
63
  - Fixed: Socket writing is now thread-safe
53
64
  - Change: Block while waiting for basic_cancel by default
54
65
  - Added: Can specify channel_max, heartbeat and frame_max as options to the Client/Connection
data/CODEOWNERS CHANGED
@@ -1 +1 @@
1
- * @84codes/customer
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
- $ bundle install
116
+ bundle install
117
117
 
118
118
  Or install it yourself as:
119
119
 
120
- $ gem install amqp-client
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`. To release a new version, update the changelog and version number in `version.rb`, make a commit, and then run `bundle exec rake release:source_control_push`, which will create a git tag for the version, push git commits and the created tag. GitHub Actions will then push the `.gem` file to [rubygems.org](https://rubygems.org/gems/amqp-client).
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 preparation process. The actual gem building and publishing is handled automatically by GitHub Actions when a tag is pushed.
131
+
132
+ #### Quick Release (Patch Version)
133
+
134
+ ```bash
135
+ rake release:prepare
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. Push commits and tags to the remote repository
145
+ 6. GitHub Actions will automatically build and publish the gem to RubyGems
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:prepare[minor]
154
+
155
+ # Major version bump (e.g., 1.2.0 → 2.0.0)
156
+ rake release:prepare[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 with changelog entries
171
+ rake release:tag
172
+
173
+ # Push tag to remote (handles conflicts)
174
+ rake release:push_tag
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. GitHub Actions will automatically build and publish the gem when the tag is pushed
127
186
 
128
187
  ## Contributing
129
188
 
data/Rakefile CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
3
  require "rake/testtask"
5
4
 
6
5
  Rake::TestTask.new(:test) do |t|
@@ -18,12 +17,181 @@ end
18
17
 
19
18
  require "rubocop/rake_task"
20
19
 
21
- RuboCop::RakeTask.new do |task|
22
- task.requires << "rubocop-minitest"
23
- end
20
+ RuboCop::RakeTask.new
24
21
 
25
22
  require "yard"
26
23
 
27
24
  YARD::Rake::YardocTask.new
28
25
 
29
- task default: [:test, *(:rubocop if RUBY_ENGINE == "ruby")]
26
+ # Release helper methods
27
+ def current_version
28
+ version_file = "lib/amqp/client/version.rb"
29
+ content = File.read(version_file)
30
+ content.match(/VERSION = "(.+)"/)[1]
31
+ end
32
+
33
+ def extract_changelog_for_version(version)
34
+ changelog = File.read("CHANGELOG.md")
35
+
36
+ # Find the section for this version
37
+ version_pattern = /^## \[#{Regexp.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\z)/m
38
+ match = changelog.match(version_pattern)
39
+
40
+ if match
41
+ # Clean up the changelog entries
42
+ entries = match[1].strip
43
+ # Remove empty lines at the start and end
44
+ entries.gsub(/\A\s*\n+/, "").gsub(/\n+\s*\z/, "")
45
+ else
46
+ "No changelog entries found for version #{version}"
47
+ end
48
+ end
49
+
50
+ def push_tag_to_remote(version)
51
+ # Check if tag exists on remote
52
+ remote_tag_exists = system("git ls-remote --tags origin | grep -q refs/tags/v#{version}")
53
+
54
+ if remote_tag_exists
55
+ puts "Tag v#{version} already exists on remote. Force pushing updated tag..."
56
+ system("git push origin v#{version} --force")
57
+ else
58
+ puts "Pushing new tag v#{version} to remote..."
59
+ system("git push origin v#{version}")
60
+ end
61
+ end
62
+
63
+ def bump_version(version_type)
64
+ unless %w[major minor patch].include?(version_type)
65
+ puts "Invalid version type. Use: major, minor, or patch"
66
+ exit 1
67
+ end
68
+
69
+ version_file = "lib/amqp/client/version.rb"
70
+ content = File.read(version_file)
71
+
72
+ current_version = content.match(/VERSION = "(.+)"/)[1]
73
+ major, minor, patch = current_version.split(".").map(&:to_i)
74
+
75
+ case version_type
76
+ when "major"
77
+ major += 1
78
+ minor = 0
79
+ patch = 0
80
+ when "minor"
81
+ minor += 1
82
+ patch = 0
83
+ when "patch"
84
+ patch += 1
85
+ end
86
+
87
+ new_version = "#{major}.#{minor}.#{patch}"
88
+ new_content = content.gsub(/VERSION = ".+"/, %(VERSION = "#{new_version}"))
89
+
90
+ File.write(version_file, new_content)
91
+ puts "Bumped version from #{current_version} to #{new_version}"
92
+ end
93
+
94
+ def update_changelog
95
+ version = current_version
96
+ date = Time.now.strftime("%Y-%m-%d")
97
+
98
+ changelog = File.read("CHANGELOG.md")
99
+
100
+ if changelog.include?("## [#{version}]")
101
+ puts "Version #{version} already exists in CHANGELOG.md"
102
+ else
103
+ updated_changelog = changelog.sub(
104
+ "## [Unreleased]",
105
+ "## [Unreleased]\n\n## [#{version}] - #{date}"
106
+ )
107
+
108
+ File.write("CHANGELOG.md", updated_changelog)
109
+ puts "Updated CHANGELOG.md with version #{version}"
110
+ end
111
+ end
112
+
113
+ def create_git_tag
114
+ version = current_version
115
+
116
+ system("git add .")
117
+ system("git commit -m 'Release #{version}'")
118
+
119
+ # Check if tag already exists locally and remove it if it does
120
+ if system("git tag -l v#{version} | grep -q v#{version}")
121
+ puts "Tag v#{version} already exists locally, removing it..."
122
+ system("git tag -d v#{version}")
123
+ end
124
+
125
+ # Extract changelog entries for this version
126
+ changelog_entries = extract_changelog_for_version(version)
127
+
128
+ # Create tag message with version and changelog
129
+ tag_message = "Release #{version}\n\n#{changelog_entries}"
130
+
131
+ # Create annotated tag with the changelog
132
+ system("git", "tag", "-a", "v#{version}", "-m", tag_message)
133
+
134
+ puts "Created git tag v#{version} with changelog entries"
135
+ end
136
+
137
+ def prepare_release_process(version_type)
138
+ puts "Preparing release process..."
139
+
140
+ # Ensure working directory is clean
141
+ unless system("git diff --quiet && git diff --cached --quiet")
142
+ puts "Working directory is not clean. Please commit or stash changes first."
143
+ exit 1
144
+ end
145
+
146
+ # Bump version
147
+ Rake::Task["release:bump"].invoke(version_type)
148
+ Rake::Task["release:bump"].reenable
149
+
150
+ # Update changelog
151
+ Rake::Task["release:changelog"].invoke
152
+ Rake::Task["release:changelog"].reenable
153
+
154
+ # Create tag and push
155
+ Rake::Task["release:tag"].invoke
156
+ Rake::Task["release:tag"].reenable
157
+
158
+ # Push to git
159
+ system("git push origin")
160
+
161
+ # Handle tag push with potential conflicts
162
+ version = current_version
163
+ push_tag_to_remote(version)
164
+
165
+ puts "Successfully prepared release #{version}!"
166
+ puts "The CI will automatically build and publish the gem when the tag is pushed."
167
+ end
168
+
169
+ namespace :release do
170
+ desc "Bump version (usage: rake release:bump[major|minor|patch])"
171
+ task :bump, [:type] do |_t, args|
172
+ bump_version(args[:type] || "patch")
173
+ end
174
+
175
+ desc "Update changelog with current version"
176
+ task :changelog do
177
+ update_changelog
178
+ end
179
+
180
+ desc "Create git tag for current version"
181
+ task :tag do
182
+ create_git_tag
183
+ end
184
+
185
+ desc "Push tag to remote (handles conflicts)"
186
+ task :push_tag do
187
+ version = current_version
188
+ push_tag_to_remote(version)
189
+ end
190
+
191
+ desc "Prepare release (bump version, update changelog, create tag, push to git)"
192
+ task :prepare, [:type] => %i[test rubocop] do |_t, args|
193
+ prepare_release_process(args[:type] || "patch")
194
+ end
195
+ end
196
+
197
+ 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 = ["Carl Hörberg"]
9
- spec.email = ["carl@cloudamqp.com"]
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.6.0")
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"
@@ -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 [Integer] correlation_id A correlation id, most often used used for RPC communication
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 maxium keep in flight
349
- # @param prefetch_size [Integer] Number of bytes to maxium keep in flight
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)
@@ -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
- if id
93
- ch = @channels[id] ||= Channel.new(self, id)
94
- else
95
- 1.upto(@channel_max) do |i|
96
- break id = i unless @channels.key? i
97
- end
98
- raise Error, "Max channels reached" if id.nil?
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
- ch = @channels[id] = Channel.new(self, id)
105
+ @channels[id] = Channel.new(self, id)
106
+ end
101
107
  end
102
108
  ch.open
103
109
  end
@@ -175,6 +181,7 @@ module AMQP
175
181
  def write_bytes(*bytes)
176
182
  @write_lock.synchronize do
177
183
  @socket.write(*bytes)
184
+ update_last_activity
178
185
  end
179
186
  rescue *READ_EXCEPTIONS => e
180
187
  raise Error::ConnectionClosed.new(*@closed) if @closed
@@ -206,6 +213,7 @@ module AMQP
206
213
 
207
214
  # parse the frame, will return false if a close frame was received
208
215
  parse_frame(type, channel_id, frame_buffer) || return
216
+ update_last_activity
209
217
  end
210
218
  nil
211
219
  rescue *READ_EXCEPTIONS => e
@@ -232,7 +240,7 @@ module AMQP
232
240
  READ_EXCEPTIONS = [IOError, OpenSSL::OpenSSLError, SystemCallError,
233
241
  RUBY_ENGINE == "jruby" ? java.lang.NullPointerException : nil].compact.freeze
234
242
 
235
- 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
236
244
  channel = @channels[channel_id]
237
245
  case type
238
246
  when 1 # method frame
@@ -277,11 +285,11 @@ module AMQP
277
285
  reply_code, reply_text_len = buf.unpack("@4 S> C")
278
286
  reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
279
287
  classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
280
- channel = @channels.delete(channel_id)
288
+ channel = @channels_lock.synchronize { @channels.delete(channel_id) }
281
289
  channel.closed!(:channel, reply_code, reply_text, classid, methodid)
282
290
  write_bytes FrameBytes.channel_close_ok(channel_id)
283
291
  when 41 # channel#close-ok
284
- channel = @channels.delete(channel_id)
292
+ channel = @channels_lock.synchronize { @channels.delete(channel_id) }
285
293
  channel.reply [:channel_close_ok]
286
294
  else raise Error::UnsupportedMethodFrame, class_id, method_id
287
295
  end
@@ -406,17 +414,61 @@ module AMQP
406
414
  channel.header_delivered body_size, properties
407
415
  when 3 # body
408
416
  channel.body_delivered buf
417
+ when 8 # heartbeat
418
+ handle_server_heartbeat(channel_id)
409
419
  else raise Error::UnsupportedFrameType, type
410
420
  end
411
421
  true
412
422
  end
413
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
+
414
464
  def expect(expected_frame_type)
415
465
  frame_type, args = @replies.pop
416
466
  if frame_type.nil?
417
467
  return if expected_frame_type == :close_ok
418
468
 
419
- raise(Error::ConnectionClosed, "while waiting for #{expected_frame_type}")
469
+ raise Error::ConnectionClosed.new(*@closed) if @closed
470
+
471
+ raise Error, "Connection closed while waiting for #{expected_frame_type}"
420
472
  end
421
473
  frame_type == expected_frame_type || raise(Error::UnexpectedFrame.new(expected_frame_type, frame_type))
422
474
  args
@@ -454,7 +506,7 @@ module AMQP
454
506
  channel_max, frame_max, heartbeat = nil
455
507
  socket.write "AMQP\x00\x00\x09\x01"
456
508
  buf = String.new(capacity: 4096)
457
- loop do
509
+ loop do # rubocop:disable Metrics/BlockLength
458
510
  begin
459
511
  socket.readpartial(4096, buf)
460
512
  rescue *READ_EXCEPTIONS => e
@@ -483,6 +535,7 @@ module AMQP
483
535
  channel_max = [channel_max, options.fetch(:channel_max, 2048).to_i].min
484
536
  frame_max = [frame_max, options.fetch(:frame_max, 131_072).to_i].min
485
537
  heartbeat = [heartbeat, options.fetch(:heartbeat, 0).to_i].min
538
+ start_heartbeats(heartbeat) if heartbeat.positive?
486
539
  socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
487
540
  socket.write FrameBytes.connection_open(vhost)
488
541
  when 41 # connection#open-ok
@@ -508,7 +561,7 @@ module AMQP
508
561
  raise e
509
562
  end
510
563
 
511
- # Enable TCP keepalive, which is prefered to heartbeats
564
+ # Enable TCP keepalive, which is preferred to heartbeats
512
565
  # @return [void]
513
566
  def enable_tcp_keepalive(socket, idle = 60, interval = 10, count = 3)
514
567
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
@@ -4,6 +4,8 @@ module AMQP
4
4
  class Client
5
5
  # High level representation of an exchange
6
6
  class Exchange
7
+ attr_reader :name
8
+
7
9
  # Should only be initialized from the Client
8
10
  # @api private
9
11
  def initialize(client, name)
@@ -14,7 +16,7 @@ module AMQP
14
16
  # Publish to the exchange
15
17
  # @param body [String] The message body
16
18
  # @param routing_key [String] The routing key of the message,
17
- # the exchange may use this when routing the message to bound queues
19
+ # the exchange may use this when routing the message to bound queues (defaults to empty string)
18
20
  # @param properties [Properties]
19
21
  # @option properties [String] content_type Content type of the message body
20
22
  # @option properties [String] content_encoding Content encoding of the body
@@ -30,28 +32,30 @@ module AMQP
30
32
  # @option properties [String] user_id Can be used to verify that this is the user that published the message
31
33
  # @option properties [String] app_id Can be used to indicates which app that generated the message
32
34
  # @return [Exchange] self
33
- def publish(body, routing_key, **properties)
35
+ def publish(body, routing_key = "", **properties)
34
36
  @client.publish(body, @name, routing_key, **properties)
35
37
  self
36
38
  end
37
39
 
38
40
  # Bind to another exchange
39
- # @param exchange [String] Name of the exchange to bind to
40
- # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
41
+ # @param source [String, Exchange] Name of the exchange to bind to, or the exchange object itself
42
+ # @param binding_key [String] Binding key on which messages that match might be routed (defaults to empty string)
41
43
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
42
44
  # @return [Exchange] self
43
- def bind(exchange, binding_key, arguments: {})
44
- @client.exchange_bind(@name, exchange, binding_key, arguments: arguments)
45
+ def bind(source, binding_key = "", arguments: {})
46
+ source = source.is_a?(String) ? source : source.name
47
+ @client.exchange_bind(@name, source, binding_key, arguments: arguments)
45
48
  self
46
49
  end
47
50
 
48
51
  # Unbind from another exchange
49
- # @param exchange [String] Name of the exchange to unbind from
50
- # @param binding_key [String] Binding key which the queue is bound to the exchange with
52
+ # @param source [String, Exchange] Name of the exchange to unbind from, or the exchange object itself
53
+ # @param binding_key [String] Binding key which the queue is bound to the exchange with (defaults to empty string)
51
54
  # @param arguments [Hash] Arguments matching the binding that's being removed
52
55
  # @return [Exchange] self
53
- def unbind(exchange, binding_key, arguments: {})
54
- @client.exchange_unbind(@name, exchange, binding_key, arguments: arguments)
56
+ def unbind(source, binding_key = "", arguments: {})
57
+ source = source.is_a?(String) ? source : source.name
58
+ @client.exchange_unbind(@name, source, binding_key, arguments: arguments)
55
59
  self
56
60
  end
57
61
 
@@ -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
  [
@@ -542,6 +542,15 @@ module AMQP
542
542
  206 # frame end
543
543
  ].pack("C S> L> S> S> C")
544
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
545
554
  end
546
555
  end
547
556
  end
@@ -4,6 +4,8 @@ module AMQP
4
4
  class Client
5
5
  # Queue abstraction
6
6
  class Queue
7
+ attr_reader :name
8
+
7
9
  # Should only be initialized from the Client
8
10
  # @api private
9
11
  def initialize(client, name)
@@ -35,21 +37,23 @@ module AMQP
35
37
  end
36
38
 
37
39
  # Bind the queue to an exchange
38
- # @param exchange [String] Name of the exchange to bind to
40
+ # @param exchange [String, Exchange] Name of the exchange to bind to, or the exchange object itself
39
41
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
40
42
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
41
43
  # @return [self]
42
44
  def bind(exchange, binding_key, arguments: {})
45
+ exchange = exchange.is_a?(String) ? exchange : exchange.name
43
46
  @client.bind(@name, exchange, binding_key, arguments: arguments)
44
47
  self
45
48
  end
46
49
 
47
50
  # Unbind the queue from an exchange
48
- # @param exchange [String] Name of the exchange to unbind from
51
+ # @param exchange [String, Exchange] Name of the exchange to unbind from, or the exchange object itself
49
52
  # @param binding_key [String] Binding key which the queue is bound to the exchange with
50
53
  # @param arguments [Hash] Arguments matching the binding that's being removed
51
54
  # @return [self]
52
55
  def unbind(exchange, binding_key, arguments: {})
56
+ exchange = exchange.is_a?(String) ? exchange : exchange.name
53
57
  @client.unbind(@name, exchange, binding_key, arguments: arguments)
54
58
  self
55
59
  end
@@ -3,6 +3,6 @@
3
3
  module AMQP
4
4
  class Client
5
5
  # Version of the client library
6
- VERSION = "1.1.7"
6
+ VERSION = "1.2.1"
7
7
  end
8
8
  end
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
 
@@ -84,6 +83,8 @@ module AMQP
84
83
  return if @stopped
85
84
 
86
85
  @stopped = true
86
+ return unless @connq.size.positive?
87
+
87
88
  conn = @connq.pop
88
89
  conn.close
89
90
  nil
@@ -116,6 +117,12 @@ module AMQP
116
117
  end
117
118
 
118
119
  # Declare an exchange and return a high level Exchange object
120
+ # @param name [String] Name of the exchange
121
+ # @param type [String] Type of the exchange, one of "direct", "fanout", "topic", "headers" or custom exchange type
122
+ # @param durable [Boolean] If true the exchange will survive broker restarts
123
+ # @param auto_delete [Boolean] If true the exchange will be deleted when the last queue is unbound
124
+ # @param internal [Boolean] If true the exchange will not accept directly published messages
125
+ # @param arguments [Hash] Custom arguments such as alternate-exchange etc.
119
126
  # @return [Exchange]
120
127
  # @example
121
128
  # amqp = AMQP::Client.new.start
@@ -131,6 +138,42 @@ module AMQP
131
138
  end
132
139
  end
133
140
 
141
+ # Declare a fanout exchange and return a high level Exchange object
142
+ # @param name [String] Name of the exchange (defaults to "amq.fanout")
143
+ # @see {#exchange} for other parameters
144
+ # @return [Exchange]
145
+ def fanout(name = "amq.fanout", **kwargs)
146
+ exchange(name, "fanout", **kwargs)
147
+ end
148
+
149
+ # Declare a direct exchange and return a high level Exchange object
150
+ # @param name [String] Name of the exchange (defaults to "" for the default direct exchange)
151
+ # @see {#exchange} for other parameters
152
+ # @return [Exchange]
153
+ def direct(name = "", **kwargs)
154
+ return exchange(name, "direct", **kwargs) unless name.empty?
155
+
156
+ @exchanges.fetch(name) do
157
+ @exchanges[name] = Exchange.new(self, name)
158
+ end
159
+ end
160
+
161
+ # Declare a topic exchange and return a high level Exchange object
162
+ # @param name [String] Name of the exchange (defaults to "amq.topic")
163
+ # @see {#exchange} for other parameters
164
+ # @return [Exchange]
165
+ def topic(name = "amq.topic", **kwargs)
166
+ exchange(name, "topic", **kwargs)
167
+ end
168
+
169
+ # Declare a headers exchange and return a high level Exchange object
170
+ # @param name [String] Name of the exchange (defaults to "amq.headers")
171
+ # @see {#exchange} for other parameters
172
+ # @return [Exchange]
173
+ def headers(name = "amq.headers", **kwargs)
174
+ exchange(name, "headers", **kwargs)
175
+ end
176
+
134
177
  # @!endgroup
135
178
  # @!group Publish
136
179
 
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.1.7
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
- - Carl Hörberg
8
- autorequire:
7
+ - CloudAMQP
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-05-12 00:00:00.000000000 Z
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
- - carl@cloudamqp.com
14
+ - team@cloudamqp.com
16
15
  executables: []
17
16
  extensions: []
18
17
  extra_rdoc_files: []
@@ -53,7 +52,6 @@ metadata:
53
52
  homepage_uri: https://github.com/cloudamqp/amqp-client.rb
54
53
  source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
55
54
  changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
56
- post_install_message:
57
55
  rdoc_options: []
58
56
  require_paths:
59
57
  - lib
@@ -61,15 +59,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
61
59
  requirements:
62
60
  - - ">="
63
61
  - !ruby/object:Gem::Version
64
- version: 2.6.0
62
+ version: 3.2.0
65
63
  required_rubygems_version: !ruby/object:Gem::Requirement
66
64
  requirements:
67
65
  - - ">="
68
66
  - !ruby/object:Gem::Version
69
67
  version: '0'
70
68
  requirements: []
71
- rubygems_version: 3.5.9
72
- signing_key:
69
+ rubygems_version: 3.6.9
73
70
  specification_version: 4
74
71
  summary: AMQP 0-9-1 client
75
72
  test_files: []