lepus 0.2.2 → 0.3.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/CHANGELOG.md +25 -0
- data/Gemfile.lock +3 -3
- data/README.md +104 -9
- data/bin/web-demo +592 -0
- data/docs/brand/BRAND.md +67 -0
- data/docs/brand/apple-touch-icon.png +0 -0
- data/docs/brand/favicon-16.png +0 -0
- data/docs/brand/favicon-32.png +0 -0
- data/docs/brand/favicon.ico +0 -0
- data/docs/brand/favicon.svg +1 -0
- data/docs/brand/icon-512.png +0 -0
- data/docs/brand/lockup-lepus-mono.svg +1 -0
- data/docs/brand/lockup-lepus-on-dark.svg +1 -0
- data/docs/brand/lockup-lepus-on-light.svg +1 -0
- data/docs/brand/lockup-lepus-saga-mono.svg +1 -0
- data/docs/brand/lockup-lepus-saga-on-dark.svg +1 -0
- data/docs/brand/lockup-lepus-saga-on-light.svg +1 -0
- data/docs/brand/lockup-lepus-saga.svg +1 -0
- data/docs/brand/lockup-lepus.svg +1 -0
- data/docs/brand/mark-mono.svg +1 -0
- data/docs/brand/mark-on-dark.svg +1 -0
- data/docs/brand/mark-on-light.svg +1 -0
- data/docs/brand/mark-small-mono.svg +1 -0
- data/docs/brand/mark-small-on-dark.svg +1 -0
- data/docs/brand/mark-small-on-light.svg +1 -0
- data/docs/brand/mark-small.svg +1 -0
- data/docs/brand/mark.svg +1 -0
- data/docs/configuration.md +1 -0
- data/docs/consumers.md +39 -5
- data/docs/getting-started.md +1 -1
- data/docs/images/lepus-web.png +0 -0
- data/docs/middleware.md +1 -1
- data/docs/producers.md +41 -0
- data/docs/testing.md +67 -48
- data/docs/web.md +39 -13
- data/gemfiles/Gemfile.rails-5.2.lock +2 -2
- data/gemfiles/Gemfile.rails-6.1.lock +4 -4
- data/gemfiles/Gemfile.rails-7.2.lock +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +1 -1
- data/lib/lepus/configuration.rb +6 -0
- data/lib/lepus/consumer.rb +34 -0
- data/lib/lepus/consumers/config.rb +8 -7
- data/lib/lepus/consumers/stats.rb +15 -2
- data/lib/lepus/delay.rb +180 -0
- data/lib/lepus/primitive/string.rb +2 -12
- data/lib/lepus/producer.rb +6 -1
- data/lib/lepus/producers/definition.rb +3 -1
- data/lib/lepus/publisher.rb +29 -3
- data/lib/lepus/testing/rspec_matchers.rb +20 -1
- data/lib/lepus/testing.rb +2 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus/web/api.rb +45 -0
- data/lib/lepus/web/app.rb +19 -16
- data/lib/lepus/web/management_api.rb +125 -2
- data/lib/lepus/web/topology.rb +341 -0
- data/lib/lepus/web.rb +17 -18
- data/lib/lepus.rb +1 -0
- data/web/assets/app.css +862 -0
- data/web/assets/apple-touch-icon.png +0 -0
- data/web/assets/favicon-32.png +0 -0
- data/web/assets/favicon.svg +1 -0
- data/web/assets/fonts/overpass/OFL.txt +93 -0
- data/web/assets/fonts/overpass/overpass-latin-wght.woff2 +0 -0
- data/web/assets/js/api.js +27 -0
- data/web/assets/js/dom.js +95 -0
- data/web/assets/js/format.js +63 -0
- data/web/assets/js/live.js +74 -0
- data/web/assets/js/main.js +156 -0
- data/web/assets/js/map.js +606 -0
- data/web/assets/js/model.js +262 -0
- data/web/assets/js/rates.js +161 -0
- data/web/assets/js/theme.js +8 -0
- data/web/assets/js/views/connections.js +61 -0
- data/web/assets/js/views/overview.js +229 -0
- data/web/assets/js/views/processes.js +101 -0
- data/web/assets/js/views/queues.js +456 -0
- data/web/index.html +58 -153
- data/web/sw.js +14 -143
- metadata +47 -12
- data/test_offline.html +0 -189
- data/web/assets/css/styles.css +0 -635
- data/web/assets/js/app.js +0 -6
- data/web/assets/js/bootstrap.js +0 -20
- data/web/assets/js/controllers/connection_controller.js +0 -44
- data/web/assets/js/controllers/dashboard_controller.js +0 -499
- data/web/assets/js/controllers/queue_controller.js +0 -17
- data/web/assets/js/controllers/theme_controller.js +0 -31
- data/web/assets/js/offline-manager.js +0 -233
- data/web/assets/js/service-worker-manager.js +0 -65
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 050afe09c334acbba0db8685b273d8746e494f767fc0ff2304b545445838acc5
|
|
4
|
+
data.tar.gz: 2a6f2c1a0059e94c444d027200adba9b6a371d9481620cf8bcac650e30e21158
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6338054122b5480c8db2f79d4b594ffb49f61b05736699722878c41ebc6cd1f591c1eae0aa824ad81203f6ca25ad68751df9c55dceede2ea312e306bacc4a7cf
|
|
7
|
+
data.tar.gz: 52e8af27c44ba24ee602f8c4aaf4732f20b1f428b4f71c234a8f186c79b311cf467dbd3e15c9b5d3217a91d0acf03d4fe30cad0d337e5e74ea53a1a9338f5cbf
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.3.0 - 2026-11-06
|
|
8
|
+
* Added: brand mark and README logo
|
|
9
|
+
* Changed: the web dashboard is redesigned around a live topology map (exchanges, queues, consumers, retry and error queues) with a message rate strip and tables for processes, queues and connections; it loads nothing from CDNs and sends a strict Content-Security-Policy
|
|
10
|
+
* Added: `GET /api/topology`, `GET /api/overview` and `GET /api/meta` dashboard endpoints, plus `ManagementAPI#bindings`, `#consumers` and `#overview`
|
|
11
|
+
* Added: consumer heartbeats report their `retry_queue` and `error_queue`
|
|
12
|
+
* Added: the dashboard shows delayed messages: delay queues and `<exchange>.delayed` exchanges are recognised by Lepus' own naming and arguments, their messages are counted on the queue or exchange they are released to (map, headline, queues table) and `/api/topology` exposes `delay_ms`, `target`, `target_type`, `delayed`, `delays` and `releases_to`
|
|
13
|
+
* Added: consumer heartbeats report the consumer's `delays` and how many messages it handed back with `delay!`, which no longer count as processed
|
|
14
|
+
* Added: `/api/queues` includes each queue's state, flags, arguments, idle time, utilisation and backlog rates
|
|
15
|
+
* Changed: `/api/exchanges` keeps the `<exchange>.delayed` exchanges of the consumers' exchanges
|
|
16
|
+
* Added: `bin/web-demo` serves the dashboard on a simulated broker (`HEAVY=1` for a large one)
|
|
17
|
+
* Removed: the dashboard's offline mode; its service worker now unregisters itself and clears its caches
|
|
18
|
+
* Fixed: the management API client uses the percent-decoded vhost from `rabbitmq_url` instead of always `/`
|
|
19
|
+
* Fixed: `Lepus::Primitive::String` no longer requires dry-inflector or ActiveSupport; it uses them only when the app has already loaded one
|
|
20
|
+
* Fixed: the dashboard no longer serves arbitrary files from outside its assets directory
|
|
21
|
+
* Fixed: the Rails mount example in `docs/web.md`
|
|
22
|
+
* Added: `delay:` option on `Producer.publish` and `Lepus::Publisher#publish` (milliseconds, or anything responding to `in_milliseconds`) delivers messages later through per-delay TTL queues, without the delayed-message plugin
|
|
23
|
+
* Added: `delay!(ms)` (aliased `delay`) consumer helper re-delivers the current message to the consumer's own queue after the delay and acks it, incrementing the `x-lepus-delayed-count` header
|
|
24
|
+
* Added: `delay:` through the default exchange delivers to the queue named by `routing_key` later, e.g. `Lepus::Publisher.new("").publish(payload, routing_key: "jobs", delay: 5_000)` (also on `channel_publish`)
|
|
25
|
+
* Added: `config.delay_queue_expires` (default 1 hour) sets how long unused delay queues are kept beyond their delay before RabbitMQ deletes them
|
|
26
|
+
* Added: `delays:` option on producer and consumer `configure` restricts delays to a fixed set, raising `Lepus::Error` for any other value, to bound the number of delay queues
|
|
27
|
+
* Added: info log the first time each process declares a delay queue
|
|
28
|
+
* Added: `delay` key in the `publish.lepus` instrumentation payload
|
|
29
|
+
* Added: `Lepus::Testing` records `delay` and `expiration` of fake messages, and `lepus_publish_message.with_delay(ms)` matcher
|
|
30
|
+
* Fixed: `Consumers::Config#retry_queue_name` and `#error_queue_name` return nil instead of raising when the queue is not configured
|
|
31
|
+
|
|
7
32
|
## 0.2.2 - 2026-10-30
|
|
8
33
|
* Added: `lepus start --except A,B` (and `Supervisor.new(except:)`) runs every consumer but the ones listed
|
|
9
34
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
lepus (0.
|
|
4
|
+
lepus (0.3.0)
|
|
5
5
|
base64
|
|
6
6
|
bunny
|
|
7
7
|
concurrent-ruby
|
|
@@ -23,7 +23,7 @@ GEM
|
|
|
23
23
|
logger (~> 1, >= 1.7)
|
|
24
24
|
sorted_set (~> 1, >= 1.0.2)
|
|
25
25
|
coderay (1.1.3)
|
|
26
|
-
concurrent-ruby (1.3.
|
|
26
|
+
concurrent-ruby (1.3.8)
|
|
27
27
|
connection_pool (2.5.5)
|
|
28
28
|
crack (1.0.0)
|
|
29
29
|
bigdecimal
|
|
@@ -56,7 +56,7 @@ GEM
|
|
|
56
56
|
rack-test (2.2.0)
|
|
57
57
|
rack (>= 1.3)
|
|
58
58
|
rainbow (3.1.1)
|
|
59
|
-
rbtree (0.4.
|
|
59
|
+
rbtree (0.4.7)
|
|
60
60
|
redis (5.4.1)
|
|
61
61
|
redis-client (>= 0.22.0)
|
|
62
62
|
redis-client (0.27.0)
|
data/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/lockup-lepus-on-dark.svg">
|
|
4
|
+
<img alt="Lepus" src="docs/brand/lockup-lepus-on-light.svg" width="278">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
2
7
|
|
|
3
|
-

|
|
4
9
|
|
|
5
10
|
Lepus is a lightweight but powerful Ruby library to help you to consume and produce messages to [RabbitMQ](https://www.rabbitmq.com/) using the [Bunny](https://github.com/ruby-amqp/bunny) gem. It's similar to the Sidekiq, Faktory, ActiveJob, SolidQueue, and other libraries, but using RabbitMQ as the message broker.
|
|
6
11
|
|
|
@@ -281,6 +286,73 @@ class MyProducer < Lepus::Producer
|
|
|
281
286
|
end
|
|
282
287
|
```
|
|
283
288
|
|
|
289
|
+
### Delayed messages
|
|
290
|
+
|
|
291
|
+
Pass `delay:` (in milliseconds, or anything responding to `in_milliseconds` such as `5.minutes`) to deliver a message later. No RabbitMQ plugin is needed:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
OrderEventsProducer.publish({ order_id: 123 }, routing_key: "order.reminder", delay: 30_000)
|
|
295
|
+
Lepus::Publisher.new("order_events").publish("ping", routing_key: "order.ping", delay: 5_000)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
A consumer can also put the message it is processing back on its own queue for later with [`delay!`](#consumer-configuration).
|
|
299
|
+
|
|
300
|
+
#### How it works
|
|
301
|
+
|
|
302
|
+
Delays are built from regular RabbitMQ queues: a message waits in a queue nobody consumes until its `x-message-ttl` expires, then RabbitMQ dead-letters it to the real destination. These are real, durable queues that you will see in the management UI.
|
|
303
|
+
|
|
304
|
+
```mermaid
|
|
305
|
+
flowchart LR
|
|
306
|
+
P["publish(delay: 30_000)"] --> DX{{"order_events.delayed<br/>(headers exchange)"}}
|
|
307
|
+
DX -- "lepus-delay = 30000" --> DQ["order_events.delayed.30000<br/>(TTL 30s, no consumers)"]
|
|
308
|
+
DX -- "lepus-delay = 5000" --> DQ2["order_events.delayed.5000<br/>(TTL 5s, no consumers)"]
|
|
309
|
+
DQ -- "TTL expires:<br/>dead-letter" --> X{{"order_events"}}
|
|
310
|
+
DQ2 -- "TTL expires:<br/>dead-letter" --> X
|
|
311
|
+
X -- "original routing key" --> Q1["your consumer queues"]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```mermaid
|
|
315
|
+
flowchart LR
|
|
316
|
+
C["consumer of reindex<br/>calls delay!(5_000)"] -- "ack + re-publish" --> DQ["reindex.delay.5000<br/>(TTL 5s, no consumers)"]
|
|
317
|
+
DQ -- "TTL expires:<br/>dead-letter" --> Q["reindex"]
|
|
318
|
+
Q --> C
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- **One queue per distinct delay value, not per message.** A million messages delayed by 30 seconds share `order_events.delayed.30000`. The number of queues is (exchanges or consumer queues using delays) × (distinct delay values).
|
|
322
|
+
- **Use a small set of fixed delays** such as 5s, 30s and 5min. Computed or random delays (`rand(1_000..60_000)`, `(run_at - Time.now) * 1000`, jitter in a backoff) create a new queue for nearly every message.
|
|
323
|
+
- **Each delay value has its own TTL**, so a long delay never holds back a shorter one.
|
|
324
|
+
- **Messages keep their routing key and headers** when they reach the target exchange, so existing bindings and consumers work unchanged.
|
|
325
|
+
- **`delay!` re-delivers only to that consumer's queue**, not to other queues bound to the same exchange. It acks the current delivery and increments the `x-lepus-delayed-count` header. Nothing is delivered while the message waits, but if your code calls `delay!` on every attempt it repeats forever, so cap it with that header.
|
|
326
|
+
|
|
327
|
+
#### Limiting the delay queues
|
|
328
|
+
|
|
329
|
+
Delay queues belong to an exchange (producer delays) or a consumer queue (`delay!`), so the allowed delays are set there with `delays:`. Any other value raises `Lepus::Error` before anything is published, including in testing mode. Without `delays:`, any delay is accepted.
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
class OrderEventsProducer < Lepus::Producer
|
|
333
|
+
configure(exchange: "order_events", delays: [5_000, 30_000])
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
class ReindexConsumer < Lepus::Consumer
|
|
337
|
+
configure(queue: "reindex", exchange: "stories", delays: [60_000, 300_000, 900_000])
|
|
338
|
+
|
|
339
|
+
def perform(message)
|
|
340
|
+
attempt = message.metadata.headers.to_h["x-lepus-delayed-count"].to_i
|
|
341
|
+
return delay!(self.class.config.delay_values[attempt]) if pending?(message) && attempt < 3
|
|
342
|
+
|
|
343
|
+
ack!
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`delay!` raising inside `perform` rejects the message like any other error. `Lepus::Publisher#publish(..., delay:)` has no allowlist; use a producer when you want one.
|
|
349
|
+
|
|
350
|
+
Lepus also logs `[Lepus::Delay] Declared delay queue "order_events.delayed.30000" (3 in this process)` at info level the first time each process declares a delay queue, so a growing count is easy to spot.
|
|
351
|
+
|
|
352
|
+
#### Cleanup
|
|
353
|
+
|
|
354
|
+
Unused delay queues are deleted by RabbitMQ after the delay plus one hour (`x-expires`), together with any message still in them. Lepus re-declares a queue while it is in use, so pending messages never outlive it. Change the window with `config.delay_queue_expires = 600_000`, or set it to `nil` to keep the queues forever. Queue arguments can't change once a queue exists, so changing this later raises `Lepus::Error` for existing queues until they are deleted. See [Producers](docs/producers.md#delayed-messages) and [Consumers](docs/consumers.md#delaying-a-message) for details.
|
|
355
|
+
|
|
284
356
|
### Configuration > Consumer Worker
|
|
285
357
|
|
|
286
358
|
You can configure the consumer process using the `worker` method. The default worker is named `:default`, but you can define more workers with different names for different consumers.
|
|
@@ -388,6 +460,24 @@ It may result in a infinite loop if the message is always rejected. To avoid thi
|
|
|
388
460
|
|
|
389
461
|
Refer to the [Dead Letter Exchanges](https://www.rabbitmq.com/docs/dlx) documentation for more information about retry.
|
|
390
462
|
|
|
463
|
+
A consumer can also put the current message back on its own queue for later with `delay!` (aliased `delay`). It acks the current delivery and increments the `x-lepus-delayed-count` header, so you can cap the attempts:
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
class ReindexConsumer < Lepus::Consumer
|
|
467
|
+
configure(queue: "reindex", exchange: "stories", routing_key: "story.published")
|
|
468
|
+
use :json
|
|
469
|
+
|
|
470
|
+
def perform(message)
|
|
471
|
+
return delay!(5_000) if replica_behind? && message.metadata.headers.to_h["x-lepus-delayed-count"].to_i < 3
|
|
472
|
+
|
|
473
|
+
reindex(message.payload)
|
|
474
|
+
ack!
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The redelivery goes only to this consumer's queue (through `reindex.delay.5000`), not to other queues bound to the exchange. See [Consumers](docs/consumers.md#delaying-a-message) for details.
|
|
480
|
+
|
|
391
481
|
### Middlewares
|
|
392
482
|
|
|
393
483
|
Consumers can use middlewares for recurring tasks like logging, error handling, parsing, and others. It comes with a few built-in middlewares:
|
|
@@ -596,7 +686,7 @@ plugin :lepus
|
|
|
596
686
|
|
|
597
687
|
Lepus includes a built-in web dashboard that provides a real-time view of your message processing infrastructure. The dashboard allows you to monitor processes, queues, connections, and consumer performance.
|
|
598
688
|
|
|
599
|
-

|
|
600
690
|
|
|
601
691
|
### Starting the Web Dashboard
|
|
602
692
|
|
|
@@ -614,13 +704,18 @@ bundle exec lepus web --port 3000 --host 127.0.0.1
|
|
|
614
704
|
|
|
615
705
|
### Web Dashboard Features
|
|
616
706
|
|
|
617
|
-
The
|
|
707
|
+
The dashboard is one page that refreshes itself (every 5 seconds by default; the **Live** control in the top bar pauses it or picks 2, 5, 15 or 60 seconds, and polling also pauses while the tab is in the background):
|
|
708
|
+
|
|
709
|
+
- **Topology map**: each exchange is a band of lines, one per bound queue, running exchange → queue → consumer → acknowledged. Amber tokens move at a pace that follows each segment's message rate, a hollow amber bar shows a backlog piling up at its queue, violet loops under a line are retries, a teal clock counts [delayed messages](#delayed-messages) waiting to be released and red bars count messages in error queues. Quiet queues fold into a "+N" row per exchange so large brokers stay readable.
|
|
710
|
+
- **Headline**: messages flowing, queues backing up, retries, delayed messages and messages in error queues, in one sentence that doubles as the map's key.
|
|
711
|
+
- **Message rates**: published, delivered, acknowledged and redelivered per second over the last 10 minutes or hour, with a table view.
|
|
712
|
+
- **Processes**: supervisors, their workers (PID, memory, connections, heartbeat health) and each consumer's processed, rejected and errored counts, and how many messages it delayed (handed back with `delay!`).
|
|
713
|
+
- **Queues**: a sortable, filterable table with ready, unacked, consumers, rates, retry and error queue sizes and what waits in delay queues.
|
|
714
|
+
- **Connections**: the broker's client connections.
|
|
715
|
+
|
|
716
|
+
The map and the queue table need the RabbitMQ Management API (see `management_api_url`); processes come from worker heartbeats. It follows the system light or dark theme, or you can pick one.
|
|
618
717
|
|
|
619
|
-
|
|
620
|
-
- **Queue Management**: Monitor queue statistics including message counts, consumer connections, and memory usage
|
|
621
|
-
- **Connection Tracking**: View active RabbitMQ connections and their states
|
|
622
|
-
- **Consumer Performance**: Track processed, rejected, and errored messages per consumer
|
|
623
|
-
- **Real-time Updates**: Dashboard automatically refreshes to show current system state
|
|
718
|
+
To work on the UI without RabbitMQ, run `bin/web-demo` from a checkout (http://127.0.0.1:9393, `HEAVY=1` for a large simulated broker, `MOUNT=/lepus` to serve it under a sub-path).
|
|
624
719
|
|
|
625
720
|
### Integrating with Rails
|
|
626
721
|
|