ears 0.21.1 → 0.22.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: 3ffa13eeabb39addd2d5acb749c6edd32515770b357f7ef6cd3f944cc6aae182
4
- data.tar.gz: 4aa87462141bd49cbc616dfff68e7d5b5bf24274b7a6c0430ca02182e15259c5
3
+ metadata.gz: 8ae7219688923da6df4b3d6939e91449621d143fe835362043a7032aaaff3b40
4
+ data.tar.gz: 989c76721a48f2a91c8db1944746e598fdde624b47b9c598193de08a47df4798
5
5
  SHA512:
6
- metadata.gz: 45a3dffcf2f868f753fc985657bc24909c1bba97f1e633eb012ed4185c231d50edc98093f529b336f1e743dabe374a5ff6e662116d72f74ad5c166c0d81146ff
7
- data.tar.gz: 020e285268b69f96269a45e70abdb2287f48ff4346eac060a3dd6c4cebc8c27248f49c045a1c0a0139ea5c8bfc6aafb959634a6c62f945328c9fb1056d71e875
6
+ metadata.gz: 0cd3fac5b725c339d83d2953f7bffab29ed6ac0236d880af665e72308534e7154554dbcf42d74ec5c55db2a95a3fadd4b9283080ae87deb3900c388690b8df5d
7
+ data.tar.gz: 7ca86de1d5545ea3b701c0d097f7925d27fbaa7e8536f6acfc8a0c5a1de9d51dbeea456ee2ded64768566f171f0a9dc6f5351057083b497c3080b56274f18f98
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.1 (2025-09-11)
4
+
5
+ - Add optional `routing_key_match` parameter to `#published_messages` in `Ears::Testing::TestHelper`
6
+ - Add `#last_published_payload` to `Ears::Testing::TestHelper`
7
+
8
+ ## 0.22.0 (2025-09-11)
9
+
10
+ - Support publisher confirms in `Ears::Publisher`
11
+
3
12
  ## 0.21.1 (2025-09-09)
4
13
 
5
14
  - Add testing abstractions: `Ears::Testing::TestHelper`, `Ears::Testing::MessageCapture`, and `Ears::Testing::PublisherMock`
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ears (0.21.1)
4
+ ears (0.22.1)
5
5
  bunny (>= 2.22.0)
6
6
  connection_pool (~> 2.4)
7
7
  multi_json
@@ -113,7 +113,7 @@ CHECKSUMS
113
113
  connection_pool (2.5.3) sha256=cfd74a82b9b094d1ce30c4f1a346da23ee19dc8a062a16a85f58eab1ced4305b
114
114
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
115
115
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
116
- ears (0.21.1)
116
+ ears (0.22.1)
117
117
  json (2.13.2) sha256=02e1f118d434c6b230a64ffa5c8dee07e3ec96244335c392eaed39e1199dbb68
118
118
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
119
119
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
data/README.md CHANGED
@@ -197,6 +197,57 @@ rescue => e
197
197
  end
