freddy 2.11.2 → 3.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a6c79059c1c1c188d1a77c6bc37ddc8f89e072c4d92a3197c8852b38253e781
4
- data.tar.gz: fcd02b33005200bf4bde9959186a694f4751f6cb39698ae7a3b1ef40cec21d51
3
+ metadata.gz: 2fee629805345ae363989fe96424d8ba0bcffd08ddd33f4e19327338ff5d0325
4
+ data.tar.gz: 4c1c4d92499a10b5e4cfd19adb390c8457a2ec873297a678fd755f70addd93e7
5
5
  SHA512:
6
- metadata.gz: 034cdcce2a26c9f2ff8a8e3d02656520bb298e5a4c07f655253648ee0846f0b57f7cf883f31c2ec3cdf54f17d91f9394589e2440f7b686a2e4850926ab7001d1
7
- data.tar.gz: fc13af3594a708c59d17cc2769bccf45f89ed0744e7107484d32f734385bc28ee1f2681d289694be30970aa67689a1892033c31562084f2b82b91d3ed7c115fe
6
+ metadata.gz: ad6991648e494845fde59d859068b42463959a81e2a846fdc5026556ac5ce7505bfa53370b78c6e9d30b101ad45b71faa67f83488d48a69a98aa5e5d6a104e90
7
+ data.tar.gz: 2786a37b993927f816e330846cc16a5aaaf07aff8d4c4c3f658c1088991f6ed9457d0931b1e6e253ee13e6ce85618e172747b801408100038f6b53f9d7ab57ee
@@ -19,11 +19,11 @@ jobs:
19
19
  - 5672:5672
20
20
  options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s --health-timeout 5s --health-retries 5
21
21
  steps:
22
- - uses: actions/checkout@v2
22
+ - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
23
23
  with:
24
24
  submodules: true
25
25
  - name: Set up Ruby
26
- uses: ruby/setup-ruby@v1
26
+ uses: ruby/setup-ruby@762794c140bbeda0f1224786aa33b4b46783a6c1 # v1.326.0
27
27
  with:
28
28
  ruby-version: ${{ matrix.ruby-version }}
29
29
  bundler-cache: true
@@ -6,16 +6,18 @@ on:
6
6
  - master
7
7
  jobs:
8
8
  build:
9
+ permissions:
10
+ contents: write
9
11
  runs-on: ubuntu-latest
10
12
 
11
13
  steps:
12
- - uses: actions/checkout@v2
14
+ - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
13
15
  with:
14
16
  fetch-depth: 2
15
17
 
16
18
 
17
19
  - name: Release Gem
18
- uses: discourse/publish-rubygems-action@4bd305c65315cb691bad1e8de97a87aaf29a0a85
20
+ uses: discourse/publish-rubygems-action@4bd305c65315cb691bad1e8de97a87aaf29a0a85 # v3
19
21
  env:
20
22
  RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
21
23
  RELEASE_COMMAND: rake release
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- freddy (2.11.2)
5
- bunny (~> 2.24)
4
+ freddy (3.0.0)
5
+ bunny (~> 3.5)
6
6
  concurrent-ruby (~> 1.0, >= 1.3.7)
7
7
  oj (~> 3.6, >= 3.17.3)
8
8
  opentelemetry-api (~> 1.0)
@@ -12,23 +12,27 @@ PATH
12
12
  GEM
13
13
  remote: https://rubygems.org/
14
14
  specs:
15
- amq-protocol (2.5.1)
15
+ amq-protocol (2.9.0)
16
16
  ast (2.4.3)
17
17
  bigdecimal (4.1.2)
18
- bunny (2.24.0)
19
- amq-protocol (~> 2.3)
18
+ bunny (3.5.0)
19
+ amq-protocol (~> 2.9)
20
+ logger (~> 1, >= 1.7)
21
+ openssl (>= 3.3)
20
22
  sorted_set (~> 1, >= 1.0.2)
21
23
  coderay (1.1.3)
22
24
  concurrent-ruby (1.3.7)
