ears 0.19.0 → 0.21.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: ffe84e09754e5329b3e82b1add44fb3222b2ac2e36c1955ad54d6ea90505df8d
4
- data.tar.gz: 2545f546c91cdc7ba3edd73e3035bcdc3253433d7dcf9d21ab7a6b1143c8722f
3
+ metadata.gz: a7786b24328603a3d9b231e06400194e69ab38b00de05ec0b0ee5424f0ec3a7b
4
+ data.tar.gz: 386cedcd1ed08364678da108e1c44e4142d5ba915478476b2d95b14afec3eff3
5
5
  SHA512:
6
- metadata.gz: 24ed423d5b49a22bbab254ebd8e46322391f9d427fa0455dc039dd50fa7eb677222cef93561f11bde207fa9832c756d4050397a0dcfbb4afcb5e103fd1c82262
7
- data.tar.gz: ffc77c61d3822b19e3e135a96983fcba0a0d61ff50b08c7a57a6e03bd5051498ac4481777e8fcccc90fc1152f4d0dc69248089e82f9ce15945be25aefd07acb9
6
+ metadata.gz: 841d1b043e68ebbcf850fab367e770e145cf3519d15e33f613db18c41e8df72d2c33d0337b50ba3c18d866894defe32d4584230d78447192eb2ac71b6dea134f
7
+ data.tar.gz: 7a2bb015f0f4175499c07609cc0ab923af8ecd2157df7a6a57c5c1233c68cfc2d5813ac2452c571551efb83658bfbb5642cf4373bb0efde0a6e9cd88a0530b31
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- ruby-version: ['3.1', '3.2', '3.3', '3.4']
15
+ ruby-version: ['3.2', '3.3', '3.4']
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v4
@@ -23,7 +23,7 @@ jobs:
23
23
  bundler-cache: true
24
24
  - uses: actions/setup-node@v4
25
25
  with:
26
- node-version: '23'
26
+ node-version: '24'
27
27
  - name: Run prettier
28
28
  run: npm ci && npm run lint
29
29
  - name: Run Rubocop
data/.rubocop.yml CHANGED
@@ -13,7 +13,7 @@ AllCops:
13
13
  - Guardfile
14
14
  - bin/*
15
15
  - tmp/**/*
16
- TargetRubyVersion: 3.0
16
+ TargetRubyVersion: 3.2
17
17
  NewCops: enable
18
18
 
19
19
  Style:
@@ -66,7 +66,7 @@ Metrics/ModuleLength:
66
66
  - 'spec/**/*.rb'
67
67
  - 'test/**/*.rb'
68
68
 
69
- require:
69
+ plugins:
70
70
  - rubocop-rspec
71
71
  - rubocop-rake
72
72
 
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-03-10 15:51:28 UTC using RuboCop version 1.72.2.
3
+ # on 2025-06-02 12:38:15 UTC using RuboCop version 1.75.8.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
data/.tool-versions CHANGED
@@ -1,2 +1,2 @@
1
- ruby 3.1.6
2
- nodejs 23.8.0
1
+ ruby 3.2.9
2
+ nodejs 24.4.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0 (2025-09-08)
4
+
5
+ - Introduce Ears::Publisher with thread-safe channel pooling
6
+ - Introduce configurable logger
7
+
8
+ ## 0.20.0 (2025-06-02)
9
+
10
+ - Drop support for Ruby 3.1
11
+
3
12
  ## 0.19.0 (2025-03-10)
4
13
 
5
14
  - Enhance Consumer#configure method and Ears.setup_consumers to accept `prefetch` option, which will be passed to Ears::Setup.
data/Gemfile.lock CHANGED
@@ -1,48 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ears (0.19.0)
4
+ ears (0.21.0)
5
5
  bunny (>= 2.22.0)
6
+ connection_pool (~> 2.4)
6
7
  multi_json
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
10
11
  specs:
11
- amq-protocol (2.3.3)
12
- ast (2.4.2)
13
- bunny (2.23.0)
14
- amq-protocol (~> 2.3, >= 2.3.1)
12
+ amq-protocol (2.3.4)
13
+ ast (2.4.3)
14
+ bunny (2.24.0)
15
+ amq-protocol (~> 2.3)
15
16
  sorted_set (~> 1, >= 1.0.2)
16
- diff-lcs (1.6.0)
17
+ connection_pool (2.5.3)
18
+ diff-lcs (1.6.2)
17
19
  docile (1.4.1)
18
- json (2.10.1)
19
- language_server-protocol (3.17.0.4)
20
+ json (2.13.2)
21
+ language_server-protocol (3.17.0.5)
20
22
  lint_roller (1.1.0)
21
- multi_json (1.15.0)
22
- parallel (1.26.3)
23
- parser (3.3.7.1)
23
+ multi_json (1.17.0)
24
+ parallel (1.27.0)
25
+ parser (3.3.9.0)
24
26
  ast (~> 2.4.1)
25
27
  racc
26
28
  prettier_print (1.2.1)
29
+ prism (1.4.0)
27
30
  racc (1.8.1)
28
31
  rainbow (3.1.1)
29
- rake (13.2.1)
32
+ rake (13.3.0)
30
33
  rbtree (0.4.6)
31
34
  regexp_parser (2.10.0)
32
- rspec (3.13.0)
35
+ rspec (3.13.1)
33
36
  rspec-core (~> 3.13.0)
34
37
  rspec-expectations (~> 3.13.0)
35
38
  rspec-mocks (~> 3.13.0)
