lepus 0.1.3 → 0.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 +4 -4
- data/.github/workflows/linter.yml +1 -1
- data/.github/workflows/specs.yml +2 -2
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/README.md +9 -0
- data/docs/configuration.md +14 -1
- data/docs/producers.md +30 -2
- data/docs/testing.md +8 -0
- data/gemfiles/Gemfile.rails-5.2.lock +1 -1
- data/gemfiles/Gemfile.rails-6.1.lock +1 -1
- data/gemfiles/Gemfile.rails-7.2.lock +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +1 -1
- data/lib/lepus/configuration.rb +47 -6
- data/lib/lepus/connection_pool.rb +4 -3
- data/lib/lepus/consumer.rb +35 -19
- data/lib/lepus/producer.rb +14 -7
- data/lib/lepus/producers/config.rb +25 -12
- data/lib/lepus/producers/definition.rb +3 -1
- data/lib/lepus/publisher.rb +7 -2
- data/lib/lepus/rails/log_subscriber.rb +8 -0
- data/lib/lepus/testing/exchange.rb +2 -0
- data/lib/lepus/testing.rb +5 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus.rb +5 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cf7f4418578e3ab6600df2c9de267e134311af605a96db80f7b0f746763096d
|
|
4
|
+
data.tar.gz: 469fda46acebe440afe6ecca36e78b7b4395c520bc789ed90183553c3eb78ed7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed0693421c238ba690ad399d072449920d73fdc63ec2437095ce8d8352bdf5aabfb65517ce34ed5aaae06fe68fa45aefa55dd31a3c1a743e5f80e43b3dbaad18
|
|
7
|
+
data.tar.gz: 4fc5b7a1735979b94c94b7d6a6fd6979bf84c3479e19ec0661ba355c604af9233f218bf735dca9990a4d31197b468185506739c1f17b5618a2ffc664c39d3a43
|
data/.github/workflows/specs.yml
CHANGED
|
@@ -8,7 +8,7 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
ruby-2-7:
|
|
11
|
-
runs-on: ubuntu-
|
|
11
|
+
runs-on: ubuntu-24.04
|
|
12
12
|
strategy:
|
|
13
13
|
fail-fast: false
|
|
14
14
|
matrix:
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
run: bundle exec rspec
|
|
33
33
|
|
|
34
34
|
ruby-3-0:
|
|
35
|
-
runs-on: ubuntu-
|
|
35
|
+
runs-on: ubuntu-24.04
|
|
36
36
|
strategy:
|
|
37
37
|
fail-fast: false
|
|
38
38
|
matrix:
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.2.1 - 2026-10-30
|
|
8
|
+
* Added: `process_delivery.lepus` instrumentation around every consumed message, with consumer, exchange, routing key, redelivered flag and result
|
|
9
|
+
* Added: `consumer_error.lepus` instrumentation when `#perform` raises, before the message is rejected
|
|
10
|
+
* Added: `Lepus::LogSubscriber` logs processed deliveries (info) and consumer errors (error)
|
|
11
|
+
* Fixed: `Lepus.instrument` without a block no longer raises when ActiveSupport is not loaded
|
|
12
|
+
|
|
13
|
+
## 0.2.0 - 2026-10-28
|
|
14
|
+
* Added: named producer connections via `config.connection(name, url:, **bunny_options)`; `rabbitmq_url` remains the `:default` connection
|
|
15
|
+
* Added: `connection:` option on `Producer.configure`, `Producer.publish` and `Lepus::Publisher.new`, with one producer connection pool per name
|
|
16
|
+
* Added: `Lepus::InvalidConnectionError` raised for unregistered connection names
|
|
17
|
+
* Added: fake messages in `Lepus::Testing` record their `connection`
|
|
18
|
+
|
|
7
19
|
## 0.0.1 - 2024-08-01
|
|
8
20
|
The first release of the gem
|
|
9
21
|
* Added: Initial implementation with support to Sidekiq and Faktory backends
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -567,6 +567,15 @@ bundle exec lepus start MyConsumer1 MyConsumer2 --debug
|
|
|
567
567
|
|
|
568
568
|
Each consumer will run in a separate process, and a supervisor will monitor them. If a consumer crashes, the supervisor will restart it.
|
|
569
569
|
|
|
570
|
+
### Instrumentation and logging
|
|
571
|
+
|
|
572
|
+
Every consumed message emits a `process_delivery.lepus` event (`consumer`, `exchange`, `routing_key`, `redelivered`, `result`), and an exception raised by `#perform` emits `consumer_error.lepus` (with `error`) before the message is rejected. In Rails, `Lepus::LogSubscriber` logs both to `Lepus.logger`:
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
Lepus-0.2.1 Processed OrdersConsumer (12.3ms) exchange: "orders", routing_key: "orders.created", redelivered: false, result: :ack
|
|
576
|
+
Lepus-0.2.1 Error in OrdersConsumer (0.0ms) exchange: "orders", routing_key: "orders.created", redelivered: false, error: "RuntimeError boom"
|
|
577
|
+
```
|
|
578
|
+
|
|
570
579
|
### Puma Plugin
|
|
571
580
|
|
|
572
581
|
We provide a Puma plugin if you want to run the Lepus's supervisor together with Puma and have Puma monitor and manage it. You just need to add
|
data/docs/configuration.md
CHANGED
|
@@ -28,6 +28,19 @@ end
|
|
|
28
28
|
| `recovery_attempts` | `10` | Max automatic reconnects; `nil` for infinite |
|
|
29
29
|
| `recover_from_connection_close` | `true` | Auto-recover after a clean close |
|
|
30
30
|
|
|
31
|
+
### Named connections
|
|
32
|
+
|
|
33
|
+
Producers can publish to more than one broker. Register extra brokers by name; `rabbitmq_url` stays the `:default` connection and remains the only one used by consumers, the supervisor, the process registry and the web dashboard.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Lepus.configure do |config|
|
|
37
|
+
config.rabbitmq_url = ENV.fetch('RABBITMQ_URL')
|
|
38
|
+
config.connection(:replica, url: ENV.fetch('REPLICA_RABBITMQ_URL'), recovery_attempts: 3)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`url:` is required and must not be blank. Extra keyword arguments are Bunny session options and override the global ones for that connection only; a `connection_name:` given here is used as-is instead of the generated label. The name `:default` is reserved. Publishing through a name that was never registered raises `Lepus::InvalidConnectionError`. See [producers.md](producers.md#named-connections).
|
|
43
|
+
|
|
31
44
|
## Application metadata
|
|
32
45
|
|
|
33
46
|
```ruby
|
|
@@ -72,7 +85,7 @@ Assign consumers to a named worker via their own `configure(process: { name: :hi
|
|
|
72
85
|
|
|
73
86
|
## Producer pool
|
|
74
87
|
|
|
75
|
-
Producers share a connection pool:
|
|
88
|
+
Producers share a connection pool per named connection (see [Named connections](#named-connections)); every pool uses these settings:
|
|
76
89
|
|
|
77
90
|
```ruby
|
|
78
91
|
config.producer do |p|
|
data/docs/producers.md
CHANGED
|
@@ -30,7 +30,8 @@ OrdersProducer.publish(
|
|
|
30
30
|
```ruby
|
|
31
31
|
configure(
|
|
32
32
|
exchange: { name: 'orders', type: :topic, durable: true },
|
|
33
|
-
publish: { persistent: true, mandatory: false }
|
|
33
|
+
publish: { persistent: true, mandatory: false },
|
|
34
|
+
connection: :default
|
|
34
35
|
)
|
|
35
36
|
```
|
|
36
37
|
|
|
@@ -38,6 +39,7 @@ configure(
|
|
|
38
39
|
|-----|---------|
|
|
39
40
|
| `exchange` | String (name only) or hash (`name`, `type`, `durable`, `auto_delete`). Auto-declared on first publish. |
|
|
40
41
|
| `publish` | Default publish options merged into every `publish` call: `persistent`, `mandatory`, `content_type`, `expiration`, etc. |
|
|
42
|
+
| `connection` | Named connection to publish through. Defaults to `:default` (`rabbitmq_url`). See [Named connections](#named-connections). |
|
|
41
43
|
|
|
42
44
|
## Publishing
|
|
43
45
|
|
|
@@ -119,7 +121,7 @@ OrdersProducer.order_created(order)
|
|
|
119
121
|
|
|
120
122
|
## Connection pooling
|
|
121
123
|
|
|
122
|
-
Producers share
|
|
124
|
+
Producers share one connection pool per named connection across the process:
|
|
123
125
|
|
|
124
126
|
```ruby
|
|
125
127
|
Lepus.configure do |config|
|
|
@@ -130,6 +132,32 @@ Lepus.configure do |config|
|
|
|
130
132
|
end
|
|
131
133
|
```
|
|
132
134
|
|
|
135
|
+
## Named connections
|
|
136
|
+
|
|
137
|
+
Register extra brokers in the configuration (see [configuration.md](configuration.md#named-connections)):
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Lepus.configure do |config|
|
|
141
|
+
config.connection(:replica, url: ENV.fetch('REPLICA_RABBITMQ_URL'))
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then choose the connection per producer or per call. The per-call value wins, then the one in `configure`, then `:default`:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class AuditProducer < Lepus::Producer
|
|
149
|
+
configure(exchange: 'audit', connection: :replica)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
OrdersProducer.publish(payload, routing_key: 'order.created') # :default
|
|
153
|
+
OrdersProducer.publish(payload, routing_key: 'order.created', connection: :replica) # :replica
|
|
154
|
+
AuditProducer.publish(payload) # :replica
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`connection` is never sent to Bunny as a publish option. Each connection gets its own lazily created pool, so a broker that is down only affects the publishes routed to it. An unregistered name raises `Lepus::InvalidConnectionError`.
|
|
158
|
+
|
|
159
|
+
The low-level publisher takes the same option: `Lepus::Publisher.new('audit', connection: :replica)`.
|
|
160
|
+
|
|
133
161
|
## Enable / disable publishing
|
|
134
162
|
|
|
135
163
|
Useful when you want to disable side effects in tests or a specific environment.
|
data/docs/testing.md
CHANGED
|
@@ -89,6 +89,14 @@ end
|
|
|
89
89
|
|
|
90
90
|
`producer_messages(ProducerClass)` returns an array of hashes with `:payload`, `:routing_key`, `:delivery_info`, `:metadata`.
|
|
91
91
|
|
|
92
|
+
Each fake message also records the named connection it was published through:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Lepus::Testing.exchange('orders').find_messages(connection: :replica)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The connection may be given as a symbol or a string. Publishing through a connection that was never registered with `config.connection` raises `Lepus::InvalidConnectionError`, as it does outside of fake mode.
|
|
99
|
+
|
|
92
100
|
## RSpec matchers
|
|
93
101
|
|
|
94
102
|
```ruby
|
data/lib/lepus/configuration.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Lepus
|
|
|
8
8
|
DEFAULT_RECOVERY_INTERVAL = 5.0
|
|
9
9
|
DEFAULT_RECOVER_FROM_CONNECTION_CLOSE = true
|
|
10
10
|
DEFAULT_CONSUMERS_DIRECTORY = Pathname.new("app/consumers")
|
|
11
|
+
DEFAULT_CONNECTION = :default
|
|
11
12
|
|
|
12
13
|
# @return [String] the connection string for RabbitMQ.
|
|
13
14
|
attr_accessor :rabbitmq_url
|
|
@@ -93,17 +94,48 @@ module Lepus
|
|
|
93
94
|
Web::ManagementAPI.new(base_url: management_api_url)
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
# Registers a named RabbitMQ connection that producers can publish through.
|
|
98
|
+
# The :default connection is reserved and always uses {#rabbitmq_url}.
|
|
99
|
+
# @param name [Symbol, String] the connection name
|
|
100
|
+
# @param url [String] the connection string for RabbitMQ
|
|
101
|
+
# @param options [Hash] Bunny session options overriding the global ones
|
|
102
|
+
# @return [Hash] the connection settings
|
|
103
|
+
def connection(name, url:, **options)
|
|
104
|
+
name = name.to_sym
|
|
105
|
+
raise ArgumentError, "The #{DEFAULT_CONNECTION.inspect} connection is configured through rabbitmq_url" if name == DEFAULT_CONNECTION
|
|
106
|
+
raise ArgumentError, "The #{name.inspect} connection requires a url" if url.to_s.strip.empty?
|
|
107
|
+
|
|
108
|
+
connections[name] = options.merge(url: url)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Hash{Symbol => Hash}] the named connections registered via {#connection}
|
|
112
|
+
def connections
|
|
113
|
+
@connections ||= {}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @param name [Symbol, String, nil] the connection name
|
|
117
|
+
# @return [Boolean] whether the connection is the default one or a registered named connection
|
|
118
|
+
def connection?(name)
|
|
119
|
+
name = (name || DEFAULT_CONNECTION).to_sym
|
|
120
|
+
name == DEFAULT_CONNECTION || connections.key?(name)
|
|
121
|
+
end
|
|
122
|
+
|
|
96
123
|
# @param suffix [String] the suffix to add to the connection name
|
|
124
|
+
# @param connection [Symbol, String] the named connection to open, :default uses {#rabbitmq_url}
|
|
97
125
|
# @return [Bunny::Session] the connection to RabbitMQ
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
# @raise [Lepus::InvalidConnectionError] when the connection name is not registered
|
|
127
|
+
def create_connection(suffix: nil, connection: DEFAULT_CONNECTION)
|
|
128
|
+
name = (connection || DEFAULT_CONNECTION).to_sym
|
|
129
|
+
url, options = connection_settings(name)
|
|
130
|
+
kwargs = connection_config.merge(options)
|
|
131
|
+
|
|
132
|
+
if connection_name && !options.key?(:connection_name)
|
|
133
|
+
label = [suffix, (name == DEFAULT_CONNECTION) ? nil : "(#{name})"].compact.join(" ")
|
|
134
|
+
kwargs[:connection_name] = "#{connection_name} #{label}" unless label.empty?
|
|
103
135
|
end
|
|
104
136
|
|
|
105
137
|
::Bunny
|
|
106
|
-
.new(
|
|
138
|
+
.new(url, **kwargs)
|
|
107
139
|
.tap { |conn| conn.start }
|
|
108
140
|
end
|
|
109
141
|
|
|
@@ -174,6 +206,15 @@ module Lepus
|
|
|
174
206
|
|
|
175
207
|
protected
|
|
176
208
|
|
|
209
|
+
def connection_settings(name)
|
|
210
|
+
return [rabbitmq_url, {}] if name == DEFAULT_CONNECTION
|
|
211
|
+
|
|
212
|
+
settings = connections.fetch(name) do
|
|
213
|
+
raise Lepus::InvalidConnectionError, "Unknown connection #{name.inspect}. Register it with Lepus.config.connection(#{name.inspect}, url: ...)"
|
|
214
|
+
end
|
|
215
|
+
[settings[:url], settings.reject { |key, _| key == :url }]
|
|
216
|
+
end
|
|
217
|
+
|
|
177
218
|
def connection_config
|
|
178
219
|
{
|
|
179
220
|
connection_name: connection_name,
|
|
@@ -9,12 +9,13 @@ module Lepus
|
|
|
9
9
|
DEFAULT_SIZE = 5
|
|
10
10
|
DEFAULT_TIMEOUT = 5.0
|
|
11
11
|
|
|
12
|
-
attr_reader :pool_size, :timeout, :conn_suffix
|
|
12
|
+
attr_reader :pool_size, :timeout, :conn_suffix, :conn_name
|
|
13
13
|
|
|
14
|
-
def initialize(size: DEFAULT_SIZE, timeout: DEFAULT_TIMEOUT, suffix: nil)
|
|
14
|
+
def initialize(size: DEFAULT_SIZE, timeout: DEFAULT_TIMEOUT, suffix: nil, connection: Lepus::Configuration::DEFAULT_CONNECTION)
|
|
15
15
|
@pool_size = size
|
|
16
16
|
@timeout = timeout
|
|
17
17
|
@conn_suffix = suffix
|
|
18
|
+
@conn_name = connection
|
|
18
19
|
@available = Concurrent::Array.new
|
|
19
20
|
@in_use = Concurrent::Array.new
|
|
20
21
|
@semaphore = Concurrent::Semaphore.new(pool_size)
|
|
@@ -62,7 +63,7 @@ module Lepus
|
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
# Create a new connection
|
|
65
|
-
connection = Lepus.config.create_connection(suffix: @conn_suffix)
|
|
66
|
+
connection = Lepus.config.create_connection(suffix: @conn_suffix, connection: @conn_name)
|
|
66
67
|
@mutex.with_write_lock do
|
|
67
68
|
@in_use << connection
|
|
68
69
|
end
|
data/lib/lepus/consumer.rb
CHANGED
|
@@ -98,26 +98,9 @@ module Lepus
|
|
|
98
98
|
message = Message.coerce(delivery_info, metadata, payload)
|
|
99
99
|
message.consumer_class = self.class
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.class.middleware_chain
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
combined_chain.execute(message) do |msg|
|
|
107
|
-
perform(msg).tap do |result|
|
|
108
|
-
verify_result(result)
|
|
109
|
-
end
|
|
101
|
+
Lepus.instrument(:process_delivery, **delivery_attributes(delivery_info)) do |event|
|
|
102
|
+
event[:result] = deliver(message, delivery_info)
|
|
110
103
|
end
|
|
111
|
-
rescue Lepus::InvalidConsumerReturnError
|
|
112
|
-
raise
|
|
113
|
-
rescue Exception # rubocop:disable Lint/RescueException
|
|
114
|
-
on_delivery_error
|
|
115
|
-
# In testing mode, re-raise exceptions if consumer_raise_errors? is enabled
|
|
116
|
-
if defined?(Lepus::Testing) && Lepus::Testing.consumer_raise_errors?
|
|
117
|
-
raise
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
reject!
|
|
121
104
|
end
|
|
122
105
|
|
|
123
106
|
# Returns whether the last delivery resulted in an error.
|
|
@@ -194,6 +177,39 @@ module Lepus
|
|
|
194
177
|
def on_delivery_error
|
|
195
178
|
end
|
|
196
179
|
|
|
180
|
+
def deliver(message, delivery_info)
|
|
181
|
+
combined_chain = MiddlewareChain.combine(
|
|
182
|
+
Lepus.config.consumer_middleware_chain,
|
|
183
|
+
self.class.middleware_chain
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
combined_chain.execute(message) do |msg|
|
|
187
|
+
perform(msg).tap do |result|
|
|
188
|
+
verify_result(result)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
rescue Lepus::InvalidConsumerReturnError
|
|
192
|
+
raise
|
|
193
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
194
|
+
Lepus.instrument(:consumer_error, error: error, **delivery_attributes(delivery_info))
|
|
195
|
+
on_delivery_error
|
|
196
|
+
# In testing mode, re-raise exceptions if consumer_raise_errors? is enabled
|
|
197
|
+
if defined?(Lepus::Testing) && Lepus::Testing.consumer_raise_errors?
|
|
198
|
+
raise
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
reject!
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def delivery_attributes(delivery_info)
|
|
205
|
+
{
|
|
206
|
+
consumer: self.class.name,
|
|
207
|
+
exchange: delivery_info&.exchange,
|
|
208
|
+
routing_key: delivery_info&.routing_key,
|
|
209
|
+
redelivered: delivery_info&.redelivered
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
197
213
|
def verify_result(result)
|
|
198
214
|
return if %i[ack reject requeue nack].include?(result)
|
|
199
215
|
|
data/lib/lepus/producer.rb
CHANGED
|
@@ -35,6 +35,7 @@ module Lepus
|
|
|
35
35
|
# @param [Hash] opts The options to configure the producer with.
|
|
36
36
|
# @option opts [String, Hash] :exchange The name of the exchange to publish to.
|
|
37
37
|
# @option opts [Hash] :publish Default publish options (persistent, mandatory, immediate).
|
|
38
|
+
# @option opts [Symbol] :connection The named connection to publish through. Defaults to :default.
|
|
38
39
|
# @yield [definition] Optional block to further configure the producer.
|
|
39
40
|
# @yieldparam [Lepus::Producers::Definition] definition The definition object.
|
|
40
41
|
# @return [Lepus::Producers::Definition] The configured producer definition.
|
|
@@ -42,6 +43,7 @@ module Lepus
|
|
|
42
43
|
raise ArgumentError, "Cannot configure an abstract class" if abstract_class?
|
|
43
44
|
|
|
44
45
|
@definition = Producers::Definition.new(opts)
|
|
46
|
+
@publishers = nil
|
|
45
47
|
yield(@definition) if block_given?
|
|
46
48
|
@definition
|
|
47
49
|
end
|
|
@@ -54,10 +56,13 @@ module Lepus
|
|
|
54
56
|
descendants.uniq
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
#
|
|
59
|
+
# Returns a publisher configured with this producer's settings, memoized per connection.
|
|
60
|
+
# @param connection [Symbol, String, nil] The named connection. Defaults to the configured one.
|
|
58
61
|
# @return [Lepus::Publisher] A publisher instance ready to send messages.
|
|
59
|
-
def publisher
|
|
60
|
-
|
|
62
|
+
def publisher(connection = nil)
|
|
63
|
+
name = (connection || definition.connection).to_sym
|
|
64
|
+
@publishers ||= {}
|
|
65
|
+
@publishers[name] ||= Publisher.new(definition.exchange_name, connection: name, **definition.exchange_options)
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
# Returns the middleware chain for this producer.
|
|
@@ -79,9 +84,11 @@ module Lepus
|
|
|
79
84
|
# Executes the middleware chain (global + per-producer) before publishing.
|
|
80
85
|
#
|
|
81
86
|
# @param payload [String, Hash] The message payload to publish.
|
|
87
|
+
# @param connection [Symbol, String, nil] The named connection to publish through,
|
|
88
|
+
# overriding the one set in {.configure}.
|
|
82
89
|
# @param options [Hash] Additional publish options (routing_key, headers, etc.).
|
|
83
90
|
# @return [void]
|
|
84
|
-
def publish(payload, **options)
|
|
91
|
+
def publish(payload, connection: nil, **options)
|
|
85
92
|
if definition.nil?
|
|
86
93
|
raise InvalidProducerConfigError, <<~ERROR
|
|
87
94
|
The #{name} producer is not configured.
|
|
@@ -99,7 +106,7 @@ module Lepus
|
|
|
99
106
|
)
|
|
100
107
|
|
|
101
108
|
combined_chain.execute(message) do |msg|
|
|
102
|
-
publisher.publish(msg.payload, **msg.to_publish_options)
|
|
109
|
+
publisher(connection).publish(msg.payload, **msg.to_publish_options)
|
|
103
110
|
end
|
|
104
111
|
end
|
|
105
112
|
|
|
@@ -142,8 +149,8 @@ module Lepus
|
|
|
142
149
|
|
|
143
150
|
attr_reader :definition
|
|
144
151
|
|
|
145
|
-
def publisher
|
|
146
|
-
|
|
152
|
+
def publisher(connection = nil)
|
|
153
|
+
self.class.publisher(connection)
|
|
147
154
|
end
|
|
148
155
|
|
|
149
156
|
def publish(message, **options)
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Lepus
|
|
6
4
|
module Producers
|
|
7
5
|
# Configuration class for producer settings
|
|
8
6
|
class Config
|
|
9
|
-
extend Forwardable
|
|
10
|
-
|
|
11
7
|
DEFAULT_POOL_SIZE = 1
|
|
12
8
|
DEFAULT_POOL_TIMEOUT = 5.0
|
|
13
9
|
|
|
14
10
|
attr_accessor :pool_size, :pool_timeout
|
|
15
11
|
|
|
16
|
-
def_delegator :connection_pool, :with_connection
|
|
17
|
-
|
|
18
12
|
def initialize
|
|
19
13
|
@pool_size = DEFAULT_POOL_SIZE
|
|
20
14
|
@pool_timeout = DEFAULT_POOL_TIMEOUT
|
|
15
|
+
@connection_pools = {}
|
|
16
|
+
@connection_pools_mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks out a connection from the pool of the given named connection.
|
|
20
|
+
# @param name [Symbol, String] the connection name, :default uses Lepus.config.rabbitmq_url
|
|
21
|
+
# @yieldparam connection [Bunny::Session]
|
|
22
|
+
# @raise [Lepus::InvalidConnectionError] when the connection name is not registered
|
|
23
|
+
def with_connection(name = Lepus::Configuration::DEFAULT_CONNECTION, &block)
|
|
24
|
+
connection_pool(name).with_connection(&block)
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# Assign multiple attributes at once from a hash of options.
|
|
@@ -33,13 +37,22 @@ module Lepus
|
|
|
33
37
|
|
|
34
38
|
private
|
|
35
39
|
|
|
40
|
+
# @param name [Symbol, String] the connection name
|
|
36
41
|
# @return [Lepus::ConnectionPool] a connection pool instance configured for producers
|
|
37
|
-
def connection_pool
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
def connection_pool(name = Lepus::Configuration::DEFAULT_CONNECTION)
|
|
43
|
+
name = (name || Lepus::Configuration::DEFAULT_CONNECTION).to_sym
|
|
44
|
+
unless name == Lepus::Configuration::DEFAULT_CONNECTION || Lepus.config.connection?(name)
|
|
45
|
+
raise Lepus::InvalidConnectionError, "Unknown connection #{name.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@connection_pools_mutex.synchronize do
|
|
49
|
+
@connection_pools[name] ||= Lepus::ConnectionPool.new(
|
|
50
|
+
size: pool_size,
|
|
51
|
+
timeout: pool_timeout,
|
|
52
|
+
suffix: "producer",
|
|
53
|
+
connection: name
|
|
54
|
+
)
|
|
55
|
+
end
|
|
43
56
|
end
|
|
44
57
|
end
|
|
45
58
|
end
|
|
@@ -4,7 +4,7 @@ module Lepus
|
|
|
4
4
|
module Producers
|
|
5
5
|
# Definition class for producer-specific settings
|
|
6
6
|
class Definition
|
|
7
|
-
attr_reader :publish_options
|
|
7
|
+
attr_reader :publish_options, :connection
|
|
8
8
|
|
|
9
9
|
def initialize(options = {})
|
|
10
10
|
opts = Lepus::Primitive::Hash.new(options).deep_symbolize_keys
|
|
@@ -16,6 +16,8 @@ module Lepus
|
|
|
16
16
|
# Handle default publish options
|
|
17
17
|
@publish_options = Lepus::Publisher::DEFAULT_PUBLISH_OPTIONS.merge(opts.delete(:publish) || {})
|
|
18
18
|
|
|
19
|
+
@connection = (opts.delete(:connection) || Lepus::Configuration::DEFAULT_CONNECTION).to_sym
|
|
20
|
+
|
|
19
21
|
# Store any remaining options for future use
|
|
20
22
|
@options = opts
|
|
21
23
|
end
|
data/lib/lepus/publisher.rb
CHANGED
|
@@ -15,17 +15,22 @@ module Lepus
|
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
17
|
# @param exchange_name [String] The name of the exchange to publish messages to.
|
|
18
|
+
# @param connection [Symbol, String] The named connection to publish through.
|
|
18
19
|
# @param options [Hash] Additional options for the exchange (type, durable, auto_delete).
|
|
19
20
|
# @return [void]
|
|
20
|
-
def initialize(exchange_name, **options)
|
|
21
|
+
def initialize(exchange_name, connection: Lepus::Configuration::DEFAULT_CONNECTION, **options)
|
|
21
22
|
@exchange_name = exchange_name
|
|
23
|
+
@connection = (connection || Lepus::Configuration::DEFAULT_CONNECTION).to_sym
|
|
22
24
|
@exchange_options = DEFAULT_EXCHANGE_OPTIONS.merge(options)
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
# @return [Symbol] the named connection this publisher uses
|
|
28
|
+
attr_reader :connection
|
|
29
|
+
|
|
25
30
|
def publish(message, **options)
|
|
26
31
|
return unless Producers.exchange_enabled?(exchange_name)
|
|
27
32
|
|
|
28
|
-
Lepus.config.producer_config.with_connection do |connection|
|
|
33
|
+
Lepus.config.producer_config.with_connection(connection) do |connection|
|
|
29
34
|
connection.with_channel do |channel|
|
|
30
35
|
channel_publish(channel, message, **options)
|
|
31
36
|
end
|
|
@@ -100,6 +100,14 @@ class Lepus::LogSubscriber < ActiveSupport::LogSubscriber
|
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
def process_delivery(event)
|
|
104
|
+
info formatted_event(event, action: "Processed #{event.payload[:consumer]}", **event.payload.slice(:exchange, :routing_key, :redelivered, :result))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def consumer_error(event)
|
|
108
|
+
error formatted_event(event, action: "Error in #{event.payload[:consumer]}", **event.payload.slice(:exchange, :routing_key, :redelivered), error: formatted_error(event.payload[:error]))
|
|
109
|
+
end
|
|
110
|
+
|
|
103
111
|
private
|
|
104
112
|
|
|
105
113
|
def formatted_event(event, action:, **attributes)
|
|
@@ -45,6 +45,8 @@ module Lepus
|
|
|
45
45
|
message[:routing_key] == value
|
|
46
46
|
when :payload
|
|
47
47
|
message[:payload] == value
|
|
48
|
+
when :connection
|
|
49
|
+
message[:connection] == value&.to_sym
|
|
48
50
|
when :headers
|
|
49
51
|
message[:headers]&.any? { |k, v| value.any? { |vk, vv| k == vk && v == vv } }
|
|
50
52
|
else
|
data/lib/lepus/testing.rb
CHANGED
|
@@ -158,6 +158,10 @@ module Lepus
|
|
|
158
158
|
return unless Lepus::Producers.exchange_enabled?(exchange_name)
|
|
159
159
|
|
|
160
160
|
if Lepus::Testing.fake_publisher_enabled?
|
|
161
|
+
unless Lepus.config.connection?(connection)
|
|
162
|
+
raise Lepus::InvalidConnectionError, "Unknown connection #{connection.inspect}"
|
|
163
|
+
end
|
|
164
|
+
|
|
161
165
|
return fake_publish(message, **options)
|
|
162
166
|
end
|
|
163
167
|
|
|
@@ -186,6 +190,7 @@ module Lepus
|
|
|
186
190
|
|
|
187
191
|
fake_message = {
|
|
188
192
|
exchange: exchange_name,
|
|
193
|
+
connection: connection,
|
|
189
194
|
payload: message, # Store the original message, not the JSON string
|
|
190
195
|
routing_key: opts[:routing_key],
|
|
191
196
|
headers: opts[:headers],
|
data/lib/lepus/version.rb
CHANGED
data/lib/lepus.rb
CHANGED
|
@@ -56,6 +56,10 @@ module Lepus
|
|
|
56
56
|
class InvalidProducerConfigError < Error
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Error that is raised when a named connection is not registered.
|
|
60
|
+
class InvalidConnectionError < Error
|
|
61
|
+
end
|
|
62
|
+
|
|
59
63
|
class ShutdownError < Error
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -73,7 +77,7 @@ module Lepus
|
|
|
73
77
|
def instrument(channel, **options, &block)
|
|
74
78
|
if defined?(ActiveSupport::Notifications)
|
|
75
79
|
ActiveSupport::Notifications.instrument("#{channel}.lepus", **options, &block)
|
|
76
|
-
|
|
80
|
+
elsif block
|
|
77
81
|
yield(options.dup)
|
|
78
82
|
end
|
|
79
83
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lepus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marcos G. Zimmermann
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exec
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|