23
25
  diff-lcs (1.4.4)
24
26
  hamster (3.0.0)
25
27
  concurrent-ruby (~> 1.0)
26
- json (2.9.1)
28
+ json (2.21.2)
27
29
  language_server-protocol (3.17.0.3)
30
+ logger (1.7.0)
28
31
  method_source (1.0.0)
29
32
  oj (3.17.3)
30
33
  bigdecimal (>= 3.0)
31
34
  ostruct (>= 0.2)
35
+ openssl (4.0.2)
32
36
  opentelemetry-api (1.5.0)
33
37
  opentelemetry-common (0.19.6)
34
38
  opentelemetry-api (~> 1.0)
data/README.md CHANGED
@@ -15,6 +15,15 @@ freddy = Freddy.build(logger, host: 'localhost', port: 5672, user: 'guest', pass
15
15
 
16
16
  A new version is created when a change is merged into the master branch that changes the version number in `freddy.gemspec`. A Github Action will create a tag for the version and push the `.gem` file to [rubygems.org](https://rubygems.org)
17
17
 
18
+ ## Upgrading from 2.x to 3.0
19
+
20
+ Freddy 3.0 runs on Bunny 3.5 instead of 2.24, which fixes RPC channels left closed after a connection recovery.
21
+
22
+ * Change the Gemfile constraint to `gem 'freddy', '~> 3.0'`; Ruby 3.2 or later is required.
23
+ * Bunny 3.5 also installs the `openssl` and `logger` gems: check TLS connections in a deployed environment before production.
24
+ * Code that calls Bunny directly must follow the "Breaking Changes" of 3.0.0 in the [Bunny changelog](https://github.com/ruby-amqp/bunny/blob/main/ChangeLog.md), e.g. delivery tags are now plain integers.
25
+ * Code that reaches into Freddy internals must follow their new signatures: `Freddy::Delivery.new` takes the recovery count, and the adapter channel's `acknowledge`/`reject` take the delivery instead of its tag.
26
+
18
27
  ## Supported message queues
19
28
 
20
29
  These message queues have been tested and are working with Freddy. Other queues can be added easily:
data/freddy.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ['lib']
19
19
 
20
- spec.add_dependency 'bunny', '~> 2.24'
20
+ spec.add_dependency 'bunny', '~> 3.5'
21
21
  spec.add_dependency 'concurrent-ruby', '~> 1.0', '>= 1.3.7'
22
22
  spec.add_dependency 'oj', '~> 3.6', '>= 3.17.3'
23
23
  spec.add_dependency 'opentelemetry-api', '~> 1.0'
@@ -20,12 +20,14 @@ class Freddy
20
20
 
21
21
  def initialize(bunny)
22
22
  @bunny = bunny
23
+ @recovery_count = Concurrent::AtomicFixnum.new
24
+ @bunny.before_recovery_attempt_starts { @recovery_count.increment }
23
25
  end
24
26
 
25
27
  def create_channel(prefetch: nil)
26
28
  bunny_channel = @bunny.create_channel
27
29
  bunny_channel.prefetch(prefetch) if prefetch
28
- Channel.new(bunny_channel)
30
+ Channel.new(bunny_channel, @recovery_count)
29
31
  end
30
32
 
31
33
  def close
@@ -37,14 +39,24 @@ class Freddy
37
39
 
38
40
  NO_ROUTE = 312
39
41
 
40
- def initialize(channel)
42
+ def initialize(channel, recovery_count)
41
43
  @channel = channel
44
+ @recovery_count = recovery_count
42
45
  end
43
46
 
44
- def_delegators :@channel, :topic, :default_exchange, :consumers, :acknowledge, :reject
47
+ def_delegators :@channel, :topic, :default_exchange, :consumers
45
48
 
46
49
  def queue(*)
47
- Queue.new(@channel.queue(*))
50
+ Queue.new(@channel.queue(*), @recovery_count)
51
+ end
52
+
53
+ # The server requeues deliveries from before a recovery; their stale tag would close this channel.
54
+ def acknowledge(delivery)
55
+ @channel.acknowledge(delivery.tag) if current?(delivery)
56
+ end
57
+
58
+ def reject(delivery, requeue: false)
59
+ @channel.reject(delivery.tag, requeue) if current?(delivery)
48
60
  end
49
61
 
50
62
  def on_no_route
@@ -52,11 +64,18 @@ class Freddy
52
64
  yield(properties[:correlation_id]) if return_info[:reply_code] == NO_ROUTE
53
65
  end
54
66
  end
67
+
68
+ private
69
+
70
+ def current?(delivery)
71
+ delivery.recovery_count == @recovery_count.value && @channel.open?
72
+ end
55
73
  end
56
74
 
57
75
  class Queue
58
- def initialize(queue)
76
+ def initialize(queue, recovery_count)
59
77
  @queue = queue
78
+ @recovery_count = recovery_count
60
79
  end
61
80
 
62
81
  def bind(*)
@@ -76,7 +95,8 @@ class Freddy
76
95
  @queue.subscribe(manual_ack: manual_ack) do |info, properties, payload|
77
96
  parsed_payload = Payload.parse(payload, properties[:content_encoding])
78
97
  delivery = Delivery.new(
79
- parsed_payload, properties, info.routing_key, info.delivery_tag, info.exchange
98
+ parsed_payload, properties, info.routing_key, info.delivery_tag, info.exchange,
99
+ @recovery_count.value
80
100
  )
81
101
  yield(delivery)
82
102
  end
@@ -42,7 +42,7 @@ class Freddy
42
42
  yield(delivery)
43
43
  end
44
44
  ensure
45
- @channel.acknowledge(delivery.tag, false)
45
+ @channel.acknowledge(delivery)
46
46
  end
47
47
  end
48
48
 
@@ -47,16 +47,16 @@ class Freddy
47
47
  @consume_thread_pool.post do
48
48
  delivery.in_span do
49
49
  yield delivery.payload, delivery.routing_key, delivery.timestamp
50
- @channel.acknowledge(delivery.tag)
50
+ @channel.acknowledge(delivery)
51
51
  end
52
52
  rescue StandardError
53
53
  case on_exception
54
54
  when :reject
55
- @channel.reject(delivery.tag)
55
+ @channel.reject(delivery)
56
56
  when :requeue
57
- @channel.reject(delivery.tag, true)
57
+ @channel.reject(delivery, requeue: true)
58
58
  else
59
- @channel.acknowledge(delivery.tag)
59
+ @channel.acknowledge(delivery)
60
60
  end
61
61
 
62
62
  raise
@@ -2,14 +2,15 @@
2
2
 
3
3
  class Freddy
4
4
  class Delivery
5
- attr_reader :routing_key, :payload, :tag
5
+ attr_reader :routing_key, :payload, :tag, :recovery_count
6
6
 
7
- def initialize(payload, metadata, routing_key, tag, exchange)
7
+ def initialize(payload, metadata, routing_key, tag, exchange, recovery_count) # rubocop:disable Metrics/ParameterLists
8
8
  @payload = payload
9
9
  @metadata = metadata
10
10
  @routing_key = routing_key
11
11
  @tag = tag
12
12
  @exchange = exchange
13
+ @recovery_count = recovery_count
13
14
  end
14
15
 
15
16
  def correlation_id
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Freddy
4
- VERSION = '2.11.2'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Freddy::Adapters::BunnyAdapter::Channel do
4
+ let(:channel) { described_class.new(bunny_channel, recovery_count) }
5
+ let(:bunny_channel) { double(open?: channel_open, acknowledge: nil, reject: nil) }
6
+ let(:channel_open) { true }
7
+ let(:recovery_count) { Concurrent::AtomicFixnum.new }
8
+ let(:delivery) { Freddy::Delivery.new({}, {}, 'routing.key', 7, '', recovery_count.value) }
9
+
10
+ context 'when the delivery is from the current connection' do
11
+ it 'acknowledges it' do
12
+ channel.acknowledge(delivery)
13
+
14
+ expect(bunny_channel).to have_received(:acknowledge).with(7)
15
+ end
16
+
17
+ it 'rejects it' do
18
+ channel.reject(delivery, requeue: true)
19
+
20
+ expect(bunny_channel).to have_received(:reject).with(7, true)
21
+ end
22
+ end
23
+
24
+ context 'when the connection was recovered after the delivery' do
25
+ before do
26
+ delivery
27
+ recovery_count.increment
28
+ end
29
+
30
+ it 'does not acknowledge it' do
31
+ channel.acknowledge(delivery)
32
+
33
+ expect(bunny_channel).not_to have_received(:acknowledge)
34
+ end
35
+
36
+ it 'does not reject it' do
37
+ channel.reject(delivery)
38
+
39
+ expect(bunny_channel).not_to have_received(:reject)
40
+ end
41
+ end
42
+
43
+ context 'when the channel is closed' do
44
+ let(:channel_open) { false }
45
+
46
+ it 'does not acknowledge the delivery' do
47
+ channel.acknowledge(delivery)
48
+
49
+ expect(bunny_channel).not_to have_received(:acknowledge)
50
+ end
51
+
52
+ it 'does not reject the delivery' do
53
+ channel.reject(delivery)
54
+
55
+ expect(bunny_channel).not_to have_received(:reject)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Connection recovery' do
4
+ let(:freddy) { Freddy.build(logger, **config, network_recovery_interval: 0.5) }
5
+ let(:requester) { Freddy.build(logger, **config) }
6
+ let(:destination) { random_destination }
7
+ let(:bunny) { freddy.instance_variable_get(:@connection).instance_variable_get(:@bunny) }
8
+
9
+ after { [freddy, requester].each(&:close) }
10
+
11
+ it 'keeps responding when a message is processed across a recovery' do
12
+ processing = Queue.new
13
+ release = Queue.new
14
+ deliveries = Concurrent::AtomicFixnum.new
15
+
16
+ freddy.respond_to destination do |payload, msg_handler|
17
+ if deliveries.increment == 1
18
+ processing << true
19
+ release.pop
20
+ end
21
+ msg_handler.success(payload)
22
+ end
23
+
24
+ first_request = Thread.new { requester.deliver_with_response(destination, { id: 1 }, timeout: 10) }
25
+ processing.pop
26
+
27
+ bunny.transport.socket.shutdown(Socket::SHUT_RDWR)
28
+ wait_for_recovery
29
+ release << true
30
+
31
+ expect(first_request.value).to eq(id: 1)
32
+ expect(requester.deliver_with_response(destination, { id: 2 })).to eq(id: 2)
33
+ expect(requester.deliver_with_response(destination, { id: 3 })).to eq(id: 3)
34
+ end
35
+
36
+ def wait_for_recovery
37
+ sleep 0.1 while bunny.open?
38
+ sleep 0.1 until bunny.open?
39
+ sleep 0.5
40
+ end
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: freddy
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.11.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Glia TechMovers
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '2.24'
18
+ version: '3.5'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '2.24'
25
+ version: '3.5'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: concurrent-ruby
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -153,6 +153,7 @@ files:
153
153
  - lib/freddy/tracing.rb
154
154
  - lib/freddy/version.rb
155
155
  - spec/.rubocop.yml
156
+ - spec/freddy/adapters/bunny_adapter/channel_spec.rb
156
157
  - spec/freddy/consumers/respond_to_consumer_spec.rb
157
158
  - spec/freddy/error_response_spec.rb
158
159
  - spec/freddy/freddy_spec.rb
@@ -161,6 +162,7 @@ files:
161
162
  - spec/freddy/responder_handler_spec.rb
162
163
  - spec/freddy/sync_response_container_spec.rb
163
164
  - spec/integration/concurrency_spec.rb
165
+ - spec/integration/connection_recovery_spec.rb
164
166
  - spec/integration/reply_spec.rb
165
167
  - spec/integration/tap_into_with_exchange_spec.rb
166
168
  - spec/integration/tap_into_with_group_spec.rb