36
- rspec-core (3.13.3)
39
+ rspec-core (3.13.5)
37
40
  rspec-support (~> 3.13.0)
38
- rspec-expectations (3.13.3)
41
+ rspec-expectations (3.13.5)
39
42
  diff-lcs (>= 1.2.0, < 2.0)
40
43
  rspec-support (~> 3.13.0)
41
- rspec-mocks (3.13.2)
44
+ rspec-mocks (3.13.5)
42
45
  diff-lcs (>= 1.2.0, < 2.0)
43
46
  rspec-support (~> 3.13.0)
44
- rspec-support (3.13.2)
45
- rubocop (1.72.2)
47
+ rspec-support (3.13.4)
48
+ rubocop (1.79.0)
46
49
  json (~> 2.3)
47
50
  language_server-protocol (~> 3.17.0.2)
48
51
  lint_roller (~> 1.1.0)
@@ -50,30 +53,33 @@ GEM
50
53
  parser (>= 3.3.0.2)
51
54
  rainbow (>= 2.2.2, < 4.0)
52
55
  regexp_parser (>= 2.9.3, < 3.0)
53
- rubocop-ast (>= 1.38.0, < 2.0)
56
+ rubocop-ast (>= 1.46.0, < 2.0)
54
57
  ruby-progressbar (~> 1.7)
58
+ tsort (>= 0.2.0)
55
59
  unicode-display_width (>= 2.4.0, < 4.0)
56
- rubocop-ast (1.38.0)
57
- parser (>= 3.3.1.0)
60
+ rubocop-ast (1.46.0)
61
+ parser (>= 3.3.7.2)
62
+ prism (~> 1.4)
58
63
  rubocop-rake (0.7.1)
59
64
  lint_roller (~> 1.1)
60
65
  rubocop (>= 1.72.1)
61
- rubocop-rspec (3.5.0)
66
+ rubocop-rspec (3.6.0)
62
67
  lint_roller (~> 1.1)
63
68
  rubocop (~> 1.72, >= 1.72.1)
64
69
  ruby-progressbar (1.13.0)
65
- set (1.1.1)
70
+ set (1.1.2)
66
71
  simplecov (0.22.0)
67
72
  docile (~> 1.1)
68
73
  simplecov-html (~> 0.11)
69
74
  simplecov_json_formatter (~> 0.1)
70
- simplecov-html (0.13.1)
75
+ simplecov-html (0.13.2)
71
76
  simplecov_json_formatter (0.1.4)
72
77
  sorted_set (1.0.3)
73
78
  rbtree
74
79
  set (~> 1.0)
75
- syntax_tree (6.2.0)
80
+ syntax_tree (6.3.0)
76
81
  prettier_print (>= 1.2.0)
82
+ tsort (0.2.0)
77
83
  unicode-display_width (3.1.4)
78
84
  unicode-emoji (~> 4.0, >= 4.0.4)
79
85
  unicode-emoji (4.0.4)
@@ -101,43 +107,46 @@ DEPENDENCIES
101
107
  yard
102
108
 
103
109
  CHECKSUMS
104
- amq-protocol (2.3.3) sha256=85b42738290913a35dcc487a2ca0dd260a4150b40ed1954c9c1932df466abc1f
105
- ast (2.4.2) sha256=1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12
106
- bunny (2.23.0) sha256=b721d18b077d111ea08eeb1934a4d5f3005ca571853c05474f31d96f7b517ada
107
- diff-lcs (1.6.0) sha256=a1e7f7b272962f8fc769358ad00001b87cdcf32ba349d6c70c6b544613d2da2e
110
+ amq-protocol (2.3.4) sha256=98be5b9244e28dc66acc8351a254dbf45d996c5a0b7d49ab3ff8b72b0d2e6308
111
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
112
+ bunny (2.24.0) sha256=072fe4ae98eaa9c95a17e4d166204f710bba8a9a7070b73a8c3b023f439d1682
113
+ connection_pool (2.5.3) sha256=cfd74a82b9b094d1ce30c4f1a346da23ee19dc8a062a16a85f58eab1ced4305b
114
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
108
115
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
109
- ears (0.19.0)
110
- json (2.10.1) sha256=ddc88ad91a1baf3f0038c174f253af3b086d30dc74db17ca4259bbde982f94dc
111
- language_server-protocol (3.17.0.4) sha256=c484626478664fd13482d8180947c50a8590484b1258b99b7aedb3b69df89669
116
+ ears (0.21.0)
117
+ json (2.13.2) sha256=02e1f118d434c6b230a64ffa5c8dee07e3ec96244335c392eaed39e1199dbb68
118
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
112
119
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
113
- multi_json (1.15.0) sha256=1fd04138b6e4a90017e8d1b804c039031399866ff3fbabb7822aea367c78615d
114
- parallel (1.26.3) sha256=d86babb7a2b814be9f4b81587bf0b6ce2da7d45969fab24d8ae4bf2bb4d4c7ef
115
- parser (3.3.7.1) sha256=7dbe61618025519024ac72402a6677ead02099587a5538e84371b76659e6aca1
120
+ multi_json (1.17.0) sha256=76581f6c96aebf2e85f8a8b9854829e0988f335e8671cd1a56a1036eb75e4a1b
121
+ parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
122
+ parser (3.3.9.0) sha256=94d6929354b1a6e3e1f89d79d4d302cc8f5aa814431a6c9c7e0623335d7687f2
116
123
  prettier_print (1.2.1) sha256=a72838b5f23facff21f90a5423cdcdda19e4271092b41f4ea7f50b83929e6ff9
