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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linter.yml +21 -0
  3. data/.github/workflows/specs.yml +93 -13
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +10 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +36 -9
  9. data/Makefile +19 -0
  10. data/README.md +562 -7
  11. data/bin/setup +5 -2
  12. data/config.ru +14 -0
  13. data/docker-compose.yml +5 -3
  14. data/docs/README.md +80 -0
  15. data/docs/cli.md +108 -0
  16. data/docs/configuration.md +171 -0
  17. data/docs/consumers.md +168 -0
  18. data/docs/getting-started.md +136 -0
  19. data/docs/images/lepus-web.png +0 -0
  20. data/docs/middleware.md +240 -0
  21. data/docs/producers.md +173 -0
  22. data/docs/prometheus.md +112 -0
  23. data/docs/rails.md +161 -0
  24. data/docs/supervisor.md +112 -0
  25. data/docs/testing.md +141 -0
  26. data/docs/web.md +85 -0
  27. data/examples/grafana-dashboard.json +450 -0
  28. data/gemfiles/Gemfile.rails-5.2 +7 -0
  29. data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
  30. data/gemfiles/Gemfile.rails-6.1 +7 -0
  31. data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
  32. data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
  33. data/gemfiles/Gemfile.rails-7.2.lock +321 -0
  34. data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
  35. data/gemfiles/Gemfile.rails-8.0.lock +322 -0
  36. data/lepus.gemspec +7 -1
  37. data/lib/lepus/cli.rb +35 -4
  38. data/lib/lepus/configuration.rb +107 -0
  39. data/lib/lepus/connection_pool.rb +135 -0
  40. data/lib/lepus/consumer.rb +59 -41
  41. data/lib/lepus/consumers/config.rb +183 -0
  42. data/lib/lepus/consumers/handler.rb +56 -0
  43. data/lib/lepus/consumers/middleware_chain.rb +22 -0
  44. data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
  45. data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
  46. data/lib/lepus/consumers/middlewares/json.rb +37 -0
  47. data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
  48. data/lib/lepus/consumers/middlewares/unique.rb +65 -0
  49. data/lib/lepus/consumers/stats.rb +70 -0
  50. data/lib/lepus/consumers/stats_registry.rb +29 -0
  51. data/lib/lepus/consumers/worker.rb +141 -0
  52. data/lib/lepus/consumers/worker_factory.rb +124 -0
  53. data/lib/lepus/consumers.rb +6 -0
  54. data/lib/lepus/message/delivery_info.rb +72 -0
  55. data/lib/lepus/message/metadata.rb +99 -0
  56. data/lib/lepus/message.rb +88 -5
  57. data/lib/lepus/middleware_chain.rb +83 -0
  58. data/lib/lepus/primitive/hash.rb +29 -0
  59. data/lib/lepus/process.rb +24 -24
  60. data/lib/lepus/process_registry/backend.rb +49 -0
  61. data/lib/lepus/process_registry/file_backend.rb +108 -0
  62. data/lib/lepus/process_registry/message_builder.rb +72 -0
  63. data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
  64. data/lib/lepus/process_registry.rb +56 -23
  65. data/lib/lepus/processes/base.rb +0 -5
  66. data/lib/lepus/processes/callbacks.rb +3 -0
  67. data/lib/lepus/processes/interruptible.rb +4 -8
  68. data/lib/lepus/processes/procline.rb +1 -1
  69. data/lib/lepus/processes/registrable.rb +1 -1
  70. data/lib/lepus/processes/runnable.rb +1 -1
  71. data/lib/lepus/processes.rb +15 -0
  72. data/lib/lepus/producer.rb +141 -30
  73. data/lib/lepus/producers/config.rb +46 -0
  74. data/lib/lepus/producers/definition.rb +48 -0
  75. data/lib/lepus/producers/hooks.rb +170 -0
  76. data/lib/lepus/producers/middleware_chain.rb +22 -0
  77. data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
  78. data/lib/lepus/producers/middlewares/header.rb +47 -0
  79. data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
  80. data/lib/lepus/producers/middlewares/json.rb +47 -0
  81. data/lib/lepus/producers/middlewares/unique.rb +67 -0
  82. data/lib/lepus/producers.rb +7 -0
  83. data/lib/lepus/prometheus/collector.rb +149 -0
  84. data/lib/lepus/prometheus/instrumentation.rb +168 -0
  85. data/lib/lepus/prometheus.rb +48 -0
  86. data/lib/lepus/publisher.rb +67 -0
  87. data/lib/lepus/supervisor/children_pipes.rb +25 -0
  88. data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
  89. data/lib/lepus/supervisor/pidfiled.rb +1 -1
  90. data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
  91. data/lib/lepus/supervisor.rb +129 -25
  92. data/lib/lepus/testing/exchange.rb +95 -0
  93. data/lib/lepus/testing/message_builder.rb +177 -0
  94. data/lib/lepus/testing/rspec_matchers.rb +258 -0
  95. data/lib/lepus/testing.rb +210 -0
  96. data/lib/lepus/unique.rb +18 -0
  97. data/lib/lepus/version.rb +1 -1
  98. data/lib/lepus/web/aggregator.rb +154 -0
  99. data/lib/lepus/web/api.rb +132 -0
  100. data/lib/lepus/web/app.rb +37 -0
  101. data/lib/lepus/web/management_api.rb +192 -0
  102. data/lib/lepus/web/respond_with.rb +28 -0
  103. data/lib/lepus/web.rb +238 -0
  104. data/lib/lepus.rb +39 -28
  105. data/test_offline.html +189 -0
  106. data/web/assets/css/styles.css +635 -0
  107. data/web/assets/js/app.js +6 -0
  108. data/web/assets/js/bootstrap.js +20 -0
  109. data/web/assets/js/controllers/connection_controller.js +44 -0
  110. data/web/assets/js/controllers/dashboard_controller.js +499 -0
  111. data/web/assets/js/controllers/queue_controller.js +17 -0
  112. data/web/assets/js/controllers/theme_controller.js +31 -0
  113. data/web/assets/js/offline-manager.js +233 -0
  114. data/web/assets/js/service-worker-manager.js +65 -0
  115. data/web/index.html +159 -0
  116. data/web/sw.js +144 -0
  117. metadata +177 -18
  118. data/lib/lepus/consumer_config.rb +0 -149
  119. data/lib/lepus/consumer_wrapper.rb +0 -46
  120. data/lib/lepus/lifecycle_hooks.rb +0 -49
  121. data/lib/lepus/middlewares/honeybadger.rb +0 -23
  122. data/lib/lepus/middlewares/json.rb +0 -35
  123. data/lib/lepus/middlewares/max_retry.rb +0 -57
  124. data/lib/lepus/processes/consumer.rb +0 -113
  125. 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