lepus 0.0.1.beta2 → 0.1.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 +4 -4
- data/.github/workflows/linter.yml +21 -0
- data/.github/workflows/specs.yml +93 -13
- data/.gitignore +2 -0
- data/.rubocop.yml +10 -0
- data/.tool-versions +1 -1
- data/Gemfile +7 -0
- data/Gemfile.lock +36 -9
- data/Makefile +19 -0
- data/README.md +562 -7
- data/bin/setup +5 -2
- data/config.ru +14 -0
- data/docker-compose.yml +5 -3
- data/docs/README.md +80 -0
- data/docs/cli.md +108 -0
- data/docs/configuration.md +171 -0
- data/docs/consumers.md +168 -0
- data/docs/getting-started.md +136 -0
- data/docs/images/lepus-web.png +0 -0
- data/docs/middleware.md +240 -0
- data/docs/producers.md +173 -0
- data/docs/prometheus.md +112 -0
- data/docs/rails.md +161 -0
- data/docs/supervisor.md +112 -0
- data/docs/testing.md +141 -0
- data/docs/web.md +85 -0
- data/examples/grafana-dashboard.json +450 -0
- data/gemfiles/Gemfile.rails-5.2 +7 -0
- data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
- data/gemfiles/Gemfile.rails-6.1 +7 -0
- data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
- data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
- data/gemfiles/Gemfile.rails-7.2.lock +321 -0
- data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +322 -0
- data/lepus.gemspec +7 -1
- data/lib/lepus/cli.rb +35 -4
- data/lib/lepus/configuration.rb +107 -0
- data/lib/lepus/connection_pool.rb +135 -0
- data/lib/lepus/consumer.rb +59 -41
- data/lib/lepus/consumers/config.rb +183 -0
- data/lib/lepus/consumers/handler.rb +56 -0
- data/lib/lepus/consumers/middleware_chain.rb +22 -0
- data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
- data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
- data/lib/lepus/consumers/middlewares/json.rb +37 -0
- data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
- data/lib/lepus/consumers/middlewares/unique.rb +65 -0
- data/lib/lepus/consumers/stats.rb +70 -0
- data/lib/lepus/consumers/stats_registry.rb +29 -0
- data/lib/lepus/consumers/worker.rb +141 -0
- data/lib/lepus/consumers/worker_factory.rb +124 -0
- data/lib/lepus/consumers.rb +6 -0
- data/lib/lepus/message/delivery_info.rb +72 -0
- data/lib/lepus/message/metadata.rb +99 -0
- data/lib/lepus/message.rb +88 -5
- data/lib/lepus/middleware_chain.rb +83 -0
- data/lib/lepus/primitive/hash.rb +29 -0
- data/lib/lepus/process.rb +24 -24
- data/lib/lepus/process_registry/backend.rb +49 -0
- data/lib/lepus/process_registry/file_backend.rb +108 -0
- data/lib/lepus/process_registry/message_builder.rb +72 -0
- data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
- data/lib/lepus/process_registry.rb +56 -23
- data/lib/lepus/processes/base.rb +0 -5
- data/lib/lepus/processes/callbacks.rb +3 -0
- data/lib/lepus/processes/interruptible.rb +4 -8
- data/lib/lepus/processes/procline.rb +1 -1
- data/lib/lepus/processes/registrable.rb +1 -1
- data/lib/lepus/processes/runnable.rb +1 -1
- data/lib/lepus/processes.rb +15 -0
- data/lib/lepus/producer.rb +141 -30
- data/lib/lepus/producers/config.rb +46 -0
- data/lib/lepus/producers/definition.rb +48 -0
- data/lib/lepus/producers/hooks.rb +170 -0
- data/lib/lepus/producers/middleware_chain.rb +22 -0
- data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
- data/lib/lepus/producers/middlewares/header.rb +47 -0
- data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
- data/lib/lepus/producers/middlewares/json.rb +47 -0
- data/lib/lepus/producers/middlewares/unique.rb +67 -0
- data/lib/lepus/producers.rb +7 -0
- data/lib/lepus/prometheus/collector.rb +149 -0
- data/lib/lepus/prometheus/instrumentation.rb +168 -0
- data/lib/lepus/prometheus.rb +48 -0
- data/lib/lepus/publisher.rb +67 -0
- data/lib/lepus/supervisor/children_pipes.rb +25 -0
- data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
- data/lib/lepus/supervisor/pidfiled.rb +1 -1
- data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
- data/lib/lepus/supervisor.rb +129 -25
- data/lib/lepus/testing/exchange.rb +95 -0
- data/lib/lepus/testing/message_builder.rb +177 -0
- data/lib/lepus/testing/rspec_matchers.rb +258 -0
- data/lib/lepus/testing.rb +210 -0
- data/lib/lepus/unique.rb +18 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus/web/aggregator.rb +154 -0
- data/lib/lepus/web/api.rb +132 -0
- data/lib/lepus/web/app.rb +37 -0
- data/lib/lepus/web/management_api.rb +192 -0
- data/lib/lepus/web/respond_with.rb +28 -0
- data/lib/lepus/web.rb +238 -0
- data/lib/lepus.rb +39 -28
- data/test_offline.html +189 -0
- data/web/assets/css/styles.css +635 -0
- data/web/assets/js/app.js +6 -0
- data/web/assets/js/bootstrap.js +20 -0
- data/web/assets/js/controllers/connection_controller.js +44 -0
- data/web/assets/js/controllers/dashboard_controller.js +499 -0
- data/web/assets/js/controllers/queue_controller.js +17 -0
- data/web/assets/js/controllers/theme_controller.js +31 -0
- data/web/assets/js/offline-manager.js +233 -0
- data/web/assets/js/service-worker-manager.js +65 -0
- data/web/index.html +159 -0
- data/web/sw.js +144 -0
- metadata +177 -18
- data/lib/lepus/consumer_config.rb +0 -149
- data/lib/lepus/consumer_wrapper.rb +0 -46
- data/lib/lepus/lifecycle_hooks.rb +0 -49
- data/lib/lepus/middlewares/honeybadger.rb +0 -23
- data/lib/lepus/middlewares/json.rb +0 -35
- data/lib/lepus/middlewares/max_retry.rb +0 -57
- data/lib/lepus/processes/consumer.rb +0 -113
- data/lib/lepus/supervisor/config.rb +0 -45
data/docs/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# lepus
|
|
2
|
+
|
|
3
|
+
RabbitMQ-backed producer/consumer framework for Ruby — with a supervisor, middleware chains, a CLI, and a live web dashboard.
|
|
4
|
+
|
|
5
|
+
Think Sidekiq or SolidQueue, but on top of RabbitMQ rather than Redis or a database. Lepus handles the operational concerns (process supervision, graceful shutdown, connection pooling, signal handling, per-worker pools) so your application code can stay focused on "what does this message do".
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Getting started](getting-started.md) — install, define your first consumer and producer, run them
|
|
10
|
+
- [Configuration](configuration.md) — the full `Lepus.configure` DSL
|
|
11
|
+
- [Consumers](consumers.md) — queue bindings, lifecycle, result codes, retries
|
|
12
|
+
- [Producers](producers.md) — exchanges, publishing, enable/disable hooks
|
|
13
|
+
- [Middleware](middleware.md) — built-in middlewares and how to write your own
|
|
14
|
+
- [CLI](cli.md) — `lepus start`, `lepus web`
|
|
15
|
+
- [Supervisor](supervisor.md) — process model, signals, graceful shutdown
|
|
16
|
+
- [Web dashboard](web.md) — the monitoring UI
|
|
17
|
+
- [Testing](testing.md) — testing consumers and producers
|
|
18
|
+
- [Rails integration](rails.md) — Railtie, executor wrapping, Puma plugin
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Gemfile
|
|
24
|
+
gem 'lepus'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## One-minute tour
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# config/initializers/lepus.rb
|
|
31
|
+
Lepus.configure do |config|
|
|
32
|
+
config.rabbitmq_url = ENV.fetch('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672')
|
|
33
|
+
config.connection_name = 'my-service'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# app/consumers/orders_consumer.rb
|
|
37
|
+
class OrdersConsumer < Lepus::Consumer
|
|
38
|
+
configure(
|
|
39
|
+
queue: 'orders',
|
|
40
|
+
exchange: { name: 'orders', type: :topic, durable: true },
|
|
41
|
+
routing_key: ['order.*']
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
use :json, symbolize_keys: true
|
|
45
|
+
use :max_retry, retries: 5
|
|
46
|
+
|
|
47
|
+
def perform(message)
|
|
48
|
+
Order.create!(message.payload)
|
|
49
|
+
:ack
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# app/producers/orders_producer.rb
|
|
54
|
+
class OrdersProducer < Lepus::Producer
|
|
55
|
+
configure(exchange: { name: 'orders', type: :topic, durable: true })
|
|
56
|
+
use :json
|
|
57
|
+
use :correlation_id
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Run the consumer:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bundle exec lepus start OrdersConsumer
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Publish from anywhere in your app:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
OrdersProducer.publish({ order_id: 42, total: 99.99 }, routing_key: 'order.created')
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Version & dependencies
|
|
74
|
+
|
|
75
|
+
- Ruby: `>= 2.7.0`
|
|
76
|
+
- Runtime: `bunny`, `thor`, `zeitwerk`, `concurrent-ruby`, `multi_json`
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT.
|
data/docs/cli.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
The `lepus` executable is the main operational entry point. It's a Thor-based CLI with two subcommands.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bundle exec lepus <command> [args] [options]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## `lepus start`
|
|
10
|
+
|
|
11
|
+
Boot the supervisor and run the specified consumers.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle exec lepus start [CONSUMER_CLASSES...] [options]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If no classes are specified **and** `config.consumers_directory` is set, every consumer class discovered in that directory is started.
|
|
18
|
+
|
|
19
|
+
### Options
|
|
20
|
+
|
|
21
|
+
| Flag | Default | Purpose |
|
|
22
|
+
|------|---------|---------|
|
|
23
|
+
| `--debug` | `false` | Set logger level to DEBUG. |
|
|
24
|
+
| `--logfile PATH` | stdout | Write logs to a file. |
|
|
25
|
+
| `--pidfile PATH` | none | Write the supervisor PID to a file. |
|
|
26
|
+
| `--require_file PATH`, `-r PATH` | none | Require this file before starting (e.g. `config/environment.rb`). |
|
|
27
|
+
|
|
28
|
+
### Examples
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# One consumer, no framework auto-load
|
|
32
|
+
bundle exec lepus start OrdersConsumer
|
|
33
|
+
|
|
34
|
+
# Multiple consumers, Rails env loaded first
|
|
35
|
+
bundle exec lepus start OrdersConsumer PaymentsConsumer \
|
|
36
|
+
--require_file config/environment.rb
|
|
37
|
+
|
|
38
|
+
# Auto-load everything under config.consumers_directory
|
|
39
|
+
bundle exec lepus start
|
|
40
|
+
|
|
41
|
+
# Debug mode + pidfile (typical for a systemd unit)
|
|
42
|
+
bundle exec lepus start OrdersConsumer \
|
|
43
|
+
--require_file config/environment.rb \
|
|
44
|
+
--pidfile /var/run/lepus.pid \
|
|
45
|
+
--logfile /var/log/lepus.log \
|
|
46
|
+
--debug
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### What happens at start
|
|
50
|
+
|
|
51
|
+
1. Load `--require_file` if given.
|
|
52
|
+
2. Resolve consumer classes (CLI args or auto-discovered).
|
|
53
|
+
3. Group consumers by their `process.name` (defaulting to `:default`).
|
|
54
|
+
4. Fork one worker subprocess per group. Parent becomes the supervisor.
|
|
55
|
+
5. Each worker opens a Bunny channel, declares queues/exchanges/bindings, and starts consuming with its configured thread pool.
|
|
56
|
+
6. Supervisor monitors children via pipes and restarts any that crash.
|
|
57
|
+
|
|
58
|
+
See [supervisor.md](supervisor.md) for the full lifecycle.
|
|
59
|
+
|
|
60
|
+
## `lepus web`
|
|
61
|
+
|
|
62
|
+
Run the web dashboard.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bundle exec lepus web [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Options
|
|
69
|
+
|
|
70
|
+
| Flag | Default | Purpose |
|
|
71
|
+
|------|---------|---------|
|
|
72
|
+
| `--port PORT`, `-p PORT` | `9292` | Port to bind. |
|
|
73
|
+
| `--host HOST`, `-o HOST` | `0.0.0.0` | Host to bind. |
|
|
74
|
+
|
|
75
|
+
### Example
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bundle exec lepus web --port 9292 --host 0.0.0.0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Visit http://localhost:9292.
|
|
82
|
+
|
|
83
|
+
The web UI reads from the process registry backend (`config.process_registry_backend`). For multi-host visibility, set the backend to `:rabbitmq`. See [web.md](web.md).
|
|
84
|
+
|
|
85
|
+
### Mounting in Rails
|
|
86
|
+
|
|
87
|
+
Instead of running the CLI, you can mount the web app directly:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# config/routes.rb
|
|
91
|
+
require 'lepus/web'
|
|
92
|
+
mount Lepus::Web::App, at: '/lepus'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See [rails.md](rails.md) for authorization patterns.
|
|
96
|
+
|
|
97
|
+
## Exit codes
|
|
98
|
+
|
|
99
|
+
- `0` — graceful shutdown (SIGTERM / SIGINT).
|
|
100
|
+
- Non-zero — unrecoverable error at boot (config invalid, RabbitMQ unreachable, etc.).
|
|
101
|
+
|
|
102
|
+
Workers that crash after boot are restarted by the supervisor; the supervisor itself only exits on an unrecoverable event or a requested shutdown.
|
|
103
|
+
|
|
104
|
+
## Environment variables
|
|
105
|
+
|
|
106
|
+
- `RABBITMQ_URL` — fallback for `config.rabbitmq_url` if not explicitly set.
|
|
107
|
+
|
|
108
|
+
Everything else is configured via `Lepus.configure`. See [configuration.md](configuration.md).
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Lepus is configured via a single block:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Lepus.configure do |config|
|
|
7
|
+
# ... see below ...
|
|
8
|
+
end
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The configuration is mutable at boot and should be set once. Values are read lazily — most only matter at worker-start time.
|
|
12
|
+
|
|
13
|
+
## Connection
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Lepus.configure do |config|
|
|
17
|
+
config.rabbitmq_url = ENV.fetch('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672')
|
|
18
|
+
config.connection_name = 'my-service'
|
|
19
|
+
config.recovery_attempts = 10 # nil = infinite
|
|
20
|
+
config.recover_from_connection_close = true
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Key | Default | Purpose |
|
|
25
|
+
|-----|---------|---------|
|
|
26
|
+
| `rabbitmq_url` | `ENV['RABBITMQ_URL']` or `amqp://guest:guest@localhost:5672` | Connection string |
|
|
27
|
+
| `connection_name` | gem-generated | Shown in RabbitMQ management UI — set to your service name |
|
|
28
|
+
| `recovery_attempts` | `10` | Max automatic reconnects; `nil` for infinite |
|
|
29
|
+
| `recover_from_connection_close` | `true` | Auto-recover after a clean close |
|
|
30
|
+
|
|
31
|
+
## Application metadata
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
config.application_name = 'orders-service'
|
|
35
|
+
config.management_api_url = 'http://rabbitmq:15672' # only for :rabbitmq registry backend
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Shown in the web dashboard. `management_api_url` is only required if `process_registry_backend = :rabbitmq`.
|
|
39
|
+
|
|
40
|
+
## Consumer discovery
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
config.consumers_directory = 'app/consumers'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When present, `lepus start` with no class arguments auto-loads every class under this directory and runs all configured consumers.
|
|
47
|
+
|
|
48
|
+
## Worker pools
|
|
49
|
+
|
|
50
|
+
A **worker** is a subprocess the supervisor forks. Consumers are grouped into workers by their `process.name`.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
config.worker(:default) do |w|
|
|
54
|
+
w.pool_size = 5 # threads per worker
|
|
55
|
+
w.pool_timeout = 10.0 # seconds before yielding to another task
|
|
56
|
+
|
|
57
|
+
w.before_fork { ActiveRecord::Base.connection_handler.clear_all_connections! }
|
|
58
|
+
w.after_fork { ActiveRecord::Base.establish_connection }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
config.worker(:high_priority, pool_size: 10)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Key | Default | Purpose |
|
|
65
|
+
|-----|---------|---------|
|
|
66
|
+
| `pool_size` | `2` | Max threads per worker process |
|
|
67
|
+
| `pool_timeout` | `10.0` | Seconds threads wait on queue checkout |
|
|
68
|
+
| `before_fork` | no-op | Block run in parent before fork — close sockets, drop DB connections, etc. |
|
|
69
|
+
| `after_fork` | no-op | Block run in child after fork — reconnect, reseed RNG, etc. |
|
|
70
|
+
|
|
71
|
+
Assign consumers to a named worker via their own `configure(process: { name: :high_priority })` (see [consumers.md](consumers.md)).
|
|
72
|
+
|
|
73
|
+
## Producer pool
|
|
74
|
+
|
|
75
|
+
Producers share a connection pool:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
config.producer do |p|
|
|
79
|
+
p.pool_size = 5
|
|
80
|
+
p.pool_timeout = 5.0
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Global middleware
|
|
85
|
+
|
|
86
|
+
Middleware chains run around every message published or consumed, in addition to any per-producer or per-consumer `use` calls.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
config.producer_middlewares do |chain|
|
|
90
|
+
chain.use :instrumentation
|
|
91
|
+
chain.use :correlation_id
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
config.consumer_middlewares do |chain|
|
|
95
|
+
chain.use :json, symbolize_keys: true
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [middleware.md](middleware.md).
|
|
100
|
+
|
|
101
|
+
## Process registry
|
|
102
|
+
|
|
103
|
+
The supervisor keeps a registry of running processes, visible to the web dashboard.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
config.process_registry_backend = :file # or :rabbitmq
|
|
107
|
+
config.process_heartbeat_interval = 60 # seconds
|
|
108
|
+
config.process_alive_threshold = 5 * 60 # seconds
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `:file` — metadata in local files under `/tmp/lepus/...`. Single-host.
|
|
112
|
+
- `:rabbitmq` — metadata in RabbitMQ itself; multi-host visibility in the web dashboard.
|
|
113
|
+
|
|
114
|
+
## Rails integration
|
|
115
|
+
|
|
116
|
+
When Rails is loaded, a Railtie wires:
|
|
117
|
+
|
|
118
|
+
- `config.app_executor = Rails.application.executor` automatically
|
|
119
|
+
- `config.logger = Rails.logger`
|
|
120
|
+
|
|
121
|
+
You can override either:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
config.app_executor = nil # disable executor wrapping
|
|
125
|
+
config.logger = MyLogger.new
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
See [rails.md](rails.md).
|
|
129
|
+
|
|
130
|
+
## Threading error handler
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
config.on_thread_error = ->(exception) {
|
|
134
|
+
Rails.error.report(exception)
|
|
135
|
+
Honeybadger.notify(exception)
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Called when a worker thread raises. Does not stop the worker.
|
|
140
|
+
|
|
141
|
+
## Logger
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
config.logger = Logger.new($stdout)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Default: `Logger.new($stdout)` (or `Rails.logger` if Rails is present). The `--debug` CLI flag sets the level to DEBUG.
|
|
148
|
+
|
|
149
|
+
## Full example
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
Lepus.configure do |config|
|
|
153
|
+
config.rabbitmq_url = ENV.fetch('RABBITMQ_URL')
|
|
154
|
+
config.connection_name = 'orders-service'
|
|
155
|
+
config.application_name = 'orders-service'
|
|
156
|
+
config.consumers_directory = 'app/consumers'
|
|
157
|
+
|
|
158
|
+
config.worker(:default) do |w|
|
|
159
|
+
w.pool_size = 5
|
|
160
|
+
w.before_fork { ActiveRecord::Base.connection_handler.clear_all_connections! }
|
|
161
|
+
w.after_fork { ActiveRecord::Base.establish_connection }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
config.producer_middlewares do |chain|
|
|
165
|
+
chain.use :instrumentation
|
|
166
|
+
chain.use :correlation_id
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
config.on_thread_error = ->(exc) { Honeybadger.notify(exc) }
|
|
170
|
+
end
|
|
171
|
+
```
|
data/docs/consumers.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Consumers
|
|
2
|
+
|
|
3
|
+
Consumers subscribe to a queue, receive messages, and process them. A consumer is a subclass of `Lepus::Consumer` with a `configure` call and a `perform` method.
|
|
4
|
+
|
|
5
|
+
## Minimal example
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class OrdersConsumer < Lepus::Consumer
|
|
9
|
+
configure(
|
|
10
|
+
queue: 'orders',
|
|
11
|
+
exchange: { name: 'orders', type: :topic, durable: true },
|
|
12
|
+
routing_key: ['order.*']
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
use :json, symbolize_keys: true
|
|
16
|
+
|
|
17
|
+
def perform(message)
|
|
18
|
+
Order.create!(message.payload)
|
|
19
|
+
:ack
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## `configure` DSL
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
configure(
|
|
28
|
+
queue: 'orders', # string or hash
|
|
29
|
+
exchange: { name: 'orders', type: :topic, durable: true },
|
|
30
|
+
routing_key: ['order.*', 'invoice.created'], # array or string
|
|
31
|
+
prefetch: 1, # channel QoS
|
|
32
|
+
retry_queue: { delay: 5_000 }, # or true, or false
|
|
33
|
+
error_queue: true, # or queue name, or false
|
|
34
|
+
process: { name: :default, threads: 5 }, # worker assignment
|
|
35
|
+
channel: { pool_size: 1, shutdown_timeout: 60, abort_on_exception: false }
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Key | Purpose |
|
|
40
|
+
|-----|---------|
|
|
41
|
+
| `queue` | The queue name — string, or a hash for advanced options (`name`, `durable`, `arguments`, …). |
|
|
42
|
+
| `exchange` | Bind to this exchange. String, or hash (`name`, `type`, `durable`, `auto_delete`). |
|
|
43
|
+
| `routing_key` | Binding routing key(s). |
|
|
44
|
+
| `prefetch` | Channel QoS (`basic.qos`). Leave at `1` unless you know you need more. |
|
|
45
|
+
| `retry_queue` | When truthy, sets up a retry queue with a delay exchange. Messages requeued via `:requeue` or `max_retry` go here. |
|
|
46
|
+
| `error_queue` | Queue name (string) or `true` to auto-name. Messages that exhaust retries land here. |
|
|
47
|
+
| `process.name` | The named worker pool this consumer runs in — see `Lepus.configure.worker(:name)`. |
|
|
48
|
+
| `process.threads` | Threads inside the consumer's worker dedicated to this consumer. |
|
|
49
|
+
|
|
50
|
+
## The `perform` method
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
def perform(message)
|
|
54
|
+
# ...
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`message` is a `Lepus::Message` with:
|
|
59
|
+
|
|
60
|
+
- `#payload` — the decoded body (raw bytes, or parsed data if a decoding middleware is in the chain).
|
|
61
|
+
- `#delivery_info` — `#exchange`, `#routing_key`, `#redelivered?`, `#delivery_tag`, `#consumer_tag`.
|
|
62
|
+
- `#metadata` — `#headers`, `#content_type`, `#content_encoding`, `#correlation_id`, `#message_id`, `#reply_to`, `#type`, `#timestamp`, `#user_id`, `#app_id`, `#priority`.
|
|
63
|
+
|
|
64
|
+
Return a disposition symbol or call one of the helpers:
|
|
65
|
+
|
|
66
|
+
| Return | Helper | Effect |
|
|
67
|
+
|--------|--------|--------|
|
|
68
|
+
| `:ack` | `ack!` | Acknowledge the message. |
|
|
69
|
+
| `:reject` | `reject!` | Reject without requeue (drops, or goes to DLX if configured). |
|
|
70
|
+
| `:requeue` | `requeue!` | Reject with requeue — back of the queue. |
|
|
71
|
+
| `:nack` | `nack!` | Negative-ack. |
|
|
72
|
+
|
|
73
|
+
A `perform` that returns nothing implicitly acks. A `perform` that raises is logged, `on_thread_error` fires, and the message is rejected.
|
|
74
|
+
|
|
75
|
+
## Middleware
|
|
76
|
+
|
|
77
|
+
Add middlewares to a consumer with `use`:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class OrdersConsumer < Lepus::Consumer
|
|
81
|
+
use :json, symbolize_keys: true
|
|
82
|
+
use :max_retry, retries: 5, error_queue: 'orders.error'
|
|
83
|
+
use :exception_logger
|
|
84
|
+
use :honeybadger
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Built-in middlewares:
|
|
89
|
+
|
|
90
|
+
| Name | Purpose |
|
|
91
|
+
|------|---------|
|
|
92
|
+
| `:json` | Parse JSON body into a hash. |
|
|
93
|
+
| `:max_retry` | Track `x-death` headers; after N retries, route to error queue. |
|
|
94
|
+
| `:exception_logger` | Log unhandled exceptions with backtrace. |
|
|
95
|
+
| `:honeybadger` | Notify Honeybadger on exceptions. |
|
|
96
|
+
| `:unique` | Idempotent dedupe by `correlation_id` (requires storage). |
|
|
97
|
+
|
|
98
|
+
Write your own — see [middleware.md](middleware.md).
|
|
99
|
+
|
|
100
|
+
## Error handling
|
|
101
|
+
|
|
102
|
+
A `perform` that raises:
|
|
103
|
+
|
|
104
|
+
1. The message is rejected by default.
|
|
105
|
+
2. `config.on_thread_error` is called with the exception.
|
|
106
|
+
3. If `:exception_logger` middleware is in the chain, it logs with backtrace.
|
|
107
|
+
4. If `:max_retry` is in the chain, it tracks retry count via RabbitMQ's `x-death` header and redirects to an error queue after N rejections.
|
|
108
|
+
|
|
109
|
+
## Retries
|
|
110
|
+
|
|
111
|
+
The built-in retry pattern uses RabbitMQ's dead-letter machinery:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
configure(
|
|
115
|
+
queue: 'orders',
|
|
116
|
+
exchange: { name: 'orders', type: :topic, durable: true },
|
|
117
|
+
routing_key: 'order.*',
|
|
118
|
+
retry_queue: { delay: 5_000 }, # 5-second delay before retry
|
|
119
|
+
error_queue: 'orders.error'
|
|
120
|
+
)
|
|
121
|
+
use :max_retry, retries: 5, error_queue: 'orders.error'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
On rejection:
|
|
125
|
+
|
|
126
|
+
1. `max_retry` inspects `x-death` headers.
|
|
127
|
+
2. If retries are below the limit, it re-publishes to the retry queue.
|
|
128
|
+
3. After TTL, the retry queue dead-letters back to the original queue for another attempt.
|
|
129
|
+
4. After N failures, the middleware routes to the error queue.
|
|
130
|
+
|
|
131
|
+
## Worker assignment
|
|
132
|
+
|
|
133
|
+
Assign a consumer to a named worker pool:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
configure(
|
|
137
|
+
queue: 'orders',
|
|
138
|
+
exchange: { name: 'orders', type: :topic },
|
|
139
|
+
process: { name: :high_priority, threads: 10 }
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then in the global config:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
Lepus.configure do |config|
|
|
147
|
+
config.worker(:high_priority) do |w|
|
|
148
|
+
w.pool_size = 20
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Consumers assigned to the same worker share a single subprocess.
|
|
154
|
+
|
|
155
|
+
## Logging
|
|
156
|
+
|
|
157
|
+
Inside `perform`, `logger` is the Lepus logger (tagged with the consumer class name):
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
def perform(message)
|
|
161
|
+
logger.info("processing #{message.delivery_info.routing_key}")
|
|
162
|
+
# ...
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
See [testing.md](testing.md) for the test-mode helpers.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
# Gemfile
|
|
7
|
+
gem 'lepus'
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bundle install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
You'll also need a running RabbitMQ instance. For local dev:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
docker run -d --rm -p 5672:5672 -p 15672:15672 rabbitmq:3-management
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configure
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# config/initializers/lepus.rb (Rails) or your bootstrap file
|
|
24
|
+
Lepus.configure do |config|
|
|
25
|
+
config.rabbitmq_url = ENV.fetch('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672')
|
|
26
|
+
config.connection_name = 'my-service'
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See [configuration.md](configuration.md) for the full DSL.
|
|
31
|
+
|
|
32
|
+
## Define a consumer
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# app/consumers/orders_consumer.rb
|
|
36
|
+
class OrdersConsumer < Lepus::Consumer
|
|
37
|
+
configure(
|
|
38
|
+
queue: 'orders',
|
|
39
|
+
exchange: { name: 'orders', type: :topic, durable: true },
|
|
40
|
+
routing_key: ['order.*']
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
use :json, symbolize_keys: true
|
|
44
|
+
|
|
45
|
+
def perform(message)
|
|
46
|
+
# message.payload => the decoded JSON body (a hash, due to the :json middleware)
|
|
47
|
+
# message.delivery_info => exchange, routing_key, redelivered?
|
|
48
|
+
# message.metadata => headers, content_type, correlation_id, etc.
|
|
49
|
+
Order.create!(message.payload)
|
|
50
|
+
:ack
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `perform` method returns a symbol indicating disposition:
|
|
56
|
+
|
|
57
|
+
- `:ack` — acknowledge; RabbitMQ removes the message.
|
|
58
|
+
- `:reject` — reject; message is dropped (or routed to a dead-letter exchange if configured).
|
|
59
|
+
- `:requeue` — reject and requeue for another delivery attempt.
|
|
60
|
+
- `:nack` — negative-ack without requeue (similar to `:reject`).
|
|
61
|
+
|
|
62
|
+
`ack!`, `reject!`, `requeue!`, `nack!` are also available as helper methods.
|
|
63
|
+
|
|
64
|
+
## Define a producer
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# app/producers/orders_producer.rb
|
|
68
|
+
class OrdersProducer < Lepus::Producer
|
|
69
|
+
configure(
|
|
70
|
+
exchange: { name: 'orders', type: :topic, durable: true },
|
|
71
|
+
publish: { persistent: true }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
use :json
|
|
75
|
+
use :correlation_id
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Publish:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
OrdersProducer.publish(
|
|
83
|
+
{ order_id: 42, total: 99.99 },
|
|
84
|
+
routing_key: 'order.created'
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Run
|
|
89
|
+
|
|
90
|
+
### One-off (development)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bundle exec lepus start OrdersConsumer
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Flags worth knowing:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bundle exec lepus start OrdersConsumer PaymentsConsumer \
|
|
100
|
+
--require_file config/environment.rb \
|
|
101
|
+
--debug
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See [cli.md](cli.md).
|
|
105
|
+
|
|
106
|
+
### As a long-running service
|
|
107
|
+
|
|
108
|
+
In production, run `lepus start` with all your consumer classes (or auto-load them — see below) under your favorite process supervisor (systemd, Foreman, Kamal, Kubernetes).
|
|
109
|
+
|
|
110
|
+
Auto-load consumers from a directory:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
Lepus.configure do |config|
|
|
114
|
+
config.consumers_directory = 'app/consumers'
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bundle exec lepus start # with no class args, starts all consumers from consumers_directory
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Monitor
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
bundle exec lepus web --port 9292
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Visit http://localhost:9292 to see consumer status, throughput, and recent activity. See [web.md](web.md).
|
|
129
|
+
|
|
130
|
+
## Next steps
|
|
131
|
+
|
|
132
|
+
- [Consumers](consumers.md) — full consumer DSL, retries, error handling
|
|
133
|
+
- [Producers](producers.md) — exchange config, publishing options, hooks
|
|
134
|
+
- [Middleware](middleware.md) — built-in middlewares and writing your own
|
|
135
|
+
- [Supervisor](supervisor.md) — process model, graceful shutdown, worker pools
|
|
136
|
+
- [Rails integration](rails.md) — Railtie, executor wrapping
|
|
Binary file
|