124
+ prism (1.4.0) sha256=dc0e3e00e93160213dc2a65519d9002a4a1e7b962db57d444cf1a71565bb703e
117
125
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
118
126
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
119
- rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d
127
+ rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493
120
128
  rbtree (0.4.6) sha256=14eea4469b24fd2472542e5f3eb105d6344c8ccf36f0b56d55fdcfeb4e0f10fc
121
129
  regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61
122
- rspec (3.13.0) sha256=d490914ac1d5a5a64a0e1400c1d54ddd2a501324d703b8cfe83f458337bab993
123
- rspec-core (3.13.3) sha256=25136507f4f9cf2e8977a2851e64e438b4331646054e345998714108745cdfe4
124
- rspec-expectations (3.13.3) sha256=0e6b5af59b900147698ea0ff80456c4f2e69cac4394fbd392fbd1ca561f66c58
125
- rspec-mocks (3.13.2) sha256=2327335def0e1665325a9b617e3af9ae20272741d80ac550336309a7c59abdef
126
- rspec-support (3.13.2) sha256=cea3a2463fd9b84b9dcc9685efd80ea701aa8f7b3decb3b3ce795ed67737dbec
127
- rubocop (1.72.2) sha256=0259a32d89fee60882bf4c4d8847e696357719c9db4971839da742bf053ae96b
128
- rubocop-ast (1.38.0) sha256=4fdf6792fe443a9a18acb12dbc8225d0d64cd1654e41fedb30e79c18edbb26ae
130
+ rspec (3.13.1) sha256=b9f9a58fa915b8d94a1d6b3195fe6dd28c4c34836a6097015142c4a9ace72140
131
+ rspec-core (3.13.5) sha256=ab3f682897c6131c67f9a17cfee5022a597f283aebe654d329a565f9937a4fa3
132
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
133
+ rspec-mocks (3.13.5) sha256=e4338a6f285ada9fe56f5893f5457783af8194f5d08884d17a87321d5195ea81
134
+ rspec-support (3.13.4) sha256=184b1814f6a968102b57df631892c7f1990a91c9a3b9e80ef892a0fc2a71a3f7
135
+ rubocop (1.79.0) sha256=c709e83b16f9fced295d83d190a3a5bbcc46c419d8f9b85f259b99ba6faf5bbe
136
+ rubocop-ast (1.46.0) sha256=0da7f6ad5b98614f89b74f11873c191059c823eae07d6ffd40a42a3338f2232b
129
137
  rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
130
- rubocop-rspec (3.5.0) sha256=710c942fe1af884ba8eea75cbb8bdbb051929a2208880a6fc2e2dce1eed5304c
138
+ rubocop-rspec (3.6.0) sha256=c0e4205871776727e54dee9cc91af5fd74578001551ba40e1fe1a1ab4b404479
131
139
  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
132
- set (1.1.1) sha256=6c7ac6c06d5907216395a4d5dae3ffe52ca5ee8a372befe6d4dea794383f98f0
140
+ set (1.1.2) sha256=ca33a60d202e788041d94a5d4c12315b1639875576f1a266f3a10913646d8ef1
133
141
  simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
134
- simplecov-html (0.13.1) sha256=5dab0b7ee612e60e9887ad57693832fdf4695b4c0c859eaea5f95c18791ef10b
142
+ simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
135
143
  simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
136
144
  sorted_set (1.0.3) sha256=4f2b8bee6e8c59cbd296228c0f1f81679357177a8b6859dcc2a99e86cce6372f
137
- syntax_tree (6.2.0) sha256=a50a01c246601af3c258edbb6b12e44373d17966ab3bebd1f7224b3b994a343d
145
+ syntax_tree (6.3.0) sha256=56e25a9692c798ec94c5442fe94c5e94af76bef91edc8bb02052cbdecf35f13d
146
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
138
147
  unicode-display_width (3.1.4) sha256=8caf2af1c0f2f07ec89ef9e18c7d88c2790e217c482bfc78aaa65eadd5415ac1
139
148
  unicode-emoji (4.0.4) sha256=2c2c4ef7f353e5809497126285a50b23056cc6e61b64433764a35eff6c36532a
140
149
  yard (0.9.37) sha256=a6e910399e78e613f80ba9add9ba7c394b1a935f083cccbef82903a3d2a26992
141
150
 
142
151
  BUNDLED WITH
143
- 2.6.5
152
+ 2.7.1
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ears
2
2
 
3
- `Ears` is a small, simple library for writing RabbitMQ consumers.
3
+ `Ears` is a small, simple library for writing RabbitMQ consumers and publishers.
4
4
 
