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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile.lock +3 -3
  4. data/README.md +104 -9
  5. data/bin/web-demo +592 -0
  6. data/docs/brand/BRAND.md +67 -0
  7. data/docs/brand/apple-touch-icon.png +0 -0
  8. data/docs/brand/favicon-16.png +0 -0
  9. data/docs/brand/favicon-32.png +0 -0
  10. data/docs/brand/favicon.ico +0 -0
  11. data/docs/brand/favicon.svg +1 -0
  12. data/docs/brand/icon-512.png +0 -0
  13. data/docs/brand/lockup-lepus-mono.svg +1 -0
  14. data/docs/brand/lockup-lepus-on-dark.svg +1 -0
  15. data/docs/brand/lockup-lepus-on-light.svg +1 -0
  16. data/docs/brand/lockup-lepus-saga-mono.svg +1 -0
  17. data/docs/brand/lockup-lepus-saga-on-dark.svg +1 -0
  18. data/docs/brand/lockup-lepus-saga-on-light.svg +1 -0
  19. data/docs/brand/lockup-lepus-saga.svg +1 -0
  20. data/docs/brand/lockup-lepus.svg +1 -0
  21. data/docs/brand/mark-mono.svg +1 -0
  22. data/docs/brand/mark-on-dark.svg +1 -0
  23. data/docs/brand/mark-on-light.svg +1 -0
  24. data/docs/brand/mark-small-mono.svg +1 -0
  25. data/docs/brand/mark-small-on-dark.svg +1 -0
  26. data/docs/brand/mark-small-on-light.svg +1 -0
  27. data/docs/brand/mark-small.svg +1 -0
  28. data/docs/brand/mark.svg +1 -0
  29. data/docs/configuration.md +1 -0
  30. data/docs/consumers.md +39 -5
  31. data/docs/getting-started.md +1 -1
  32. data/docs/images/lepus-web.png +0 -0
  33. data/docs/middleware.md +1 -1
  34. data/docs/producers.md +41 -0
  35. data/docs/testing.md +67 -48
  36. data/docs/web.md +39 -13
  37. data/gemfiles/Gemfile.rails-5.2.lock +2 -2
  38. data/gemfiles/Gemfile.rails-6.1.lock +4 -4
  39. data/gemfiles/Gemfile.rails-7.2.lock +1 -1
  40. data/gemfiles/Gemfile.rails-8.0.lock +1 -1
  41. data/lib/lepus/configuration.rb +6 -0
  42. data/lib/lepus/consumer.rb +34 -0
  43. data/lib/lepus/consumers/config.rb +8 -7
  44. data/lib/lepus/consumers/stats.rb +15 -2
  45. data/lib/lepus/delay.rb +180 -0
  46. data/lib/lepus/primitive/string.rb +2 -12
  47. data/lib/lepus/producer.rb +6 -1
  48. data/lib/lepus/producers/definition.rb +3 -1
  49. data/lib/lepus/publisher.rb +29 -3
  50. data/lib/lepus/testing/rspec_matchers.rb +20 -1
  51. data/lib/lepus/testing.rb +2 -0
  52. data/lib/lepus/version.rb +1 -1
  53. data/lib/lepus/web/api.rb +45 -0
  54. data/lib/lepus/web/app.rb +19 -16
  55. data/lib/lepus/web/management_api.rb +125 -2
  56. data/lib/lepus/web/topology.rb +341 -0
  57. data/lib/lepus/web.rb +17 -18
  58. data/lib/lepus.rb +1 -0
  59. data/web/assets/app.css +862 -0
  60. data/web/assets/apple-touch-icon.png +0 -0
  61. data/web/assets/favicon-32.png +0 -0
  62. data/web/assets/favicon.svg +1 -0
  63. data/web/assets/fonts/overpass/OFL.txt +93 -0
  64. data/web/assets/fonts/overpass/overpass-latin-wght.woff2 +0 -0
  65. data/web/assets/js/api.js +27 -0
  66. data/web/assets/js/dom.js +95 -0
  67. data/web/assets/js/format.js +63 -0
  68. data/web/assets/js/live.js +74 -0
  69. data/web/assets/js/main.js +156 -0
  70. data/web/assets/js/map.js +606 -0
  71. data/web/assets/js/model.js +262 -0
  72. data/web/assets/js/rates.js +161 -0
  73. data/web/assets/js/theme.js +8 -0
  74. data/web/assets/js/views/connections.js +61 -0
  75. data/web/assets/js/views/overview.js +229 -0
  76. data/web/assets/js/views/processes.js +101 -0
  77. data/web/assets/js/views/queues.js +456 -0
  78. data/web/index.html +58 -153
  79. data/web/sw.js +14 -143
  80. metadata +47 -12
  81. data/test_offline.html +0 -189
  82. data/web/assets/css/styles.css +0 -635
  83. data/web/assets/js/app.js +0 -6
  84. data/web/assets/js/bootstrap.js +0 -20
  85. data/web/assets/js/controllers/connection_controller.js +0 -44
  86. data/web/assets/js/controllers/dashboard_controller.js +0 -499
  87. data/web/assets/js/controllers/queue_controller.js +0 -17
  88. data/web/assets/js/controllers/theme_controller.js +0 -31
  89. data/web/assets/js/offline-manager.js +0 -233
  90. 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: a9eb1d12b4ce05f9c4e9070d7083b68417b7a41b7cdb2b1a99cad2931c9879fe
4
- data.tar.gz: 1a6eafc5a47b48a228279e5ede1a06acfea053b04cf686e8984719633d60e66b
3
+ metadata.gz: 050afe09c334acbba0db8685b273d8746e494f767fc0ff2304b545445838acc5
4
+ data.tar.gz: 2a6f2c1a0059e94c444d027200adba9b6a371d9481620cf8bcac650e30e21158
5
5
  SHA512:
6
- metadata.gz: 40432e83f566507da124995e9a60130e10dbf54cc499ad8d3a6598369c0b8d3d9b78ab28be9fa6ca82812951092e28baead30d15d8b83feb6eeed9c6a30ebe32
7
- data.tar.gz: '08948dbc7e9a927e2d26787a81c1106c1afb392aed5373248c54a02909a96d8d21b7e9f8c42a90e3ac4bc6179bfe1e5cc0c77c483be7d4808293bc6c51259213'
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.2.2)
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.6)
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.6)
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
- # Lepus
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
- ![Lepus web dashboard](https://github.com/user-attachments/assets/a640fc43-2f53-4aa7-aede-f1464f3e6e03)
8
+ ![Lepus web dashboard](docs/images/lepus-web.png)
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
- ![Lepus web dashboard — overview of supervisors, workers, and recent activity](https://github.com/user-attachments/assets/a640fc43-2f53-4aa7-aede-f1464f3e6e03)
689
+ ![Lepus web dashboard: the topology map with messages flowing from exchanges through queues to consumers](docs/images/lepus-web.png)
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 Lepus web dashboard provides:
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
- - **Process Monitoring**: View all running supervisors and workers with their PIDs, memory usage, and heartbeat status
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