198
198
  ```
199
199
 
200
+ ### Publisher Confirms
201
+
202
+ #### Basic Usage
203
+
204
+ For guaranteed message delivery, use `publish_with_confirmation` which waits for broker acknowledgment:
205
+
206
+ ```ruby
207
+ publisher = Ears::Publisher.new('events', :topic)
208
+
209
+ # Publish with confirmation - blocks until broker acknowledges
210
+ publisher.publish_with_confirmation(
211
+ { user_id: 123, action: 'payment_processed' },
212
+ routing_key: 'payment.processed',
213
+ )
214
+ ```
215
+
216
+ #### Configuration
217
+
218
+ Publisher confirms use a separate channel pool with configurable settings:
219
+
220
+ ```ruby
221
+ Ears.configure do |config|
222
+ # Confirms-specific channel pool size (default: 32)
223
+ config.publisher_confirms_pool_size = 32
224
+
225
+ # Timeout for waiting for confirms in seconds (default: 5.0)
226
+ config.publisher_confirms_timeout = 5.0
227
+
228
+ # Cleanup timeout after confirmation failure (default: 1.0)
229
+ config.publisher_confirms_cleanup_timeout = 1.0
230
+ end
231
+ ```
232
+
233
+ #### Error Handling
234
+
235
+ Publisher confirms raise specific exceptions that are NOT automatically retried:
236
+
237
+ ```ruby
238
+ begin
239
+ publisher.publish_with_confirmation(data, routing_key: 'important.event')
240
+ rescue Ears::PublishConfirmationTimeout => e
241
+ # Message may or may not have reached broker
242
+ logger.error "Confirmation timed out: #{e.message}"
243
+ rescue Ears::PublishNacked => e
244
+ # Broker explicitly rejected the message
245
+ logger.error "Message was nacked: #{e.message}"
246
+ end
247
+ ```
248
+
249
+ **Note:** Unlike regular publishing, confirmation errors are not retried to avoid message duplication.
250
+
200
251
  ### Basic consumer usage
201
252
 
202
253
  First, you should configure `Ears`.
@@ -17,6 +17,9 @@ module Ears
17
17
  DEFAULT_PUBLISHER_MAX_RETRIES = 3
18
18
  DEFAULT_PUBLISHER_RETRY_BASE_DELAY = 0.1
19
19
  DEFAULT_PUBLISHER_RETRY_BACKOFF_FACTOR = 2
20
+ DEFAULT_PUBLISHER_CONFIRMS_POOL_SIZE = 32
21
+ DEFAULT_PUBLISHER_CONFIRMS_TIMEOUT = 5.0
22
+ DEFAULT_PUBLISHER_CONFIRMS_CLEANUP_TIMEOUT = 1.0
20
23
 
21
24
  # @return [String] the connection string for RabbitMQ.
22
25
  attr_accessor :rabbitmq_url
@@ -57,7 +60,16 @@ module Ears
57
60
  # @return [Logger] the logger instance for Ears operations
58
61
  attr_accessor :logger
59
62
 
60
- def initialize
63
+ # @return [Integer] the size of the publisher confirms channel pool
64
+ attr_accessor :publisher_confirms_pool_size
65
+
66
+ # @return [Float] the timeout in seconds for waiting for publisher confirms
67
+ attr_accessor :publisher_confirms_timeout
68
+
69
+ # @return [Float] the timeout in seconds for cleanup operations after confirmation timeout
70
+ attr_accessor :publisher_confirms_cleanup_timeout
71
+
72
+ def initialize # rubocop:disable Metrics/MethodLength
61
73
  @rabbitmq_url = DEFAULT_RABBITMQ_URL
62
74
  @recovery_attempts = DEFAULT_RECOVERY_ATTEMPTS
63
75
  @publisher_pool_size = DEFAULT_PUBLISHER_POOL_SIZE
@@ -69,6 +81,10 @@ module Ears
69
81
  @publisher_max_retries = DEFAULT_PUBLISHER_MAX_RETRIES
70
82
  @publisher_retry_base_delay = DEFAULT_PUBLISHER_RETRY_BASE_DELAY
71
83
  @publisher_retry_backoff_factor = DEFAULT_PUBLISHER_RETRY_BACKOFF_FACTOR
84
+ @publisher_confirms_pool_size = DEFAULT_PUBLISHER_CONFIRMS_POOL_SIZE
85
+ @publisher_confirms_timeout = DEFAULT_PUBLISHER_CONFIRMS_TIMEOUT
86
+ @publisher_confirms_cleanup_timeout =
87
+ DEFAULT_PUBLISHER_CONFIRMS_CLEANUP_TIMEOUT
72
88
  @logger = Logger.new(IO::NULL)
73
89
  end
74
90
 
data/lib/ears/errors.rb CHANGED
@@ -2,4 +2,16 @@ module Ears
2
2
  # Error that is raised when the Bunny recovery attempts are exhausted.
3
3
  class MaxRecoveryAttemptsExhaustedError < StandardError
4
4
  end
5
+
6
+ # Base error class for publisher-related errors.
7
+ class PublishError < StandardError
8
+ end
9
+
10
+ # Error raised when a publisher confirmation times out.
11
+ class PublishConfirmationTimeout < PublishError
12
+ end
13
+
14
+ # Error raised when a message is nacked by the broker.
15
+ class PublishNacked < PublishError
16
+ end
5
17
  end
@@ -1,6 +1,8 @@
1
1
  require 'bunny'
2
2
  require 'ears/publisher_channel_pool'
3
3
  require 'ears/publisher_retry_handler'
4
+ require 'ears/publisher_confirmation_handler'
5
+ require 'ears/errors'
4
6
 
5
7
  module Ears
6
8
  # Publisher for sending messages to RabbitMQ exchanges.
@@ -51,6 +53,50 @@ module Ears
51
53
  end
52
54
  end
53
55
 
56
+ # Publishes a message to the configured exchange with confirmation.
57
+ # Waits for RabbitMQ to confirm the message was received.
58
+ #
59
+ # @param [Hash, Array, Object] data The data to serialize as JSON and publish.
60
+ # @param [String] routing_key The routing key for the message.
61
+ #
62
+ # @option opts [String] :routing_key Routing key
63
+ # @option opts [Boolean] :persistent Should the message be persisted to disk?
64
+ # @option opts [Boolean] :mandatory Should the message be returned if it cannot be routed to any queue?
65
+ # @option opts [Integer] :timestamp A timestamp associated with this message
66
+ # @option opts [Integer] :expiration Expiration time after which the message will be deleted
67
+ # @option opts [String] :type Message type, e.g. what type of event or command this message represents. Can be any string
68
+ # @option opts [String] :reply_to Queue name other apps should send the response to
69
+ # @option opts [String] :content_type Message content type (e.g. application/json)
70
+ # @option opts [String] :content_encoding Message content encoding (e.g. gzip)
71
+ # @option opts [String] :correlation_id Message correlated to this one, e.g. what request this message is a reply for
72
+ # @option opts [Integer] :priority Message priority, 0 to 9. Not used by RabbitMQ, only applications
73
+ # @option opts [String] :message_id Any message identifier
74
+ # @option opts [String] :user_id Optional user ID. Verified by RabbitMQ against the actual connection username
75
+ # @option opts [String] :app_id Optional application ID
76
+ #
77
+ # @raise [PublishConfirmationTimeout] if confirmation times out
78
+ # @raise [PublishNacked] if message is nacked by the broker
79
+ # @return [void]
80
+ def publish_with_confirmation(data, routing_key:, **options)
81
+ publish_options = default_publish_options.merge(options)
82
+
83
+ retry_handler.run do
84
+ validate_connection!
85
+
86
+ PublisherChannelPool.with_channel(confirms: true) do |channel|
87
+ exchange = create_exchange(channel)
88
+
89
+ publisher_confirmation_handler.publish_with_confirmation(
90
+ channel: channel,
91
+ exchange: exchange,
92
+ data: data,
93
+ routing_key: routing_key,
94
+ options: publish_options,
95
+ )
96
+ end
97
+ end
98
+ end
99
+
54
100
  # Resets the channel pool, forcing new channels to be created.
55
101
  # This can be useful for connection recovery scenarios.
56
102
  #
@@ -68,12 +114,9 @@ module Ears
68
114
  :logger
69
115
 
70
116
  def publish_with_channel(data:, routing_key:, publish_options:)
71
- unless Ears.connection.open?
72
- raise PublisherRetryHandler::PublishToStaleChannelError,
73
- 'Connection is not open'
74
- end
117
+ validate_connection!
75
118
 
76
- PublisherChannelPool.with_channel do |channel|
119
+ PublisherChannelPool.with_channel(confirms: false) do |channel|
77
120
  exchange = create_exchange(channel)
78
121
  exchange.publish(
79
122
  data,
@@ -82,6 +125,13 @@ module Ears
82
125
  end
83
126
  end
84
127
 
128
+ def validate_connection!
129
+ unless Ears.connection.open?
130
+ raise PublisherRetryHandler::PublishToStaleChannelError,
131
+ 'Connection is not open'
132
+ end
133
+ end
134
+
85
135
  def create_exchange(channel)
86
136
  Bunny::Exchange.new(
87
137
  channel,
@@ -104,5 +154,10 @@ module Ears
104
154
  def retry_handler
105
155
  @retry_handler ||= PublisherRetryHandler.new(config, logger)
106
156
  end
157
+
158
+ def publisher_confirmation_handler
159
+ @publisher_confirmation_handler ||=
160
+ PublisherConfirmationHandler.new(config: config, logger: logger)
161
+ end
107
162
  end
108
163
  end
@@ -3,50 +3,97 @@ require 'connection_pool'
3
3
  module Ears
4
4
  # Channel pool management for publishers.
5
5
  # Provides thread-safe channel pooling separate from consumer channels.
6
+ # Maintains two separate pools: one for standard publishing and one for confirmed publishing.
6
7
  class PublisherChannelPool
7
8
  class << self
8
- # Executes the given block with a channel from the pool.
9
+ # Executes the given block with a channel from the appropriate pool.
9
10
  #
11
+ # @param [Boolean] confirms Whether to use a channel with publisher confirms enabled
10
12
  # @yieldparam [Bunny::Channel] channel The channel to use for publishing
11
13
  # @return [Object] The result of the block
12
- def with_channel(&)
13
- channel_pool.with(&)
14
+ def with_channel(confirms: false, &)
15
+ # CRITICAL: Preserve fork-safety at the entry point
16
+ reset! if @creator_pid && @creator_pid != Process.pid
17
+
18
+ pool = confirms ? confirms_pool : standard_pool
19
+ pool.with(&)
14
20
  end
15
21
 
16
- # Resets the channel pool, forcing new channels to be created.
22
+ # Resets both channel pools, forcing new channels to be created.
17
23
  # This is useful for connection recovery scenarios.
18
24
  #
19
25
  # @return [void]
20
26
  def reset!
21
- pool = @channel_pool
22
- @channel_pool = nil
27
+ std_pool = @standard_pool
28
+ cnf_pool = @confirms_pool
29
+ @standard_pool = nil
30
+ @confirms_pool = nil
23
31
  @creator_pid = nil
24
32
 
25
- pool&.shutdown(&:close)
33
+ std_pool&.shutdown(&:close)
34
+ cnf_pool&.shutdown(&:close)
26
35
  nil
27
36
  end
28
37
 
29
- private
38
+ # Resets only the confirms channel pool, forcing new confirmed channels to be created.
39
+ # This is useful when confirmation failures occur and channels may have contaminated state.
40
+ #
41
+ # @return [void]
42
+ def reset_confirms_pool!
43
+ cnf_pool = @confirms_pool
44
+ @confirms_pool = nil
30
45
 
31
- def channel_pool
32
- # Recreate lazily after a fork
33
- reset! if @creator_pid && @creator_pid != Process.pid
46
+ cnf_pool&.shutdown(&:close)
47
+ nil
48
+ end
34
49
 
35
- return @channel_pool if @channel_pool
50
+ private
51
+
52
+ def standard_pool
53
+ # CRITICAL: Lazy-init must be thread-safe
54
+ return @standard_pool if @standard_pool
36
55
 
37
56
  init_mutex.synchronize do
38
- @channel_pool ||=
57
+ # Double-check in case another thread was waiting
58
+ @standard_pool ||=
39
59
  begin
40
- @creator_pid = Process.pid
60
+ @creator_pid ||= Process.pid
61
+ create_pool(confirms: false)
62
+ end
63
+ end
64
+ end
65
+
66
+ def confirms_pool
67
+ # CRITICAL: Lazy-init must be thread-safe
68
+ return @confirms_pool if @confirms_pool
41
69
 
42
- ConnectionPool.new(
43
- size: Ears.configuration.publisher_pool_size,
44
- timeout: Ears.configuration.publisher_pool_timeout,
45
- ) { Ears.connection.create_channel }
70
+ init_mutex.synchronize do
71
+ # Double-check in case another thread was waiting
72
+ @confirms_pool ||=
73
+ begin
74
+ @creator_pid ||= Process.pid
75
+ create_pool(confirms: true)
46
76
  end
47
77
  end
48
78
  end
49
79
 
80
+ def create_pool(confirms:)
81
+ ConnectionPool.new(
82
+ size: pool_size_for(confirms),
83
+ timeout: Ears.configuration.publisher_pool_timeout,
84
+ ) do
85
+ channel = Ears.connection.create_channel
86
+ channel.confirm_select if confirms
87
+ channel
88
+ end
89
+ end
90
+
91
+ def pool_size_for(confirms)
92
+ return Ears.configuration.publisher_confirms_pool_size if confirms
93
+
94
+ Ears.configuration.publisher_pool_size
95
+ end
96
+
50
97
  def init_mutex
51
98
  @init_mutex ||= Mutex.new
52
99
  end
@@ -0,0 +1,91 @@
1
+ require 'ears/errors'
2
+
3
+ module Ears
4
+ # Handles publisher confirmations for RabbitMQ messages.
5
+ #
6
+ # This class encapsulates the logic for publishing messages with confirmations,
7
+ # including timeout handling, channel cleanup, and pool coordination.
8
+ class PublisherConfirmationHandler
9
+ # Creates a new confirmation handler.
10
+ #
11
+ # @param [Ears::Configuration] config The Ears configuration object.
12
+ # @param [Logger] logger The logger instance for warnings and errors.
13
+ def initialize(config:, logger:)
14
+ @config = config
15
+ @logger = logger
16
+ end
17
+
18
+ # Publishes a message with confirmation.
19
+ #
20
+ # @param [Bunny::Channel] channel The channel to use for publishing.
21
+ # @param [Bunny::Exchange] exchange The exchange to publish to.
22
+ # @param [Hash, Array, Object] data The data to publish.
23
+ # @param [String] routing_key The routing key for the message.
24
+ # @param [Hash] options The publish options.
25
+ #
26
+ # @raise [PublishConfirmationTimeout] if confirmation times out
27
+ # @raise [PublishNacked] if message is nacked by the broker
28
+ # @return [void]
29
+ def publish_with_confirmation(
30
+ channel:,
31
+ exchange:,
32
+ data:,
33
+ routing_key:,
34
+ options:
35
+ )
36
+ exchange.publish(data, { routing_key: routing_key }.merge(options))
37
+
38
+ timeout = config.publisher_confirms_timeout
39
+ unless wait_for_confirms_with_timeout(channel, timeout)
40
+ handle_confirmation_failure(channel, timeout)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :config, :logger
47
+
48
+ def wait_for_confirms_with_timeout(channel, timeout)
49
+ return channel.wait_for_confirms if timeout.nil?
50
+
51
+ waiter = Thread.new { channel.wait_for_confirms }
52
+
53
+ return waiter.value if waiter.join(timeout)
54
+
55
+ begin
56
+ channel.close if channel.open?
57
+ rescue StandardError => e
58
+ warn("Failed closing channel on timeout: #{e.message}")
59
+ end
60
+
61
+ cleanup_timeout = config.publisher_confirms_cleanup_timeout
62
+ waiter.join(cleanup_timeout) ||
63
+ warn('Confirm waiter did not stop promptly after close')
64
+
65
+ false
66
+ end
67
+
68
+ def handle_confirmation_failure(channel, timeout)
69
+ begin
70
+ channel.close if channel&.open?
71
+ rescue StandardError => e
72
+ warn("Failed closing channel on failed confirmation: #{e.message}")
73
+ end
74
+
75
+ PublisherChannelPool.reset_confirms_pool!
76
+
77
+ if channel.nacked_set&.any?
78
+ warn('Publisher confirmation failed: message was nacked by broker.')
79
+ raise PublishNacked, 'Message was nacked by broker'
80
+ else
81
+ warn("Publisher confirmation failed: timeout after #{timeout}s.")
82
+ raise PublishConfirmationTimeout,
83
+ "Confirmation timeout after #{timeout}s"
84
+ end
85
+ end
86
+
87
+ def warn(message)
88
+ logger.warn(message)
89
+ end
90
+ end
91
+ end
@@ -19,6 +19,10 @@ module Ears
19
19
  Timeout::Error,
20
20
  ].freeze
21
21
 
22
+ # Confirmation errors should NOT be retried - they indicate the message
23
+ # was published but confirmation failed, so retrying could duplicate the message
24
+ NON_RETRYABLE_ERRORS = [PublishConfirmationTimeout, PublishNacked].freeze
25
+
22
26
  def initialize(config, logger)
23
27
  @config = config
24
28
  @logger = logger
@@ -28,6 +32,8 @@ module Ears
28
32
  attempt = 1
29
33
  begin
30
34
  block.call
35
+ rescue *NON_RETRYABLE_ERRORS => e
36
+ raise e
31
37
  rescue *CONNECTION_ERRORS => e
32
38
  attempt = handle_connection_error(e, attempt) # rubocop:disable Lint/UselessAssignment
33
39
  retry
@@ -41,6 +41,7 @@ module Ears
41
41
  setup_exchange_declare(channel)
42
42
  setup_register_exchange(channel)
43
43
  setup_basic_publish(channel)
44
+ setup_publisher_confirms(channel)
44
45
  end
45
46
  end
46
47
 
@@ -66,6 +67,14 @@ module Ears
66
67
  end
67
68
  end
68
69
 
70
+ def setup_publisher_confirms(channel)
71
+ allow(channel).to receive(:confirm_select)
72
+ allow(channel).to receive(:wait_for_confirms).and_return(true)
73
+ allow(channel).to receive(:nacked_set).and_return(Set.new)
74
+ allow(channel).to receive(:open?).and_return(true)
75
+ allow(channel).to receive(:close)
76
+ end
77
+
69
78
  def create_or_get_mock_exchange(name, type, _options)
70
79
  mock_exchanges[name] ||= create_mock_exchange(name, type)
71
80
  end
@@ -28,23 +28,38 @@ module Ears
28
28
  end
29
29
  end
30
30
 
31
- def published_messages(exchange_name = nil)
31
+ def published_messages(exchange_name = nil, routing_key_match: nil)
32
32
  return [] unless Ears::Testing.message_capture
33
33
 
34
- if exchange_name
35
- return Ears::Testing.message_capture.messages_for(exchange_name)
36
- end
34
+ messages = exchange_name ? messages_for(exchange_name) : all_messages
35
+ return messages unless routing_key_match
37
36
 
38
- Ears::Testing.message_capture.all_messages
37
+ messages.select do |message|
38
+ message.routing_key.match?(routing_key_match)
39
+ end
39
40
  end
40
41
 
41
42
  def last_published_message(exchange_name = nil)
42
43
  published_messages(exchange_name).last
43
44
  end
44
45
 
46
+ def last_published_payload(exchange_name = nil)
47
+ last_published_message(exchange_name).data
48
+ end
49
+
45
50
  def clear_published_messages
46
51
  Ears::Testing.message_capture&.clear
47
52
  end
53
+
54
+ private
55
+
56
+ def messages_for(exchange_name)
57
+ Ears::Testing.message_capture.messages_for(exchange_name)
58
+ end
59
+
60
+ def all_messages
61
+ Ears::Testing.message_capture.all_messages
62
+ end
48
63
  end
49
64
  end
50
65
  end
data/lib/ears/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ears
2
- VERSION = '0.21.1'
2
+ VERSION = '0.22.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ears
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.1
4
+ version: 0.22.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - InVision AG
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-09 00:00:00.000000000 Z
11
+ date: 2025-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -89,6 +89,7 @@ files:
89
89
  - lib/ears/middlewares/max_retries.rb
90
90
  - lib/ears/publisher.rb
91
91
  - lib/ears/publisher_channel_pool.rb
92
+ - lib/ears/publisher_confirmation_handler.rb
92
93
  - lib/ears/publisher_retry_handler.rb
93
94
  - lib/ears/setup.rb
94
95
  - lib/ears/testing.rb