5
5
  [![CodeQL](https://github.com/ivx/ears/actions/workflows/codeql.yml/badge.svg)](https://github.com/ivx/ears/actions/workflows/codeql.yml)
6
6
 
@@ -22,7 +22,182 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- ### Basic usage
25
+ ### Publishing Messages
26
+
27
+ `Ears` provides a thread-safe publisher for sending messages to RabbitMQ exchanges with automatic retry and connection recovery capabilities.
28
+
29
+ #### Basic Publisher Usage
30
+
31
+ To publish messages, create an `Ears::Publisher` instance and call `publish`:
32
+
33
+ ```ruby
34
+ require 'ears'
35
+
36
+ # Configure Ears (same configuration is shared by consumers and publishers)
37
+ Ears.configure do |config|
38
+ config.rabbitmq_url = 'amqp://user:password@myrmq:5672'
39
+ config.connection_name = 'My Publisher'
40
+ end
41
+
42
+ # Create a publisher for a topic exchange
43
+ publisher = Ears::Publisher.new('my_exchange', :topic, durable: true)
44
+
45
+ # Publish a message
46
+ data = { user_id: 123, action: 'login', timestamp: Time.now.iso8601 }
47
+ publisher.publish(data, routing_key: 'user.login')
48
+ ```
49
+
50
+ #### Exchange Types and Options
51
+
52
+ Publishers support all RabbitMQ exchange types:
53
+
54
+ ```ruby
55
+ # Topic exchange (default)
56
+ topic_publisher = Ears::Publisher.new('events', :topic)
57
+
58
+ # Direct exchange
59
+ direct_publisher = Ears::Publisher.new('commands', :direct)
60
+
61
+ # Fanout exchange
62
+ fanout_publisher = Ears::Publisher.new('broadcasts', :fanout)
63
+
64
+ # Headers exchange
65
+ headers_publisher = Ears::Publisher.new('complex_routing', :headers)
66
+
67
+ # Custom exchange options
68
+ publisher =
69
+ Ears::Publisher.new(
70
+ 'my_exchange',
71
+ :topic,
72
+ durable: true,
73
+ auto_delete: false,
74
+ arguments: {
75
+ 'x-message-ttl' => 60_000,
76
+ },
77
+ )
78
+ ```
79
+
80
+ #### Message Options
81
+
82
+ The `publish` method accepts various message options:
83
+
84
+ ```ruby
85
+ publisher.publish(
86
+ { message: 'Hello World' },
87
+ routing_key: 'greeting.hello',
88
+ persistent: true, # Persist message to disk (default: true)
89
+ headers: {
90
+ version: '1.0',
91
+ }, # Custom headers
92
+ timestamp: Time.now.to_i, # Message timestamp (default: current time)
93
+ message_id: SecureRandom.uuid, # Unique message identifier
94
+ correlation_id: 'abc-123', # Correlation ID for request/response patterns
95
+ reply_to: 'response_queue', # Queue for responses
96
+ expiration: '60000', # Message TTL in milliseconds
97
+ priority: 5, # Message priority (0-9)
98
+ type: 'user_event', # Message type
99
+ app_id: 'my_application', # Application identifier
100
+ user_id: 'system', # User identifier (verified by RabbitMQ)
101
+ )
102
+ ```
103
+
104
+ #### Thread-Safe Publishing
105
+
106
+ Publishers use a connection pool for thread-safe operation, making them suitable for concurrent use:
107
+
108
+ ```ruby
109
+ # Single publisher can be safely used across multiple threads
110
+ publisher = Ears::Publisher.new('events', :topic)
111
+
112
+ # Example with multiple threads
113
+ threads =
114
+ 10.times.map do |i|
115
+ Thread.new do
116
+ 100.times do |j|
117
+ publisher.publish({ thread: i, message: j }, routing_key: "thread.#{i}")
118
+ end
119
+ end
120
+ end
121
+
122
+ threads.each(&:join)
123
+ ```
124
+
125
+ #### Publisher Configuration
126
+
127
+ Publisher behavior can be fine-tuned through configuration options:
128
+
129
+ ```ruby
130
+ Ears.configure do |config|
131
+ # Connection settings
132
+ config.rabbitmq_url = 'amqp://user:password@myrmq:5672'
133
+ config.connection_name = 'My Application'
134
+
135
+ # Publisher-specific settings
136
+ config.publisher_pool_size = 32 # Channel pool size (default: 32)
137
+ config.publisher_pool_timeout = 2 # Pool checkout timeout in seconds (default: 2)
138
+
139
+ # Connection retry settings
140
+ config.publisher_connection_attempts = 30 # Connection retry attempts (default: 30)
141
+ config.publisher_connection_base_delay = 1 # Base delay between connection attempts (default: 1s)
142
+ config.publisher_connection_backoff_factor = 1.5 # Connection backoff multiplier (default: 1.5)
143
+
144
+ # Publish retry settings
145
+ config.publisher_max_retries = 3 # Max publish retry attempts (default: 3)
146
+ config.publisher_retry_base_delay = 0.1 # Base delay between publish retries (default: 0.1s)
147
+ config.publisher_retry_backoff_factor = 2 # Publish retry backoff multiplier (default: 2)
148
+ end
149
+ ```
150
+
151
+ #### Fault Tolerance and Recovery
152
+
153
+ Publishers automatically handle connection failures and provide several recovery mechanisms:
154
+
155
+ ##### Automatic Retry with Exponential Backoff
156
+
157
+ ```ruby
158
+ # Publishers automatically retry failed operations
159
+ publisher = Ears::Publisher.new('events', :topic)
160
+
161
+ # This will automatically retry with exponential backoff if the connection fails
162
+ publisher.publish({ event: 'user_signup' }, routing_key: 'user.signup')
163
+ ```
164
+
165
+ ##### Manual Recovery
166
+
167
+ If you need to manually reset the connection pool (e.g., after detecting connection issues):
168
+
169
+ ```ruby
170
+ publisher = Ears::Publisher.new('events', :topic)
171
+
172
+ # Reset the channel pool to force new connections
173
+ publisher.reset!
174
+
175
+ # Subsequent publishes will use fresh channels
176
+ publisher.publish({ event: 'recovery_test' }, routing_key: 'system.recovery')
177
+ ```
178
+
179
+ ##### Error Handling
180
+
181
+ Publishers raise specific exceptions that you can handle:
182
+
183
+ ```ruby
184
+ require 'ears'
185
+
186
+ publisher = Ears::Publisher.new('events', :topic)
187
+
188
+ begin
189
+ publisher.publish({ data: 'test' }, routing_key: 'test.message')
190
+ rescue Ears::PublisherRetryHandler::PublishError => e
191
+ # Handle publish failures (after all retries exhausted)
192
+ logger.error "Failed to publish message: #{e.message}"
193
+ # Consider queuing message for later retry or alerting
194
+ rescue => e
195
+ # Handle other unexpected errors
196
+ logger.error "Unexpected error: #{e.message}"
197
+ end
198
+ ```
199
+
200
+ ### Basic consumer usage
26
201
 
27
202
  First, you should configure `Ears`.
28
203
 
@@ -34,6 +209,16 @@ Ears.configure do |config|
34
209
  config.connection_name = 'My Consumer'
35
210
  config.recover_from_connection_close = false # optional configuration, defaults to true if not set
36
211
  config.recovery_attempts = 3 # optional configuration, defaults to 10, Bunny::Session would have been nil
212
+
213
+ # Publisher configuration (optional)
214
+ config.publisher_pool_size = 32 # Thread pool size for publishers (default: 32)
215
+ config.publisher_pool_timeout = 2 # Timeout for pool checkout in seconds (default: 2)
216
+ config.publisher_connection_attempts = 30 # Connection retry attempts (default: 30)
217
+ config.publisher_connection_base_delay = 1 # Base delay between connection attempts in seconds (default: 1)
218
+ config.publisher_connection_backoff_factor = 1.5 # Connection retry backoff multiplier (default: 1.5)
219
+ config.publisher_max_retries = 3 # Max publish retry attempts (default: 3)
220
+ config.publisher_retry_base_delay = 0.1 # Base delay between publish retries in seconds (default: 0.1)
221
+ config.publisher_retry_backoff_factor = 2 # Publish retry backoff multiplier (default: 2)
37
222
  end
38
223
  ```
39
224
 
@@ -285,6 +470,84 @@ Ears.stop!
285
470
 
286
471
  It will close and reset the current Bunny connection, leading to all consumers being shut down. Also, it will reset the channel.
287
472
 
473
+ ### Complete Example: Consumer and Publisher
474
+
475
+ Here's a complete example showing both consumer and publisher usage:
476
+
477
+ ```ruby
478
+ require 'ears'
479
+
480
+ # Shared configuration
481
+ Ears.configure do |config|
482
+ config.rabbitmq_url = 'amqp://guest:guest@localhost:5672'
483
+ config.connection_name = 'Order Processing Service'
484
+ config.publisher_pool_size = 16
485
+ end
486
+
487
+ # Consumer that processes orders and publishes events
488
+ class OrderProcessor < Ears::Consumer
489
+ configure(
490
+ queue: 'orders',
491
+ exchange: 'ecommerce',
492
+ routing_keys: %w[order.created order.updated],
493
+ retry_queue: true,
494
+ error_queue: true,
495
+ )
496
+
497
+ def initialize
498
+ super
499
+ @event_publisher = Ears::Publisher.new('events', :topic, durable: true)
500
+ end
501
+
502
+ def work(delivery_info, metadata, payload)
503
+ order = JSON.parse(payload)
504
+
505
+ # Process the order
506
+ process_order(order)
507
+
508
+ # Publish success event
509
+ @event_publisher.publish(
510
+ {
511
+ order_id: order['id'],
512
+ status: 'processed',
513
+ processed_at: Time.now.iso8601,
514
+ },
515
+ routing_key: 'order.processed',
516
+ )
517
+
518
+ ack
519
+ rescue => error
520
+ # Publish error event
521
+ @event_publisher.publish(
522
+ {
523
+ order_id: order&.dig('id'),
524
+ error: error.message,
525
+ failed_at: Time.now.iso8601,
526
+ },
527
+ routing_key: 'order.failed',
528
+ )
529
+
530
+ reject # Send to error queue
531
+ end
532
+
533
+ private
534
+
535
+ def process_order(order)
536
+ # Order processing logic here
537
+ sleep(0.1) # Simulate processing time
538
+ end
539
+ end
540
+
541
+ # Setup and run
542
+ Ears.setup { Ears.setup_consumers(OrderProcessor) }
543
+
544
+ begin
545
+ Ears.run!
546
+ ensure
547
+ # Cleanup code here
548
+ end
549
+ ```
550
+
288
551
  ## Documentation
289
552
 
290
553
  If you need more in-depth information, look at [our API documentation](https://www.rubydoc.info/gems/ears).
data/ears.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = 'A gem for building RabbitMQ consumers.'
11
11
  spec.homepage = 'https://github.com/ivx/ears'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 3.0.7')
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.2.9')
14
14
 
15
15
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
16
16
 
@@ -33,5 +33,6 @@ Gem::Specification.new do |spec|
33
33
  spec.require_paths = ['lib']
34
34
 
35
35
  spec.add_dependency 'bunny', '>= 2.22.0'
36
+ spec.add_dependency 'connection_pool', '~> 2.4'
36
37
  spec.add_dependency 'multi_json'
37
38
  end
@@ -1,4 +1,5 @@
1
1
  require 'ears/errors'
2
+ require 'logger'
2
3
 
3
4
  module Ears
4
5
  # The class representing the global {Ears} configuration.
@@ -8,6 +9,14 @@ module Ears
8
9
 
9
10
  DEFAULT_RABBITMQ_URL = 'amqp://guest:guest@localhost:5672'
10
11
  DEFAULT_RECOVERY_ATTEMPTS = 10
12
+ DEFAULT_PUBLISHER_POOL_SIZE = 32
13
+ DEFAULT_PUBLISHER_POOL_TIMEOUT = 2
14
+ DEFAULT_PUBLISHER_CONNECTION_ATTEMPTS = 30
15
+ DEFAULT_PUBLISHER_CONNECTION_BASE_DELAY = 1
16
+ DEFAULT_PUBLISHER_CONNECTION_BACKOFF_FACTOR = 1.5
17
+ DEFAULT_PUBLISHER_MAX_RETRIES = 3
18
+ DEFAULT_PUBLISHER_RETRY_BASE_DELAY = 0.1
19
+ DEFAULT_PUBLISHER_RETRY_BACKOFF_FACTOR = 2
11
20
 
12
21
  # @return [String] the connection string for RabbitMQ.
13
22
  attr_accessor :rabbitmq_url
@@ -21,9 +30,46 @@ module Ears
21
30
  # @return [Integer] max number of recovery attempts, nil means forever
22
31
  attr_accessor :recovery_attempts
23
32
 
33
+ # @return [Integer] the size of the publisher channel pool
34
+ attr_accessor :publisher_pool_size
35
+
36
+ # @return [Integer] the timeout in seconds for acquiring a channel from the publisher pool
37
+ attr_accessor :publisher_pool_timeout
38
+
39
+ # @return [Integer] the number of connection attempts for the publisher
40
+ attr_accessor :publisher_connection_attempts
41
+
42
+ # @return [Float] the base delay in seconds between connection attempts
43
+ attr_accessor :publisher_connection_base_delay
44
+
45
+ # @return [Float] the backoff factor for exponential connection delays
46
+ attr_accessor :publisher_connection_backoff_factor
47
+
48
+ # @return [Integer] the maximum number of retries for failed publish attempts
49
+ attr_accessor :publisher_max_retries
50
+
51
+ # @return [Float] the base delay in seconds between retry attempts
52
+ attr_accessor :publisher_retry_base_delay
53
+
54
+ # @return [Float] the backoff factor for exponential retry delays
55
+ attr_accessor :publisher_retry_backoff_factor
56
+
57
+ # @return [Logger] the logger instance for Ears operations
58
+ attr_accessor :logger
59
+
24
60
  def initialize
25
61
  @rabbitmq_url = DEFAULT_RABBITMQ_URL
26
62
  @recovery_attempts = DEFAULT_RECOVERY_ATTEMPTS
63
+ @publisher_pool_size = DEFAULT_PUBLISHER_POOL_SIZE
64
+ @publisher_pool_timeout = DEFAULT_PUBLISHER_POOL_TIMEOUT
65
+ @publisher_connection_attempts = DEFAULT_PUBLISHER_CONNECTION_ATTEMPTS
66
+ @publisher_connection_base_delay = DEFAULT_PUBLISHER_CONNECTION_BASE_DELAY
67
+ @publisher_connection_backoff_factor =
68
+ DEFAULT_PUBLISHER_CONNECTION_BACKOFF_FACTOR
69
+ @publisher_max_retries = DEFAULT_PUBLISHER_MAX_RETRIES
70
+ @publisher_retry_base_delay = DEFAULT_PUBLISHER_RETRY_BASE_DELAY
71
+ @publisher_retry_backoff_factor = DEFAULT_PUBLISHER_RETRY_BACKOFF_FACTOR
72
+ @logger = Logger.new(IO::NULL)
27
73
  end
28
74
 
29
75
  # @return [Proc] that is passed to Bunny’s recovery_attempts_exhausted block. Nil if recovery_attempts is nil.
@@ -0,0 +1,108 @@
1
+ require 'bunny'
2
+ require 'ears/publisher_channel_pool'
3
+ require 'ears/publisher_retry_handler'
4
+
5
+ module Ears
6
+ # Publisher for sending messages to RabbitMQ exchanges.
7
+ #
8
+ # Uses a connection pool for thread-safe publishing with configurable pool size.
9
+ # This provides better performance and thread safety compared to using per-thread channels.
10
+ class Publisher
11
+ # Creates a new publisher for the specified exchange.
12
+ #
13
+ # @param [String] exchange_name The name of the exchange to publish to.
14
+ # @param [Symbol] exchange_type The type of the exchange (:direct, :fanout, :topic or :headers).
15
+ # @param [Hash] exchange_options The options for the exchange. These are passed on to +Bunny::Exchange.new+.
16
+ def initialize(exchange_name, exchange_type = :topic, exchange_options = {})
17
+ @exchange_name = exchange_name
18
+ @exchange_type = exchange_type
19
+ @exchange_options = { durable: true }.merge(exchange_options)
20
+ @config = Ears.configuration
21
+ @logger = Ears.configuration.logger
22
+ end
23
+
24
+ # Publishes a JSON message to the configured exchange.
25
+ #
26
+ # @param [Hash, Array, Object] data The data to serialize as JSON and publish.
27
+ # @param [String] routing_key The routing key for the message.
28
+ #
29
+ # @option opts [String] :routing_key Routing key
30
+ # @option opts [Boolean] :persistent Should the message be persisted to disk?
31
+ # @option opts [Boolean] :mandatory Should the message be returned if it cannot be routed to any queue?
32
+ # @option opts [Integer] :timestamp A timestamp associated with this message
33
+ # @option opts [Integer] :expiration Expiration time after which the message will be deleted
34
+ # @option opts [String] :type Message type, e.g. what type of event or command this message represents. Can be any string
35
+ # @option opts [String] :reply_to Queue name other apps should send the response to
36
+ # @option opts [String] :content_type Message content type (e.g. application/json)
37
+ # @option opts [String] :content_encoding Message content encoding (e.g. gzip)
38
+ # @option opts [String] :correlation_id Message correlated to this one, e.g. what request this message is a reply for
39
+ # @option opts [Integer] :priority Message priority, 0 to 9. Not used by RabbitMQ, only applications
40
+ # @option opts [String] :message_id Any message identifier
41
+ # @option opts [String] :user_id Optional user ID. Verified by RabbitMQ against the actual connection username
42
+ # @option opts [String] :app_id Optional application ID
43
+ #
44
+ # @raise [PublishError] if publishing fails
45
+ # @return [void]
46
+ def publish(data, routing_key:, **options)
47
+ publish_options = default_publish_options.merge(options)
48
+
49
+ retry_handler.run do
50
+ publish_with_channel(data:, routing_key:, publish_options:)
51
+ end
52
+ end
53
+
54
+ # Resets the channel pool, forcing new channels to be created.
55
+ # This can be useful for connection recovery scenarios.
56
+ #
57
+ # @return [void]
58
+ def reset!
59
+ PublisherChannelPool.reset!
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :exchange_name,
65
+ :exchange_type,
66
+ :exchange_options,
67
+ :config,
68
+ :logger
69
+
70
+ 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
75
+
76
+ PublisherChannelPool.with_channel do |channel|
77
+ exchange = create_exchange(channel)
78
+ exchange.publish(
79
+ data,
80
+ { routing_key: routing_key }.merge(publish_options),
81
+ )
82
+ end
83
+ end
84
+
85
+ def create_exchange(channel)
86
+ Bunny::Exchange.new(
87
+ channel,
88
+ exchange_type,
89
+ exchange_name,
90
+ exchange_options,
91
+ )
92
+ end
93
+
94
+ def default_publish_options
95
+ {
96
+ persistent: true,
97
+ timestamp: Time.now.to_i,
98
+ headers: {
99
+ },
100
+ content_type: 'application/json',
101
+ }
102
+ end
103
+
104
+ def retry_handler
105
+ @retry_handler ||= PublisherRetryHandler.new(config, logger)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,55 @@
1
+ require 'connection_pool'
2
+
3
+ module Ears
4
+ # Channel pool management for publishers.
5
+ # Provides thread-safe channel pooling separate from consumer channels.
6
+ class PublisherChannelPool
7
+ class << self
8
+ # Executes the given block with a channel from the pool.
9
+ #
10
+ # @yieldparam [Bunny::Channel] channel The channel to use for publishing
11
+ # @return [Object] The result of the block
12
+ def with_channel(&)
13
+ channel_pool.with(&)
14
+ end
15
+
16
+ # Resets the channel pool, forcing new channels to be created.
17
+ # This is useful for connection recovery scenarios.
18
+ #
19
+ # @return [void]
20
+ def reset!
21
+ pool = @channel_pool
22
+ @channel_pool = nil
23
+ @creator_pid = nil
24
+
25
+ pool&.shutdown(&:close)
26
+ nil
27
+ end
28
+
29
+ private
30
+
31
+ def channel_pool
32
+ # Recreate lazily after a fork
33
+ reset! if @creator_pid && @creator_pid != Process.pid
34
+
35
+ return @channel_pool if @channel_pool
36
+
37
+ init_mutex.synchronize do
38
+ @channel_pool ||=
39
+ begin
40
+ @creator_pid = Process.pid
41
+
42
+ ConnectionPool.new(
43
+ size: Ears.configuration.publisher_pool_size,
44
+ timeout: Ears.configuration.publisher_pool_timeout,
45
+ ) { Ears.connection.create_channel }
46
+ end
47
+ end
48
+ end
49
+
50
+ def init_mutex
51
+ @init_mutex ||= Mutex.new
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ require 'bunny'
2
+
3
+ module Ears
4
+ # A handler for retries and connection recovery when publishing messages.
5
+ class PublisherRetryHandler
6
+ # Exception for publishing to a stale/closed channel
7
+ class PublishToStaleChannelError < StandardError
8
+ end
9
+
10
+ # Connection errors that should trigger retries
11
+ CONNECTION_ERRORS = [
12
+ PublishToStaleChannelError,
13
+ Bunny::ChannelAlreadyClosed,
14
+ Bunny::ConnectionClosedError,
15
+ Bunny::ConnectionForced,
16
+ Bunny::NetworkFailure,
17
+ Bunny::TCPConnectionFailed,
18
+ IOError,
19
+ Timeout::Error,
20
+ ].freeze
21
+
22
+ def initialize(config, logger)
23
+ @config = config
24
+ @logger = logger
25
+ end
26
+
27
+ def run(&block)
28
+ attempt = 1
29
+ begin
30
+ block.call
31
+ rescue *CONNECTION_ERRORS => e
32
+ attempt = handle_connection_error(e, attempt) # rubocop:disable Lint/UselessAssignment
33
+ retry
34
+ rescue StandardError => e
35
+ attempt = handle_standard_error(e, attempt)
36
+ retry
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :config, :logger
43
+
44
+ def handle_connection_error(error, attempt)
45
+ raise(error) unless retry?(error, attempt)
46
+
47
+ wait_for_connection(error)
48
+ logger.info('Resetting channel pool after connection recovery')
49
+ PublisherChannelPool.reset!
50
+
51
+ attempt + 1
52
+ end
53
+
54
+ def handle_standard_error(error, attempt)
55
+ raise(error) unless retry?(error, attempt)
56
+
57
+ sleep(retry_backoff_delay(attempt)) if attempt > 1
58
+ attempt + 1
59
+ end
60
+
61
+ def retry?(error, attempt)
62
+ logger.info(
63
+ "Trying to recover from publish error. Attempt #{attempt}: #{error.class}: #{error.message}",
64
+ )
65
+
66
+ if attempt > config.publisher_max_retries
67
+ logger.warn(
68
+ "Connection attempts exhausted, giving up: #{error.class}: #{error.message}",
69
+ )
70
+ return false
71
+ end
72
+
73
+ true
74
+ end
75
+
76
+ def wait_for_connection(original_error)
77
+ connection_attempt = 0
78
+ logger.info('Trying to reconnect after connection error')
79
+ while !Ears.connection.open?
80
+ logger.info(
81
+ "Connection still closed, attempt #{connection_attempt + 1}",
82
+ )
83
+ connection_attempt += 1
84
+
85
+ if connection_attempt > config.publisher_connection_attempts
86
+ logger.error('Connection attempts exhausted, giving up')
87
+ raise original_error
88
+ end
89
+
90
+ sleep(connection_backoff_delay(connection_attempt))
91
+ end
92
+ end
93
+
94
+ def retry_backoff_delay(attempt)
95
+ config.publisher_retry_base_delay *
96
+ (config.publisher_retry_backoff_factor**(attempt - 1))
97
+ end
98
+
99
+ def connection_backoff_delay(attempt)
100
+ config.publisher_connection_base_delay *
101
+ (config.publisher_connection_backoff_factor**(attempt - 1))
102
+ end
103
+ end
104
+ end
data/lib/ears/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ears
2
- VERSION = '0.19.0'
2
+ VERSION = '0.21.0'
3
3
  end
data/lib/ears.rb CHANGED
@@ -2,6 +2,8 @@ require 'bunny'
2
2
  require 'ears/configuration'
3
3
  require 'ears/consumer'
4
4
  require 'ears/middleware'
5
+ require 'ears/publisher'
6
+ require 'ears/publisher_channel_pool'
5
7
  require 'ears/setup'
6
8
  require 'ears/version'
7
9
 
@@ -43,8 +45,8 @@ module Ears
43
45
  end
44
46
 
45
47
  # Used to set up your exchanges, queues and consumers. See {Ears::Setup} for implementation details.
46
- def setup(&block)
47
- Ears::Setup.new.instance_eval(&block)
48
+ def setup(&)
49
+ Ears::Setup.new.instance_eval(&)
48
50
  end
49
51
 
50
52
  # Quick setup your consumers (including exchanges and queues).
data/package-lock.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "devDependencies": {
9
9
  "@invisionag/prettier-config": "^2.1.3",
10
10
  "@prettier/plugin-ruby": "^4.0.4",
11
- "prettier": "^3.5.2"
11
+ "prettier": "^3.6.2"
12
12
  }
13
13
  },
14
14
  "node_modules/@invisionag/prettier-config": {
@@ -30,9 +30,9 @@
30
30
  }
31
31
  },
32
32
  "node_modules/prettier": {
33
- "version": "3.5.2",
34
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
35
- "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
33
+ "version": "3.6.2",
34
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
35
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
36
36
  "dev": true,
37
37
  "license": "MIT",
38
38
  "bin": {
@@ -62,9 +62,9 @@
62
62
  "requires": {}
63
63
  },
64
64
  "prettier": {
65
- "version": "3.5.2",
66
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
67
- "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
65
+ "version": "3.6.2",
66
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
67
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
68
68
  "dev": true
69
69
  }
70
70
  }
data/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "devDependencies": {
10
10
  "@invisionag/prettier-config": "^2.1.3",
11
11
  "@prettier/plugin-ruby": "^4.0.4",
12
- "prettier": "^3.5.2"
12
+ "prettier": "^3.6.2"
13
13
  },
14
14
  "prettier": "@invisionag/prettier-config/ruby"
15
15
  }
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.19.0
4
+ version: 0.21.0
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-03-10 00:00:00.000000000 Z
11
+ date: 2025-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 2.22.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.4'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: multi_json
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -73,6 +87,9 @@ files:
73
87
  - lib/ears/middlewares/appsignal.rb
74
88
  - lib/ears/middlewares/json.rb
75
89
  - lib/ears/middlewares/max_retries.rb
90
+ - lib/ears/publisher.rb
91
+ - lib/ears/publisher_channel_pool.rb
92
+ - lib/ears/publisher_retry_handler.rb
76
93
  - lib/ears/setup.rb
77
94
  - lib/ears/version.rb
78
95
  - package-lock.json
@@ -94,14 +111,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
111
  requirements:
95
112
  - - ">="
96
113
  - !ruby/object:Gem::Version
97
- version: 3.0.7
114
+ version: 3.2.9
98
115
  required_rubygems_version: !ruby/object:Gem::Requirement
99
116
  requirements:
100
117
  - - ">="
101
118
  - !ruby/object:Gem::Version
102
119
  version: '0'
103
120
  requirements: []
104
- rubygems_version: 3.3.27
121
+ rubygems_version: 3.4.19
105
122
  signing_key:
106
123
  specification_version: 4
107
124
  summary: A gem for building RabbitMQ consumers.