mudis 0.8.0 → 0.9.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.
data/README.md CHANGED
@@ -1,932 +1,958 @@
1
- ![mudis_signet](design/mudis.png "Mudis")
2
-
3
- [![RubyMine](https://www.elegantobjects.org/rubymine.svg)](https://www.jetbrains.com/ruby/)
4
-
5
- [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1&cachebust=0)](https://badge.fury.io/rb/mudis)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
-
8
- **Mudis** is a fast, thread-safe, in-memory, sharded LRU (Least Recently Used) cache for Ruby applications. Inspired by Redis, it provides value serialization, optional compression, per-key expiry, and metric tracking in a lightweight, dependency-free package that lives inside your Ruby process.
9
-
10
- It’s ideal for scenarios where performance and process-local caching are critical, and where a full Redis setup is overkill or otherwise not possible/desirable.
11
-
12
- Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a [Mudis Web Cache Server](#create-a-mudis-web-cache-server).
13
-
14
- Mudis also works naturally in Hanami because it’s a pure Ruby in-memory cache. Whether used as a singleton within a process or via IPC in cluster mode, it preserves Hanami’s lightweight and modular architecture.
15
-
16
- ---
17
-
18
- ## Table of Contents
19
-
20
- - [Why Another Caching Gem?](#why-another-caching-gem)
21
- - [Similar Gems](#similar-gems)
22
- - [Feature / Function Comparison](#feature--function-comparison)
23
- - [Design](#design)
24
- - [Internal Structure and Behaviour](#internal-structure-and-behaviour)
25
- - [Write - Read - Eviction](#write---read---eviction)
26
- - [Cache Key Lifecycle](#cache-key-lifecycle)
27
- - [Features](#features)
28
- - [Installation](#installation)
29
- - [Configuration (Rails)](#configuration-rails)
30
- - [Configuration (Hanami)](#configuration-hanami)
31
- - [Basic Usage](#basic-usage)
32
- - [Developer Utilities](#developer-utilities)
33
- - [`Mudis.reset!`](#mudisreset)
34
- - [`Mudis.reset_metrics!`](#mudisreset_metrics)
35
- - [`Mudis.least_touched`](#mudisleast_touched)
36
- - [`Mudis.keys(namespace:)`](#mudiskeysnamespace)
37
- - [`Mudis.clear_namespace(namespace:)`](#mudisclear_namespacenamespace)
38
- - [Rails Service Integration](#rails-service-integration)
39
- - [Metrics](#metrics)
40
- - [Advanced Configuration](#advanced-configuration)
41
- - [Soft Persistence (Snapshots)](#soft-persistence-snapshots)
42
- - [Inter-Process Caching (IPC Mode)](#inter-process-caching-ipc-mode)
43
- - [Overview](#overview)
44
- - [Setup (Puma)](#setup-puma)
45
- - [Benchmarks](#benchmarks)
46
- - [Graceful Shutdown](#graceful-shutdown)
47
- - [Known Limitations](#known-limitations)
48
- - [Create a Mudis Web Cache Server](#create-a-mudis-web-cache-server)
49
- - [Minimal Setup](#minimal-setup)
50
- - [Project Philosophy](#project-philosophy)
51
- - [Roadmap](#roadmap)
52
-
53
- ---
54
-
55
- ### Why another Caching Gem?
56
-
57
- There are plenty out there, in various states of maintenance and in many shapes and sizes. So why on earth do we need another? I needed a drop-in replacement for Kredis, and the reason I was interested in using Kredis was for the simplified API and keyed management it gave me in extension to Redis. But what I didn't really need was Redis. I needed an observable, fast, simple, easy to use, flexible and highly configurable, thread-safe and high performant caching system which didn't require too many dependencies or standing up additional services. So, Mudis was born. In its most rudimentary state it was extremely useful in my project, which was an API gateway connecting into mutliple micro-services and a wide selection of APIs. The majority of the data was cold and produced by repeat expensive queries across several domains. Mudis allowed for me to minimize the footprint of the gateway, and improve end user experience, and increase performance. So, yeah, there's a lot of these gems out there, but none which really met all my needs. I decided to provide Mudis for anyone else. If you use it, I'd be interested to know how and whether you got any benefit.
58
-
59
- #### Similar Gems
60
-
61
- - [FastCache](https://github.com/swoop-inc/fast_cache)
62
- - [EasyCache](https://github.com/malvads/easycache)
63
- - [MiniCache](https://github.com/derrickreimer/mini_cache)
64
- - [Zache](https://github.com/yegor256/zache)
65
-
66
- #### Feature / Function Comparison
67
-
68
- | **Feature** | **Mudis** | **MemoryStore** (`Rails.cache`) | **FastCache** | **Zache** | **EasyCache** | **MiniCache** |
69
- | -------------------------------------- | ---------------- | ------------------------------- | -------------- | ------------- | ------------- | -------------- |
70
- | **LRU eviction strategy** | Per-bucket | Global | ✅ Global | | | Simplistic |
71
- | **TTL expiry support** | | | | | | |
72
- | **Background expiry cleanup thread** | ✅ | (only on access) | ❌ | ✅ | ❌ | |
73
- | **Thread safety** | ✅ Bucketed | ⚠️ Global lock | ✅ Fine-grained | ✅ | ⚠️ | ⚠️ |
74
- | **Sharding (buckets)** | ✅ | ❌ | | ❌ | ❌ | ❌ |
75
- | **Custom serializers** | ✅ | | | | | |
76
- | **Compression (Zlib)** | ✅ | | | ❌ | ❌ | ❌ |
77
- | **Hard memory cap** | ✅ | | ❌ | ❌ | ❌ | ❌ |
78
- | **Max value size enforcement** | ✅ | | ❌ | ❌ | ❌ | ❌ |
79
- | **Metrics (hits, misses, evictions)** | ✅ | ⚠️ Partial | ❌ | ❌ | ❌ | ❌ |
80
- | **Fetch/update pattern** | ✅ Full | ✅ Standard | ⚠️ Partial | ✅ Basic | ✅ Basic | ✅ Basic |
81
- | **Namespacing** | ✅ | | ❌ | ❌ | ❌ | ❌ |
82
- | **Replace (if exists)** | ✅ | ✅ | | | | |
83
- | **Clear/delete method** | ✅ | ✅ | | | | |
84
- | **Key inspection with metadata** | ✅ | | ❌ | ❌ | ❌ | ❌ |
85
- | **Concurrency model** | ✅ | | ✅ | | | |
86
- | **Maintenance level** | ✅ | | | ⚠️ | ⚠️ | ⚠️ |
87
- | **Suitable for APIs or microservices** | ✅ | ⚠️ Limited | ✅ | ⚠️ Small apps | ⚠️ Small apps | ❌ |
88
- | **Inter-process Caching** | ✅ | | | | | |
89
-
90
- ---
91
-
92
- ## Design
93
-
94
- #### Internal Structure and Behaviour
95
-
96
- ![mudis_flow](design/mudis_obj.png "Mudis Internals")
97
-
98
- #### Write - Read - Eviction
99
-
100
- ![mudis_flow](design/mudis_flow.png "Write - Read - Eviction")
101
-
102
- #### Cache Key Lifecycle
103
-
104
- ![mudis_lru](design/mudis_lru.png "Mudis Cache Key Lifecycle")
105
-
106
- ---
107
-
108
- ## Features
109
-
110
- - **Thread-safe**: Uses per-bucket mutexes for high concurrency.
111
- - **Sharded**: Buckets data across multiple internal stores to minimize lock contention.
112
- - **LRU Eviction**: Automatically evicts least recently used items as memory fills up.
113
- - **Expiry Support**: Optional TTL per key with background cleanup thread.
114
- - **Compression**: Optional Zlib compression for large values.
115
- - **Metrics**: Tracks hits, misses, and evictions.
116
- - **IPC Mode**: Shared cross-process caching for multi-process aplications.
117
-
118
- ---
119
-
120
- ## Installation
121
-
122
- Add this line to your Gemfile:
123
-
124
- ```ruby
125
- gem 'mudis'
126
- ```
127
-
128
- Or install it manually:
129
-
130
- ```bash
131
- gem install mudis
132
- ```
133
-
134
- ---
135
-
136
- ## Configuration (Rails)
137
-
138
- In your Rails app, create an initializer:
139
-
140
- ```ruby
141
- # config/initializers/mudis.rb
142
- Mudis.configure do |c|
143
- c.serializer = JSON # or Marshal | Oj
144
- c.compress = true # Compress values using Zlib
145
- c.max_value_bytes = 2_000_000 # Reject values > 2MB
146
- c.hard_memory_limit = true # enforce hard memory limits
147
- c.max_bytes = 1_073_741_824 # set maximum cache size
148
- end
149
-
150
- Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
151
-
152
- at_exit do
153
- Mudis.stop_expiry_thread
154
- end
155
- ```
156
-
157
- Or with direct setters:
158
-
159
- ```ruby
160
- Mudis.serializer = JSON # or Marshal | Oj
161
- Mudis.compress = true # Compress values using Zlib
162
- Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
163
- Mudis.hard_memory_limit = true # enforce hard memory limits
164
- Mudis.max_bytes = 1_073_741_824 # set maximum cache size
165
-
166
- Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
167
-
168
- ## set at exit hook
169
- ```
170
-
171
- ---
172
-
173
- ## Configuration (Hanami)
174
-
175
- Mudis integrates seamlessly with [Hanami](https://hanamirb.org) applications. It provides the same configuration flexibility and thread-safe caching capabilities as in Rails, with minimal setup differences.
176
-
177
- Create a boot file:
178
-
179
- ```ruby
180
- # config/boot/mudis.rb
181
- require "mudis"
182
-
183
- Mudis.configure do |c|
184
- c.serializer = JSON
185
- c.compress = true
186
- c.max_value_bytes = 2_000_000
187
- c.hard_memory_limit = true
188
- c.max_bytes = 1_073_741_824
189
- end
190
-
191
- Mudis.start_expiry_thread(interval: 60)
192
-
193
- at_exit { Mudis.stop_expiry_thread }
194
- ```
195
-
196
- Then require it from `config/app.rb`:
197
-
198
- ```ruby
199
- # config/app.rb
200
- require_relative "./boot/mudis"
201
-
202
- module MyApp
203
- class App < Hanami::App
204
- end
205
- end
206
- ```
207
-
208
- This ensures Mudis is initialized and available globally throughout your Hanami application in the same as it would in Rails.
209
-
210
- ---
211
-
212
- ### Using Mudis with Hanami’s Dependency Container
213
-
214
- You can register Mudis as a dependency in the Hanami container to access it via dependency injection:
215
-
216
- ```ruby
217
- # config/container.rb
218
- require "mudis"
219
-
220
- MyApp::Container.register(:cache, Mudis)
221
- ```
222
-
223
- Then use it inside your actions, repositories, or services:
224
-
225
- ```ruby
226
- # apps/main/actions/users/show.rb
227
- module Main
228
- module Actions
229
- module Users
230
- class Show < Main::Action
231
- include Deps[cache: "cache"]
232
-
233
- def handle(req, res)
234
- res[:user] = cache.fetch("user:#{req.params[:id]}", expires_in: 60) do
235
- UserRepo.new.find(req.params[:id])
236
- end
237
- end
238
- end
239
- end
240
- end
241
- end
242
- ```
243
-
244
- ---
245
-
246
- ## Basic Usage
247
-
248
- ```ruby
249
- require 'mudis'
250
-
251
- # Write a value with optional TTL
252
- Mudis.write('user:123', { name: 'Alice' }, expires_in: 600)
253
-
254
- # Read it back
255
- Mudis.read('user:123') # => { "name" => "Alice" }
256
-
257
- # Check if it exists
258
- Mudis.exists?('user:123') # => true
259
-
260
- # Atomically update
261
- Mudis.update('user:123') { |data| data.merge(age: 30) }
262
-
263
- # Delete a key
264
- Mudis.delete('user:123')
265
- ```
266
-
267
- ### Developer Utilities
268
-
269
- Mudis provides utility methods to help with test environments, console debugging, and dev tool resets.
270
-
271
- #### `Mudis.reset!`
272
- Clears the internal cache state. Including all keys, memory tracking, and metrics. Also stops the expiry thread.
273
-
274
- ```ruby
275
- Mudis.write("foo", "bar")
276
- Mudis.reset!
277
- Mudis.read("foo") # => nil
278
- ```
279
-
280
- - Wipe all buckets (@stores, @lru_nodes, @current_bytes)
281
- - Reset all metrics (:hits, :misses, :evictions, :rejected)
282
- - Stop any running background expiry thread
283
-
284
- #### `Mudis.reset_metrics!`
285
-
286
- Clears only the metric counters and preserves all cached values.
287
-
288
- ```ruby
289
- Mudis.write("key", "value")
290
- Mudis.read("key") # => "value"
291
- Mudis.metrics # => { hits: 1, misses: 0, ... }
292
-
293
- Mudis.reset_metrics!
294
- Mudis.metrics # => { hits: 0, misses: 0, ... }
295
- Mudis.read("key") # => "value" (still cached)
296
- ```
297
-
298
- #### `Mudis.least_touched`
299
-
300
- Returns the top `n` (or all) keys that have been read the fewest number of times, across all buckets. This is useful for identifying low-value cache entries that may be safe to remove or exclude from caching altogether.
301
-
302
- Each result includes the full key and its access count.
303
-
304
- ```ruby
305
- Mudis.least_touched
306
- # => [["foo", 0], ["user:42", 1], ["product:123", 2], ...]
307
-
308
- Mudis.least_touched(5)
309
- # => returns top 5 least accessed keys
310
- ```
311
-
312
- #### `Mudis.keys(namespace:)`
313
-
314
- Returns all keys for a given namespace.
315
-
316
- ```ruby
317
- Mudis.write("u1", "alpha", namespace: "users")
318
- Mudis.write("u2", "beta", namespace: "users")
319
-
320
- Mudis.keys(namespace: "users")
321
- # => ["u1", "u2"]
322
-
323
- ```
324
-
325
- #### `Mudis.clear_namespace(namespace:)`
326
-
327
- Deletes all keys within a namespace.
328
-
329
- ```ruby
330
- Mudis.clear_namespace("users")
331
- Mudis.read("u1", namespace: "users") # => nil
332
- ```
333
-
334
- ---
335
-
336
- ## Rails Service Integration
337
-
338
- For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
339
-
340
- ```ruby
341
- class MudisService
342
- attr_reader :cache_key, :namespace
343
-
344
- # Initialize the service with a cache key and optional namespace
345
- #
346
- # @param cache_key [String] the base key to use
347
- # @param namespace [String, nil] optional logical namespace
348
- def initialize(cache_key, namespace: nil)
349
- @cache_key = cache_key
350
- @namespace = namespace
351
- end
352
-
353
- # Write a value to the cache
354
- #
355
- # @param data [Object] the value to cache
356
- # @param expires_in [Integer, nil] optional TTL in seconds
357
- def write(data, expires_in: nil)
358
- Mudis.write(cache_key, data, expires_in: expires_in, namespace: namespace)
359
- end
360
-
361
- # Read the cached value or return default
362
- #
363
- # @param default [Object] fallback value if key is not present
364
- def read(default: nil)
365
- Mudis.read(cache_key, namespace: namespace) || default
366
- end
367
-
368
- # Update the cached value using a block
369
- #
370
- # @yieldparam current [Object] the current value
371
- # @yieldreturn [Object] the updated value
372
- def update
373
- Mudis.update(cache_key, namespace: namespace) { |current| yield(current) }
374
- end
375
-
376
- # Delete the key from cache
377
- def delete
378
- Mudis.delete(cache_key, namespace: namespace)
379
- end
380
-
381
- # Return true if the key exists in cache
382
- def exists?
383
- Mudis.exists?(cache_key, namespace: namespace)
384
- end
385
-
386
- # Fetch from cache or compute and store it
387
- #
388
- # @param expires_in [Integer, nil] optional TTL
389
- # @param force [Boolean] force recomputation
390
- # @yield return value if key is missing
391
- def fetch(expires_in: nil, force: false)
392
- Mudis.fetch(cache_key, expires_in: expires_in, force: force, namespace: namespace) do
393
- yield
394
- end
395
- end
396
-
397
- # Inspect metadata for the current key
398
- #
399
- # @return [Hash, nil] metadata including :expires_at, :created_at, :size_bytes, etc.
400
- def inspect_meta
401
- Mudis.inspect(cache_key, namespace: namespace)
402
- end
403
- end
404
-
405
- ```
406
-
407
- Use it like:
408
-
409
- ```ruby
410
- cache = MudisService.new("user:42:profile", namespace: "users")
411
-
412
- cache.write({ name: "Alice" }, expires_in: 300)
413
- cache.read # => { "name" => "Alice" }
414
- cache.exists? # => true
415
-
416
- cache.update { |data| data.merge(age: 30) }
417
- cache.fetch(expires_in: 60) { expensive_query }
418
- cache.inspect_meta # => { key: "users:user:42:profile", ... }
419
- ```
420
-
421
- *This pattern can also be applied in Hanami services using the registered Mudis dependency*
422
-
423
- ---
424
-
425
- ## Metrics
426
-
427
- Track cache effectiveness and performance:
428
-
429
- ```ruby
430
- Mudis.metrics
431
- # => {
432
- # hits: 15,
433
- # misses: 5,
434
- # evictions: 3,
435
- # rejected: 0,
436
- # total_memory: 45678,
437
- # least_touched: [
438
- # ["user:1", 0],
439
- # ["post:5", 1],
440
- # ...
441
- # ],
442
- # buckets: [
443
- # { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
444
- # ...
445
- # ]
446
- # }
447
-
448
- ```
449
-
450
- Optionally, expose Mudis metrics from a controller or action for remote analysis and monitoring.
451
-
452
- **Rails:**
453
-
454
- ```ruby
455
- class MudisController < ApplicationController
456
- def metrics
457
- render json: { mudis: Mudis.metrics }
458
- end
459
-
460
- end
461
- ```
462
-
463
- **Hanami:**
464
-
465
- *Mudis Registered in the container*
466
-
467
- ```ruby
468
- # apps/main/actions/metrics/show.rb
469
- module Main::Actions::Metrics
470
- class Show < Main::Action
471
- include Deps[cache: "cache"]
472
-
473
- def handle(*, res)
474
- res.format = :json
475
- res.body = { mudis: cache.metrics }.to_json
476
- end
477
- end
478
- end
479
- ```
480
-
481
- *Mudis not registered in the container*
482
-
483
- ```ruby
484
- # apps/main/actions/metrics/show.rb
485
- module Main::Actions::Metrics
486
- class Show < Main::Action
487
- def handle(*, res)
488
- res.format = :json
489
- res.body = { mudis: Mudis.metrics }.to_json
490
- end
491
- end
492
- end
493
- ```
494
-
495
- ```ruby
496
- # config/routes.rb
497
- module Main
498
- class Routes < Hanami::Routes
499
- root { "OK" }
500
-
501
- get "/metrics", to: "metrics.show"
502
- end
503
- end
504
- ```
505
-
506
- ---
507
-
508
- ## Advanced Configuration
509
-
510
- | Setting | Description | Default |
511
- |--------------------------|---------------------------------------------|--------------------|
512
- | `Mudis.serializer` | JSON, Marshal, or Oj | `JSON` |
513
- | `Mudis.compress` | Enable Zlib compression | `false` |
514
- | `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
515
- | `Mudis.buckets` | Number of cache shards | `32` |
516
- | `Mudis.start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
517
- | `Mudis.hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
518
- | `Mudis.max_bytes` | Maximum allowed cache size | `1GB`|
519
- | `Mudis.max_ttl` | Set the maximum permitted TTL | `nil` (no limit) |
520
- | `Mudis.default_ttl` | Set the default TTL for fallback when none is provided | `nil` |
521
-
522
- Buckets can also be set using a `MUDIS_BUCKETS` environment variable.
523
-
524
- When setting `serializer`, be mindful of the below
525
-
526
- | Serializer | Recommended for |
527
- | ---------- | ------------------------------------- |
528
- | `Marshal` | Ruby-only apps, speed-sensitive logic |
529
- | `JSON` | Cross-language interoperability |
530
- | `Oj` | API-heavy apps using JSON at scale |
531
-
532
- ---
533
-
534
- ## Soft Persistence (Snapshots)
535
-
536
- Mudis can optionally soft-persist its in-memory cache to disk between process restarts.
537
- When enabled, it will automatically:
538
-
539
- - **Save** a snapshot of the current cache at shutdown
540
- - **Reload** it on startup
541
-
542
- This feature is **off by default** and can be enabled via configuration.
543
-
544
- Example configuration:
545
-
546
- ```ruby
547
- Mudis.configure do |config|
548
- config.persistence_enabled = true
549
- config.persistence_path = "tmp/mudis_snapshot.dump"
550
- config.persistence_format = :marshal # or :json
551
- config.persistence_safe_write = true # atomic temp write + rename
552
- end
553
-
554
- ## Optionally install the exit hook manually (usually automatic)
555
- # Mudis.install_persistence_hook!
556
- ```
557
-
558
- From your startup routine:
559
-
560
- ```ruby
561
- # Restore snapshot on startup
562
- Mudis.load_snapshot!
563
- ```
564
-
565
- *This feature works identically in Rails, Hanami, and standalone Ruby scripts, as long as `Mudis.configure` is called prior to `Mudis.load_snapshot!`.*
566
-
567
- ### Behavior
568
-
569
- | Operation | Description |
570
- |------------|-------------|
571
- | `save_snapshot!` | Dumps all non-expired cache entries to disk. |
572
- | `load_snapshot!` | Reads snapshot and restores entries via normal write path. |
573
- | `install_persistence_hook!` | Registers an `at_exit` hook to automatically save on process exit. |
574
-
575
- #### Notes
576
-
577
- - Disabled by default for backward compatibility.
578
- - Uses Ruby’s `Marshal` by default for fastest serialization; `:json` also supported.
579
- - Respects TTLs: expired entries are never written to disk.
580
- - Thread-safe: snapshots are taken under shard-level locks.
581
- - Safe write: when `persistence_safe_write = true`, Mudis writes to a temp file and renames it atomically.
582
-
583
- #### Example Flow
584
-
585
- ![mudis_persistence](design/mudis_persistence.png "Mudis Persistence Strategy")
586
-
587
- 1. On startup, `Mudis.load_snapshot!` repopulates the cache.
588
- 2. Your app uses the cache as normal (`write`, `read`, `fetch`, etc.).
589
- 3. When the process exits, `at_exit` automatically triggers `save_snapshot!`.
590
- 4. A file (e.g., `tmp/mudis_snapshot.dump`) holds your persisted cache.
591
-
592
- #### Safety
593
-
594
- If the snapshot file is **missing** or **corrupted**, Mudis will:
595
- - Log a warning via `warn "[Mudis] Failed to load snapshot..."`, and
596
- - Continue booting normally with an empty cache.
597
-
598
- ---
599
-
600
- ## Inter-Process Caching (IPC Mode)
601
-
602
- While Mudis was originally designed as an in-process cache, it can also operate as a shared inter-process cache when running in environments that use concurrent processes (such as Puma in cluster mode). This is achieved through a local UNIX socket server that allows all workers to access a single, centralized Mudis instance.
603
-
604
- ### Overview
605
-
606
- In IPC mode, Mudis runs as a singleton server within the master process.
607
-
608
- Each worker connects to that server through a lightweight client (`MudisClient`) using a local UNIX domain socket (default: `/tmp/mudis.sock`).
609
- All cache operations, e.g., read, write, delete, fetch, etc., are transparently proxied to the master process, which holds the authoritative cache state.
610
-
611
- This design allows multiple workers to share the same cache without duplicating memory or losing synchronization, while retaining Mudis’ performance, configurability, and thread safety.
612
-
613
- | **Benefit** | **Description** |
614
- | --------------------------------- | ---------------------------------------------------------------------------------------- |
615
- | **Shared Cache Across Processes** | All Puma workers share one Mudis instance via IPC. |
616
- | **Zero External Dependencies** | No Redis, Memcached, or separate daemon required. |
617
- | **Memory Efficient** | Cache data stored only once, not duplicated per worker. |
618
- | **Full Feature Support** | All Mudis features (TTL, compression, metrics, etc.) work transparently. |
619
- | **Safe & Local** | Communication is limited to the host system’s UNIX socket, ensuring isolation and speed. |
620
-
621
- ![mudis_ipc](design/mudis_ipc.png "Mudis IPC")
622
-
623
- ### Setup (Puma)
624
-
625
- Enable IPC mode by adding the following to your Puma configuration:
626
-
627
- ```ruby
628
- # config/puma.rb
629
- preload_app!
630
-
631
- before_fork do
632
- require "mudis"
633
- require "mudis_server"
634
-
635
- # typical Mudis configuration
636
- Mudis.configure do |c|
637
- c.serializer = JSON
638
- c.compress = true
639
- c.max_value_bytes = 2_000_000
640
- c.hard_memory_limit = true
641
- c.max_bytes = 1_073_741_824
642
- end
643
-
644
- Mudis.start_expiry_thread(interval: 60)
645
- MudisServer.start!
646
-
647
- at_exit { Mudis.stop_expiry_thread }
648
- end
649
-
650
- on_worker_boot do
651
- require "mudis_client"
652
- $mudis = MudisClient.new
653
- require "mudis_proxy" # optionally require the default mudis proxy to invisibly patch Mudis
654
- end
655
- ```
656
-
657
- For more granular control over Mudis, adding a custom Proxy manually to `initializers` (Rails) or `boot` (Hanami) allows seamless use of the API as documented.
658
-
659
- **Do not require `mudis_proxy` if following this method**
660
-
661
- Example custom proxy:
662
-
663
- ```ruby
664
- # config/<<initializers|boot>>/mudis_proxy.rb
665
- if defined?($mudis) && $mudis
666
- class Mudis
667
- def self.read(*a, **k) = $mudis.read(*a, **k)
668
- def self.write(*a, **k) = $mudis.write(*a, **k)
669
- def self.delete(*a, **k) = $mudis.delete(*a, **k)
670
- def self.fetch(*a, **k, &b) = $mudis.fetch(*a, **k, &b)
671
- def self.metrics = $mudis.metrics
672
- def self.reset_metrics! = $mudis.reset_metrics!
673
- def self.reset! = $mudis.reset!
674
- end
675
-
676
- end
677
- ```
678
-
679
- **Use IPC mode when:**
680
-
681
- - Running Rails or Rack apps under Puma cluster or multi-process background job workers.
682
- - You need cache consistency and memory efficiency without standing up Redis.
683
- - You want to preserve Mudis’s observability, configurability, and in-process simplicity at a larger scale.
684
-
685
- ---
686
-
687
- ## Benchmarks
688
-
689
- #### Serializer(s)
690
-
691
- _100000 iterations_
692
-
693
- | Serializer | Total Time (s) | Ops/sec |
694
- |----------------|------------|----------------|
695
- | oj | 0.1342 | 745320 |
696
- | marshal | 0.3228 | 309824 |
697
- | json | 0.9035 | 110682 |
698
- | oj + zlib | 1.8050 | 55401 |
699
- | marshal + zlib | 1.8057 | 55381 |
700
- | json + zlib | 2.7949 | 35780 |
701
-
702
- > If opting for OJ, you will need to install the dependency in your project and configure as needed.
703
-
704
- #### Mudis vs Rails.cache
705
-
706
- Mudis is marginally slower than `Rails.cache` by design; it trades raw speed for control, observability, and safety.
707
-
708
- _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON_
709
-
710
- | Operation | `Rails.cache` | `Mudis` | Delta |
711
- | --------- | ------------- | ----------- | --------- |
712
- | Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
713
- | Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
714
-
715
- > For context: a typical database query or HTTP call takes 10–50ms. A difference of less than 1ms per operation is negligible for most apps.
716
-
717
- #### **Why this overhead exists**
718
-
719
- Mudis includes features that MemoryStore doesn’t:
720
-
721
- | Feature | Mudis | Rails.cache (MemoryStore) |
722
- | ------------------ | ---------------------- | --------------------------- |
723
- | Per-key TTL expiry | ✅ | ⚠️ on access |
724
- | True LRU eviction | ✅ | ❌ |
725
- | Hard memory limits | | |
726
- | Value compression | ✅ | ❌ |
727
- | Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
728
- | Observability | | |
729
- | Namespacing | | ❌ Manual scoping |
730
- | IPC Aware | ✅ (if enabled) | ❌ Requires manual configuration and additional gems |
731
-
732
- It will be down to the developer to decide if a fraction of a millisecond is worth
733
-
734
- - Predictable eviction
735
- - Configurable expiry
736
- - Memory protection
737
- - Namespace scoping
738
- - Real-time metrics for hits, misses, evictions, memory usage
739
-
740
- _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)_
741
-
742
- | Operation | `Rails.cache` | `Mudis` | Delta |
743
- | --------- | ------------- | ----------- | ------------- |
744
- | Write | 2.342 ms/op | 0.501 ms/op | **−1.841 ms** |
745
- | Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
746
-
747
- With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
748
-
749
- #### Other Benchmarks
750
-
751
- _10000 iterations of 512KB, JSON, compression OFF (to match MemoryStore default)_
752
-
753
- | Operation | `Rails.cache` | `Mudis` | Delta |
754
- | --------- | ------------- | ----------- | ------------- |
755
- | Write | 1.291 ms/op | 0.32 ms/op | **−0.971 ms** |
756
- | Read | 0.011 ms/op | 0.048 ms/op | +0.037 ms |
757
-
758
- _10000 iterations of 512KB, JSON, compression ON_
759
-
760
- | Operation | `Rails.cache` | `Mudis` | Delta |
761
- | --------- | ------------- | ----------- | ------------- |
762
- | Write | 1.11 ms/op | 1.16 ms/op | +0.05 ms |
763
- | Read | 0.07 ms/op | 0.563 ms/op | +0.493 ms |
764
-
765
- ---
766
-
767
- ## Graceful Shutdown
768
-
769
- Don’t forget to stop the expiry thread when your app exits:
770
-
771
- ```ruby
772
- at_exit { Mudis.stop_expiry_thread }
773
- ```
774
-
775
- ---
776
-
777
- ## Known Limitations
778
-
779
- - Data is **non-persistent**; only soft-persistence is optionally provided.
780
- - Compression introduces CPU overhead.
781
-
782
- ---
783
-
784
- ## Create a Mudis Web Cache Server
785
-
786
- ### Minimal Setup
787
-
788
- - Create a new Rails API app:
789
-
790
- ```bash
791
- rails new mudis-server --api
792
- cd mudis-server
793
- ```
794
-
795
- - Add mudis to your Gemfile
796
- - Create Initializer: `config/initializers/mudis.rb`
797
- - Define routes
798
-
799
- ```ruby
800
- Rails.application.routes.draw do
801
- get "/cache/:key", to: "cache#show"
802
- post "/cache/:key", to: "cache#write"
803
- delete "/cache/:key", to: "cache#delete"
804
- get "/metrics", to: "cache#metrics"
805
- end
806
- ```
807
-
808
- - Create a `cache_controller` (with optional per caller/consumer namespace)
809
-
810
- ```ruby
811
- class CacheController < ApplicationController
812
-
813
- def show
814
- key = params[:key]
815
- ns = params[:namespace]
816
-
817
- value = Mudis.read(key, namespace: ns)
818
- if value.nil?
819
- render json: { error: "not found" }, status: :not_found
820
- else
821
- render json: { value: value }
822
- end
823
- end
824
-
825
- def write
826
- key = params[:key]
827
- ns = params[:namespace]
828
- val = params[:value]
829
- ttl = params[:expires_in]&.to_i
830
-
831
- Mudis.write(key, val, expires_in: ttl, namespace: ns)
832
- render json: { status: "written", key: key }
833
- end
834
-
835
- def delete
836
- key = params[:key]
837
- ns = params[:namespace]
838
-
839
- Mudis.delete(key, namespace: ns)
840
- render json: { status: "deleted" }
841
- end
842
-
843
- def metrics
844
- render json: Mudis.metrics
845
- end
846
- end
847
- ```
848
-
849
- - Test it
850
-
851
- ```bash
852
- curl http://localhost:3000/cache/foo
853
- curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
854
- curl http://localhost:3000/metrics
855
-
856
- # Write with namespace
857
- curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
858
- -d "value=123&expires_in=60"
859
-
860
- # Read from namespace
861
- curl "http://localhost:3000/cache/foo?namespace=orders"
862
-
863
- # Delete from namespace
864
- curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
865
-
866
- ```
867
-
868
- ---
869
-
870
- ## Project Philosophy
871
-
872
- Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
873
-
874
- - In-process caching
875
- - Fine-grained memory and namespace control
876
- - Observability and testing friendliness
877
- - Minimal external dependencies
878
- - Configurability without complexity
879
-
880
- The primary use cases are:
881
-
882
- - Per-service application caches
883
- - Short-lived local caching inside background jobs or API layers
884
-
885
- Mudis is not intended to be a general-purpose, distributed caching platform. You are, however, welcome to build on top of Mudis if you want its functionality in such projects. E.g.,
886
-
887
- - mudis-web-cache-server – expose Mudis via HTTP, web sockets, hooks, etc
888
- - mudis-broker distributed key routing layer for coordinating multiple Mudis nodes
889
- - mudis-activejob-store adapter for using Mudis in job queues or retry buffers
890
-
891
- ---
892
-
893
- ## Roadmap
894
-
895
- #### API Enhancements
896
-
897
- - [x] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
898
-
899
- #### Safety & Policy Controls
900
-
901
- - [x] max_ttl: Enforce a global upper bound on expires_in to prevent excessively long-lived keys
902
- - [x] default_ttl: Provide a fallback TTL when one is not specified
903
-
904
- #### Debugging
905
-
906
- - [x] clear_namespace(namespace): Remove all keys in a namespace in one call
907
-
908
- #### Refactor Mudis
909
-
910
- - [x] Review Mudis for improved readability and reduce complexity in top-level functions
911
- - [x] Enhanced guards
912
- - [ ] Review for functionality gaps and enhance as needed
913
-
914
- ---
915
-
916
- ## License
917
-
918
- MIT License © kiebor81
919
-
920
- ---
921
-
922
- ## Contributing
923
-
924
- See [contributor's guide](CONTRIBUTING.md)
925
-
926
- ---
927
-
928
- ## Contact
929
-
930
- For issues, suggestions, or feedback, please open a GitHub issue
931
-
932
- ---
1
+ ![mudis_signet](design/mudis.png "Mudis")
2
+
3
+ [![RubyMine](https://www.elegantobjects.org/rubymine.svg)](https://www.jetbrains.com/ruby/)
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1&cachebust=0)](https://badge.fury.io/rb/mudis)
6
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://www.rubydoc.info/gems/mudis)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ **Mudis** is a fast, thread-safe, in-memory, sharded LRU (Least Recently Used) cache for Ruby applications. Inspired by Redis, it provides value serialization, optional compression, per-key expiry, and metric tracking in a lightweight, dependency-free package that lives inside your Ruby process.
10
+
11
+ It’s ideal for scenarios where performance and process-local caching are critical, and where a full Redis setup is overkill or otherwise not possible/desirable.
12
+
13
+ Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails/Hanami/Roda/etc app to provide a [Mudis Web Cache Server](#create-a-mudis-web-cache-server).
14
+
15
+ Mudis also works naturally in Hanami because it’s a pure Ruby in-memory cache. Whether used as a singleton within a process or via IPC in cluster mode, it preserves Hanami’s lightweight and modular architecture.
16
+
17
+ ---
18
+
19
+ ## Table of Contents
20
+
21
+ - [Why Another Caching Gem?](#why-another-caching-gem)
22
+ - [Similar Gems](#similar-gems)
23
+ - [Feature / Function Comparison](#feature--function-comparison)
24
+ - [Design](#design)
25
+ - [Internal Structure and Behaviour](#internal-structure-and-behaviour)
26
+ - [Write - Read - Eviction](#write---read---eviction)
27
+ - [Cache Key Lifecycle](#cache-key-lifecycle)
28
+ - [Features](#features)
29
+ - [Installation](#installation)
30
+ - [Configuration (Ruby/Rails)](#configuration-rubyrails)
31
+ - [Configuration (Hanami)](#configuration-hanami)
32
+ - [Start and Stop Exipry Thread](#start-and-stop-exipry-thread)
33
+ - [Starting Exipry Thread](#starting-exipry-thread)
34
+ - [Graceful Shutdown](#graceful-shutdown)
35
+ - [Basic Usage](#basic-usage)
36
+ - [Developer Utilities](#developer-utilities)
37
+ - [`Mudis.reset!`](#mudisreset)
38
+ - [`Mudis.reset_metrics!`](#mudisreset_metrics)
39
+ - [`Mudis.least_touched`](#mudisleast_touched)
40
+ - [`Mudis.keys(namespace:)`](#mudiskeysnamespace)
41
+ - [`Mudis.clear_namespace(namespace:)`](#mudisclear_namespacenamespace)
42
+ - [Rails Service Integration](#rails-service-integration)
43
+ - [Metrics](#metrics)
44
+ - [Advanced Configuration](#advanced-configuration)
45
+ - [Soft Persistence (Snapshots)](#soft-persistence-snapshots)
46
+ - [Inter-Process Caching (IPC Mode)](#inter-process-caching-ipc-mode)
47
+ - [Overview](#overview)
48
+ - [Setup (Puma)](#setup-puma)
49
+ - [Benchmarks](#benchmarks)
50
+ - [Known Limitations](#known-limitations)
51
+ - [Create a Mudis Web Cache Server](#create-a-mudis-web-cache-server)
52
+ - [Project Philosophy](#project-philosophy)
53
+ - [Roadmap](#roadmap)
54
+
55
+ ---
56
+
57
+ ### Why another Caching Gem?
58
+
59
+ There are plenty out there, in various states of maintenance and in many shapes and sizes. So why on earth do we need another? I needed a drop-in replacement for Kredis, and the reason I was interested in using Kredis was for the simplified API and keyed management it gave me in extension to Redis. But what I didn't really need was Redis. I needed an observable, fast, simple, easy to use, flexible and highly configurable, thread-safe and high performant caching system which didn't require too many dependencies or standing up additional services. So, Mudis was born. In its most rudimentary state it was extremely useful in my project, which was an API gateway connecting into mutliple micro-services and a wide selection of APIs. The majority of the data was cold and produced by repeat expensive queries across several domains. Mudis allowed for me to minimize the footprint of the gateway, and improve end user experience, and increase performance. So, yeah, there's a lot of these gems out there, but none which really met all my needs. I decided to provide Mudis for anyone else. If you use it, I'd be interested to know how and whether you got any benefit.
60
+
61
+ #### Similar Gems
62
+
63
+ - [FastCache](https://github.com/swoop-inc/fast_cache)
64
+ - [EasyCache](https://github.com/malvads/easycache)
65
+ - [MiniCache](https://github.com/derrickreimer/mini_cache)
66
+ - [Zache](https://github.com/yegor256/zache)
67
+
68
+ #### Feature / Function Comparison
69
+
70
+ | **Feature** | **Mudis** | **MemoryStore** (`Rails.cache`) | **FastCache** | **Zache** | **EasyCache** | **MiniCache** |
71
+ | -------------------------------------- | ---------------- | ------------------------------- | -------------- | ------------- | ------------- | -------------- |
72
+ | **LRU eviction strategy** | ✅ Per-bucket | Global | Global | ❌ | ❌ | ✅ Simplistic |
73
+ | **TTL expiry support** | ✅ | | ✅ | ✅ | | |
74
+ | **Background expiry cleanup thread** | ✅ | ❌ (only on access) | ❌ | ✅ | ❌ | ❌ |
75
+ | **Thread safety** | ✅ Bucketed | ⚠️ Global lock | ✅ Fine-grained | | ⚠️ | ⚠️ |
76
+ | **Sharding (buckets)** | ✅ | | | ❌ | ❌ | ❌ |
77
+ | **Custom serializers** | ✅ | | ❌ | ❌ | ❌ | ❌ |
78
+ | **Compression (Zlib)** | ✅ | | ❌ | ❌ | ❌ | ❌ |
79
+ | **Hard memory cap** | ✅ | | ❌ | ❌ | ❌ | ❌ |
80
+ | **Max value size enforcement** | ✅ | | | | | |
81
+ | **Metrics (hits, misses, evictions)** | ✅ | ⚠️ Partial | ❌ | ❌ | ❌ | ❌ |
82
+ | **Fetch/update pattern** | ✅ Full | ✅ Standard | ⚠️ Partial | ✅ Basic | ✅ Basic | ✅ Basic |
83
+ | **Namespacing** | ✅ | ✅ | | | | |
84
+ | **Replace (if exists)** | ✅ | | ❌ | ❌ | ❌ | ❌ |
85
+ | **Clear/delete method** | ✅ | | ✅ | | | |
86
+ | **Key inspection with metadata** | ✅ | | | | | |
87
+ | **Concurrency model** | ✅ | | ✅ | | | ❌ |
88
+ | **Maintenance level** | ✅ | | | ⚠️ | ⚠️ | ⚠️ |
89
+ | **Suitable for APIs or microservices** | ✅ | ⚠️ Limited | ✅ | ⚠️ Small apps | ⚠️ Small apps | ❌ |
90
+ | **Inter-process Caching** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
91
+ | **Warm boot data snapshot** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
92
+
93
+ ---
94
+
95
+ ## Design
96
+
97
+ #### Internal Structure and Behaviour
98
+
99
+ ![mudis_flow](design/mudis_obj.png "Mudis Internals")
100
+
101
+ #### Write - Read - Eviction
102
+
103
+ ![mudis_flow](design/mudis_flow.png "Write - Read - Eviction")
104
+
105
+ #### Cache Key Lifecycle
106
+
107
+ ![mudis_lru](design/mudis_lru.png "Mudis Cache Key Lifecycle")
108
+
109
+ ---
110
+
111
+ ## Features
112
+
113
+ - **Thread-safe**: Uses per-bucket mutexes for high concurrency.
114
+ - **Sharded**: Buckets data across multiple internal stores to minimize lock contention.
115
+ - **LRU Eviction**: Automatically evicts least recently used items as memory fills up.
116
+ - **Expiry Support**: Optional TTL per key with background cleanup thread.
117
+ - **Compression**: Optional Zlib compression for large values.
118
+ - **Metrics**: Tracks hits, misses, and evictions.
119
+ - **IPC Mode**: Shared cross-process caching for multi-process aplications.
120
+ - **Soft-persistence**: Data snapshot and reload.
121
+
122
+ ---
123
+
124
+ ## Installation
125
+
126
+ Add this line to your Gemfile:
127
+
128
+ ```ruby
129
+ gem 'mudis'
130
+ ```
131
+
132
+ Or install it manually:
133
+
134
+ ```bash
135
+ gem install mudis
136
+ ```
137
+ ---
138
+
139
+ ## Configuration (Ruby/Rails)
140
+
141
+ In your Rails app, create an initializer (in your entry point if using Ruby outside of Rails):
142
+
143
+ ```ruby
144
+ Mudis.configure do |c|
145
+ c.serializer = JSON # or Marshal | Oj
146
+ c.compress = true # Compress values using Zlib
147
+ c.max_value_bytes = 2_000_000 # Reject values > 2MB
148
+ c.hard_memory_limit = true # enforce hard memory limits
149
+ c.max_bytes = 1_073_741_824 # set maximum cache size
150
+ end
151
+
152
+ Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
153
+
154
+ at_exit do
155
+ Mudis.stop_expiry_thread
156
+ end
157
+ ```
158
+
159
+ Or with direct setters:
160
+
161
+ ```ruby
162
+ Mudis.serializer = JSON # or Marshal | Oj
163
+ Mudis.compress = true # Compress values using Zlib
164
+ Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
165
+ Mudis.hard_memory_limit = true # enforce hard memory limits
166
+ Mudis.max_bytes = 1_073_741_824 # set maximum cache size
167
+
168
+ Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
169
+
170
+ ## set at exit hook
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Configuration (Hanami)
176
+
177
+ Mudis integrates seamlessly with [Hanami](https://hanamirb.org) applications. It provides the same configuration flexibility and thread-safe caching capabilities as in Rails, with minimal setup differences.
178
+
179
+ Create a boot file:
180
+
181
+ ```ruby
182
+ # config/boot/mudis.rb
183
+ require "mudis"
184
+
185
+ Mudis.configure do |c|
186
+ c.serializer = JSON
187
+ c.compress = true
188
+ c.max_value_bytes = 2_000_000
189
+ c.hard_memory_limit = true
190
+ c.max_bytes = 1_073_741_824
191
+ end
192
+
193
+ Mudis.start_expiry_thread(interval: 60)
194
+
195
+ at_exit { Mudis.stop_expiry_thread }
196
+ ```
197
+
198
+ Then require it from `config/app.rb`:
199
+
200
+ ```ruby
201
+ # config/app.rb
202
+ require_relative "./boot/mudis"
203
+
204
+ module MyApp
205
+ class App < Hanami::App
206
+ end
207
+ end
208
+ ```
209
+
210
+ This ensures Mudis is initialized and available globally throughout your Hanami application in the same as it would in Rails.
211
+
212
+ ---
213
+
214
+ ### Using Mudis with Hanami’s Dependency Container
215
+
216
+ You can register Mudis as a dependency in the Hanami container to access it via dependency injection:
217
+
218
+ ```ruby
219
+ # config/container.rb
220
+ require "mudis"
221
+
222
+ MyApp::Container.register(:cache, Mudis)
223
+ ```
224
+
225
+ Then use it inside your actions, repositories, or services:
226
+
227
+ ```ruby
228
+ # apps/main/actions/users/show.rb
229
+ module Main
230
+ module Actions
231
+ module Users
232
+ class Show < Main::Action
233
+ include Deps[cache: "cache"]
234
+
235
+ def handle(req, res)
236
+ res[:user] = cache.fetch("user:#{req.params[:id]}", expires_in: 60) do
237
+ UserRepo.new.find(req.params[:id])
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Start and Stop Exipry Thread
249
+
250
+ The expiry thread is not triggered automatically when your application starts. You must add the start and stop in the respective process hooks.
251
+
252
+ ### Starting Exipry Thread
253
+
254
+ To enable background expiration and removal, you must start the expiry thread at start up after configuration.
255
+
256
+ ```ruby
257
+ Mudis.start_expiry_thread(interval: 60)
258
+ ```
259
+
260
+ ### Graceful Shutdown
261
+
262
+ Don’t forget to stop the expiry thread in your exit hook when your app exits:
263
+
264
+ ```ruby
265
+ at_exit { Mudis.stop_expiry_thread }
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Basic Usage
271
+
272
+ ```ruby
273
+ require 'mudis'
274
+
275
+ # Write a value with optional TTL
276
+ Mudis.write('user:123', { name: 'Alice' }, expires_in: 600)
277
+
278
+ # Write a value with explicit namespace separation
279
+ Mudis.write('123', { name: 'Alice' }, expires_in: 600, namespace:'user')
280
+
281
+ # Read it back
282
+ Mudis.read('user:123') # => { "name" => "Alice" }
283
+
284
+ # Read back from namespace
285
+ Mudis.read('123', namespace:'user') # => { "name" => "Alice" }
286
+
287
+ # Check if it exists
288
+ Mudis.exists?('user:123') # => true
289
+
290
+ # Atomically update
291
+ Mudis.update('user:123') { |data| data.merge(age: 30) }
292
+
293
+ # Delete a key
294
+ Mudis.delete('user:123')
295
+ ```
296
+
297
+ ### Developer Utilities
298
+
299
+ Mudis provides utility methods to help with test environments, console debugging, and dev tool resets.
300
+
301
+ #### `Mudis.reset!`
302
+ Clears the internal cache state. Including all keys, memory tracking, and metrics. Also stops the expiry thread.
303
+
304
+ ```ruby
305
+ Mudis.write("foo", "bar")
306
+ Mudis.reset!
307
+ Mudis.read("foo") # => nil
308
+ ```
309
+
310
+ - Wipe all buckets (@stores, @lru_nodes, @current_bytes)
311
+ - Reset all metrics (:hits, :misses, :evictions, :rejected)
312
+ - Stop any running background expiry thread
313
+
314
+ #### `Mudis.reset_metrics!`
315
+
316
+ Clears only the metric counters and preserves all cached values.
317
+
318
+ ```ruby
319
+ Mudis.write("key", "value")
320
+ Mudis.read("key") # => "value"
321
+ Mudis.metrics # => { hits: 1, misses: 0, ... }
322
+
323
+ Mudis.reset_metrics!
324
+ Mudis.metrics # => { hits: 0, misses: 0, ... }
325
+ Mudis.read("key") # => "value" (still cached)
326
+ ```
327
+
328
+ #### `Mudis.least_touched`
329
+
330
+ Returns the top `n` (or all) keys that have been read the fewest number of times, across all buckets. This is useful for identifying low-value cache entries that may be safe to remove or exclude from caching altogether.
331
+
332
+ Each result includes the full key and its access count.
333
+
334
+ ```ruby
335
+ Mudis.least_touched
336
+ # => [["foo", 0], ["user:42", 1], ["product:123", 2], ...]
337
+
338
+ Mudis.least_touched(5)
339
+ # => returns top 5 least accessed keys
340
+ ```
341
+
342
+ #### `Mudis.keys(namespace:)`
343
+
344
+ Returns all keys for a given namespace.
345
+
346
+ ```ruby
347
+ Mudis.write("u1", "alpha", namespace: "users")
348
+ Mudis.write("u2", "beta", namespace: "users")
349
+
350
+ Mudis.keys(namespace: "users")
351
+ # => ["u1", "u2"]
352
+
353
+ ```
354
+
355
+ #### `Mudis.clear_namespace(namespace:)`
356
+
357
+ Deletes all keys within a namespace.
358
+
359
+ ```ruby
360
+ Mudis.clear_namespace("users")
361
+ Mudis.read("u1", namespace: "users") # => nil
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Rails Service Integration
367
+
368
+ For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
369
+
370
+ ```ruby
371
+ class MudisService
372
+ attr_reader :cache_key, :namespace
373
+
374
+ # Initialize the service with a cache key and optional namespace
375
+ #
376
+ # @param cache_key [String] the base key to use
377
+ # @param namespace [String, nil] optional logical namespace
378
+ def initialize(cache_key, namespace: nil)
379
+ @cache_key = cache_key
380
+ @namespace = namespace
381
+ end
382
+
383
+ # Write a value to the cache
384
+ #
385
+ # @param data [Object] the value to cache
386
+ # @param expires_in [Integer, nil] optional TTL in seconds
387
+ def write(data, expires_in: nil)
388
+ Mudis.write(cache_key, data, expires_in: expires_in, namespace: namespace)
389
+ end
390
+
391
+ # Read the cached value or return default
392
+ #
393
+ # @param default [Object] fallback value if key is not present
394
+ def read(default: nil)
395
+ Mudis.read(cache_key, namespace: namespace) || default
396
+ end
397
+
398
+ # Update the cached value using a block
399
+ #
400
+ # @yieldparam current [Object] the current value
401
+ # @yieldreturn [Object] the updated value
402
+ def update
403
+ Mudis.update(cache_key, namespace: namespace) { |current| yield(current) }
404
+ end
405
+
406
+ # Delete the key from cache
407
+ def delete
408
+ Mudis.delete(cache_key, namespace: namespace)
409
+ end
410
+
411
+ # Return true if the key exists in cache
412
+ def exists?
413
+ Mudis.exists?(cache_key, namespace: namespace)
414
+ end
415
+
416
+ # Fetch from cache or compute and store it
417
+ #
418
+ # @param expires_in [Integer, nil] optional TTL
419
+ # @param force [Boolean] force recomputation
420
+ # @yield return value if key is missing
421
+ def fetch(expires_in: nil, force: false)
422
+ Mudis.fetch(cache_key, expires_in: expires_in, force: force, namespace: namespace) do
423
+ yield
424
+ end
425
+ end
426
+
427
+ # Inspect metadata for the current key
428
+ #
429
+ # @return [Hash, nil] metadata including :expires_at, :created_at, :size_bytes, etc.
430
+ def inspect_meta
431
+ Mudis.inspect(cache_key, namespace: namespace)
432
+ end
433
+ end
434
+
435
+ ```
436
+
437
+ Use it like:
438
+
439
+ ```ruby
440
+ cache = MudisService.new("user:42:profile", namespace: "users")
441
+
442
+ cache.write({ name: "Alice" }, expires_in: 300)
443
+ cache.read # => { "name" => "Alice" }
444
+ cache.exists? # => true
445
+
446
+ cache.update { |data| data.merge(age: 30) }
447
+ cache.fetch(expires_in: 60) { expensive_query }
448
+ cache.inspect_meta # => { key: "users:user:42:profile", ... }
449
+ ```
450
+
451
+ *This pattern can also be applied in Hanami services using the registered Mudis dependency*
452
+
453
+ ---
454
+
455
+ ## Metrics
456
+
457
+ Track cache effectiveness and performance:
458
+
459
+ ```ruby
460
+ Mudis.metrics
461
+ # => {
462
+ # hits: 15,
463
+ # misses: 5,
464
+ # evictions: 3,
465
+ # rejected: 0,
466
+ # total_memory: 45678,
467
+ # least_touched: [
468
+ # ["user:1", 0],
469
+ # ["post:5", 1],
470
+ # ...
471
+ # ],
472
+ # buckets: [
473
+ # { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
474
+ # ...
475
+ # ]
476
+ # }
477
+
478
+ ```
479
+
480
+ Optionally, expose Mudis metrics from a controller or action for remote analysis and monitoring.
481
+
482
+ **Rails:**
483
+
484
+ ```ruby
485
+ class MudisController < ApplicationController
486
+ def metrics
487
+ render json: { mudis: Mudis.metrics }
488
+ end
489
+
490
+ end
491
+ ```
492
+
493
+ **Hanami:**
494
+
495
+ *Mudis Registered in the container*
496
+
497
+ ```ruby
498
+ # apps/main/actions/metrics/show.rb
499
+ module Main::Actions::Metrics
500
+ class Show < Main::Action
501
+ include Deps[cache: "cache"]
502
+
503
+ def handle(*, res)
504
+ res.format = :json
505
+ res.body = { mudis: cache.metrics }.to_json
506
+ end
507
+ end
508
+ end
509
+ ```
510
+
511
+ *Mudis not registered in the container*
512
+
513
+ ```ruby
514
+ # apps/main/actions/metrics/show.rb
515
+ module Main::Actions::Metrics
516
+ class Show < Main::Action
517
+ def handle(*, res)
518
+ res.format = :json
519
+ res.body = { mudis: Mudis.metrics }.to_json
520
+ end
521
+ end
522
+ end
523
+ ```
524
+
525
+ ```ruby
526
+ # config/routes.rb
527
+ module Main
528
+ class Routes < Hanami::Routes
529
+ root { "OK" }
530
+
531
+ get "/metrics", to: "metrics.show"
532
+ end
533
+ end
534
+ ```
535
+
536
+ ---
537
+
538
+ ## Advanced Configuration
539
+
540
+ | Setting | Description | Default |
541
+ |--------------------------|---------------------------------------------|--------------------|
542
+ | `Mudis.serializer` | JSON, Marshal, or Oj | `JSON` |
543
+ | `Mudis.compress` | Enable Zlib compression | `false` |
544
+ | `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
545
+ | `Mudis.buckets` | Number of cache shards | `32` |
546
+ | `Mudis.start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
547
+ | `Mudis.hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
548
+ | `Mudis.max_bytes` | Maximum allowed cache size | `1GB`|
549
+ | `Mudis.max_ttl` | Set the maximum permitted TTL | `nil` (no limit) |
550
+ | `Mudis.default_ttl` | Set the default TTL for fallback when none is provided | `nil` |
551
+
552
+ Buckets can also be set using a `MUDIS_BUCKETS` environment variable.
553
+
554
+ When setting `serializer`, be mindful of the below
555
+
556
+ | Serializer | Recommended for |
557
+ | ---------- | ------------------------------------- |
558
+ | `Marshal` | Ruby-only apps, speed-sensitive logic |
559
+ | `JSON` | Cross-language interoperability |
560
+ | `Oj` | API-heavy apps using JSON at scale |
561
+
562
+ ---
563
+
564
+ ## Soft Persistence (Snapshots)
565
+
566
+ Originally, Mudis was not designed to provide any form of persistence. But I recently had a situation where warm data recovery post-crash reboot was necessary.This could be achieved with a parallel service or peripheral worker, but I decided instead to address this challenge for my own purposes by expanding Mudis to optionally soft-persist its in-memory cache to disk between process restarts.
567
+
568
+ When enabled, it will automatically:
569
+
570
+ - **Save** a snapshot of the current cache at shutdown
571
+ - **Reload** it on startup
572
+
573
+ This feature is **off by default** to preserve existing configurations and can be enabled as below:
574
+
575
+ ```ruby
576
+ Mudis.configure do |config|
577
+ config.persistence_enabled = true
578
+ config.persistence_path = "tmp/mudis_snapshot.dump"
579
+ config.persistence_format = :marshal # or :json
580
+ config.persistence_safe_write = true # atomic temp write + rename
581
+ end
582
+
583
+ ## Optionally install the exit hook manually (usually automatic)
584
+ # Mudis.install_persistence_hook!
585
+ ```
586
+
587
+ From your startup routine:
588
+
589
+ ```ruby
590
+ # Restore snapshot on startup
591
+ Mudis.load_snapshot!
592
+ ```
593
+
594
+ *This feature works identically in Rails, Hanami, and standalone Ruby scripts, as long as `Mudis.configure` is called prior to `Mudis.load_snapshot!`.*
595
+
596
+ ### Behavior
597
+
598
+ | Operation | Description |
599
+ |------------|-------------|
600
+ | `save_snapshot!` | Dumps all non-expired cache entries to disk. |
601
+ | `load_snapshot!` | Reads snapshot and restores entries via normal write path. |
602
+ | `install_persistence_hook!` | Registers an `at_exit` hook to automatically save on process exit. |
603
+
604
+ #### Notes
605
+
606
+ - Disabled by default for backward compatibility.
607
+ - Uses Ruby’s `Marshal` by default for fastest serialization; `:json` also supported.
608
+ - Respects TTLs: expired entries are never written to disk.
609
+ - Thread-safe: snapshots are taken under shard-level locks.
610
+ - Safe write: when `persistence_safe_write = true`, Mudis writes to a temp file and renames it atomically.
611
+
612
+ #### Example Flow
613
+
614
+ ![mudis_persistence](design/mudis_persistence.png "Mudis Persistence Strategy")
615
+
616
+ 1. On startup, `Mudis.load_snapshot!` repopulates the cache.
617
+ 2. Your app uses the cache as normal (`write`, `read`, `fetch`, etc.).
618
+ 3. When the process exits, `at_exit` automatically triggers `save_snapshot!`.
619
+ 4. A file (e.g., `tmp/mudis_snapshot.dump`) holds your persisted cache.
620
+
621
+ #### Safety
622
+
623
+ If the snapshot file is **missing** or **corrupted**, Mudis will:
624
+ - Log a warning via `warn "[Mudis] Failed to load snapshot..."`, and
625
+ - Continue booting normally with an empty cache.
626
+
627
+ ---
628
+
629
+ ## Inter-Process Caching (IPC Mode)
630
+
631
+ While Mudis was originally designed as an in-process cache, it can also operate as a shared inter-process cache when running in environments that use concurrent processes (such as Puma in cluster mode). This is achieved through a local UNIX socket server that allows all workers to access a single, centralized Mudis instance. When running otherwise as a per process (worker) singleton you may encounter oddities or duplicate cache data.
632
+
633
+ ### Overview
634
+
635
+ In IPC mode, Mudis runs as a singleton server within the master process.
636
+
637
+ Each worker connects to that server through a lightweight client (`MudisClient`) using a local UNIX domain socket (default: `/tmp/mudis.sock`).
638
+ All cache operations, e.g., read, write, delete, fetch, etc., are transparently proxied to the master process, which holds the authoritative cache state.
639
+
640
+ This design allows multiple workers to share the same cache without duplicating memory or losing synchronization, while retaining Mudis’ performance, configurability, and thread safety.
641
+
642
+ | **Benefit** | **Description** |
643
+ | --------------------------------- | ---------------------------------------------------------------------------------------- |
644
+ | **Shared Cache Across Processes** | All Puma workers share one Mudis instance via IPC. |
645
+ | **Zero External Dependencies** | No Redis, Memcached, or separate daemon required. |
646
+ | **Memory Efficient** | Cache data stored only once, not duplicated per worker. |
647
+ | **Full Feature Support** | All Mudis features (TTL, compression, metrics, etc.) work transparently. |
648
+ | **Safe & Local** | Communication is limited to the host system’s UNIX socket, ensuring isolation and speed. |
649
+
650
+ ![mudis_ipc](design/mudis_ipc.png "Mudis IPC")
651
+
652
+ ### Setup (Puma)
653
+
654
+ Enable IPC mode by adding the following to your Puma configuration:
655
+
656
+ ```ruby
657
+ # config/puma.rb
658
+ preload_app!
659
+
660
+ before_fork do
661
+ require "mudis"
662
+ require "mudis_server"
663
+
664
+ # typical Mudis configuration
665
+ Mudis.configure do |c|
666
+ c.serializer = JSON
667
+ c.compress = true
668
+ c.max_value_bytes = 2_000_000
669
+ c.hard_memory_limit = true
670
+ c.max_bytes = 1_073_741_824
671
+ end
672
+
673
+ Mudis.start_expiry_thread(interval: 60)
674
+ MudisServer.start!
675
+
676
+ ## any start up services or background workers
677
+ # which require the cache should be started here
678
+ at_exit { Mudis.stop_expiry_thread }
679
+ end
680
+
681
+ on_worker_boot do
682
+ require "mudis_client"
683
+ $mudis = MudisClient.new
684
+ require "mudis_proxy" # optionally require the default mudis proxy to invisibly patch Mudis
685
+
686
+ end
687
+ ```
688
+
689
+ For more granular control over Mudis, adding a custom Proxy manually to `initializers` (Rails) or `boot` (Hanami) allows seamless use of the API as documented.
690
+
691
+ **Do not require `mudis_proxy` if following this method**
692
+
693
+ Example custom proxy:
694
+
695
+ ```ruby
696
+ # config/<<initializers|boot>>/mudis_proxy.rb
697
+ if defined?($mudis) && $mudis
698
+ class Mudis
699
+ def self.read(*a, **k) = $mudis.read(*a, **k)
700
+ def self.write(*a, **k) = $mudis.write(*a, **k)
701
+ def self.delete(*a, **k) = $mudis.delete(*a, **k)
702
+ def self.fetch(*a, **k, &b) = $mudis.fetch(*a, **k, &b)
703
+ def self.metrics = $mudis.metrics
704
+ def self.reset_metrics! = $mudis.reset_metrics!
705
+ def self.reset! = $mudis.reset!
706
+ end
707
+
708
+ end
709
+ ```
710
+
711
+ **Use IPC mode when:**
712
+
713
+ - Running Rails or Rack apps under Puma cluster or multi-process background job workers.
714
+ - You need cache consistency and memory efficiency without standing up Redis.
715
+ - You want to preserve Mudis’s observability, configurability, and in-process simplicity at a larger scale.
716
+
717
+ ---
718
+
719
+ ## Benchmarks
720
+
721
+ #### Serializer(s)
722
+
723
+ _100000 iterations_
724
+
725
+ | Serializer | Total Time (s) | Ops/sec |
726
+ |----------------|------------|----------------|
727
+ | oj | 0.1342 | 745320 |
728
+ | marshal | 0.3228 | 309824 |
729
+ | json | 0.9035 | 110682 |
730
+ | oj + zlib | 1.8050 | 55401 |
731
+ | marshal + zlib | 1.8057 | 55381 |
732
+ | json + zlib | 2.7949 | 35780 |
733
+
734
+ > If opting for OJ, you will need to install the dependency in your project and configure as needed.
735
+
736
+ #### Mudis vs Rails.cache
737
+
738
+ Mudis is marginally slower than `Rails.cache` by design; it trades raw speed for control, observability, and safety.
739
+
740
+ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON_
741
+
742
+ | Operation | `Rails.cache` | `Mudis` | Delta |
743
+ | --------- | ------------- | ----------- | --------- |
744
+ | Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
745
+ | Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
746
+
747
+ > For context: a typical database query or HTTP call takes 10–50ms. A difference of less than 1ms per operation is negligible for most apps.
748
+
749
+ #### **Why this overhead exists**
750
+
751
+ Mudis includes features that MemoryStore doesn’t:
752
+
753
+ | Feature | Mudis | Rails.cache (MemoryStore) |
754
+ | ------------------ | ---------------------- | --------------------------- |
755
+ | Per-key TTL expiry | ✅ | ⚠️ on access |
756
+ | True LRU eviction | | |
757
+ | Hard memory limits | ✅ | ❌ |
758
+ | Value compression | ✅ | ❌ |
759
+ | Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
760
+ | Observability | | |
761
+ | Namespacing | | Manual scoping |
762
+ | IPC Aware | (if enabled) | Requires manual configuration and additional gems |
763
+
764
+ It will be down to the developer to decide if a fraction of a millisecond is worth
765
+
766
+ - Predictable eviction
767
+ - Configurable expiry
768
+ - Memory protection
769
+ - Namespace scoping
770
+ - Real-time metrics for hits, misses, evictions, memory usage
771
+
772
+ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)_
773
+
774
+ | Operation | `Rails.cache` | `Mudis` | Delta |
775
+ | --------- | ------------- | ----------- | ------------- |
776
+ | Write | 2.342 ms/op | 0.501 ms/op | **−1.841 ms** |
777
+ | Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
778
+
779
+ With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
780
+
781
+ #### Other Benchmarks
782
+
783
+ _10000 iterations of 512KB, JSON, compression OFF (to match MemoryStore default)_
784
+
785
+ | Operation | `Rails.cache` | `Mudis` | Delta |
786
+ | --------- | ------------- | ----------- | ------------- |
787
+ | Write | 1.291 ms/op | 0.32 ms/op | **−0.971 ms** |
788
+ | Read | 0.011 ms/op | 0.048 ms/op | +0.037 ms |
789
+
790
+ _10000 iterations of 512KB, JSON, compression ON_
791
+
792
+ | Operation | `Rails.cache` | `Mudis` | Delta |
793
+ | --------- | ------------- | ----------- | ------------- |
794
+ | Write | 1.11 ms/op | 1.16 ms/op | +0.05 ms |
795
+ | Read | 0.07 ms/op | 0.563 ms/op | +0.493 ms |
796
+
797
+ ---
798
+
799
+ ## Known Limitations
800
+
801
+ - Data is **non-persistent**; only soft-persistence is optionally provided.
802
+ - No SQL or equivallent query interface for cached data. Data is per Key retrieval only.
803
+ - Compression introduces CPU overhead.
804
+
805
+ ---
806
+
807
+ ## Create a Mudis Web Cache Server
808
+
809
+ ### Minimal Setup
810
+
811
+ #### Example using Rails
812
+
813
+ - Create a new Rails API app:
814
+
815
+ ```bash
816
+ rails new mudis-server --api
817
+ cd mudis-server
818
+ ```
819
+
820
+ - Add mudis to your Gemfile
821
+ - Create Initializer: `config/initializers/mudis.rb`
822
+ - Define routes
823
+
824
+ ```ruby
825
+ Rails.application.routes.draw do
826
+ get "/cache/:key", to: "cache#show"
827
+ post "/cache/:key", to: "cache#write"
828
+ delete "/cache/:key", to: "cache#delete"
829
+ get "/metrics", to: "cache#metrics"
830
+ end
831
+ ```
832
+
833
+ - Create a `cache_controller` (with optional per caller/consumer namespace)
834
+
835
+ ```ruby
836
+ class CacheController < ApplicationController
837
+
838
+ def show
839
+ key = params[:key]
840
+ ns = params[:namespace]
841
+
842
+ value = Mudis.read(key, namespace: ns)
843
+ if value.nil?
844
+ render json: { error: "not found" }, status: :not_found
845
+ else
846
+ render json: { value: value }
847
+ end
848
+ end
849
+
850
+ def write
851
+ key = params[:key]
852
+ ns = params[:namespace]
853
+ val = params[:value]
854
+ ttl = params[:expires_in]&.to_i
855
+
856
+ Mudis.write(key, val, expires_in: ttl, namespace: ns)
857
+ render json: { status: "written", key: key }
858
+ end
859
+
860
+ def delete
861
+ key = params[:key]
862
+ ns = params[:namespace]
863
+
864
+ Mudis.delete(key, namespace: ns)
865
+ render json: { status: "deleted" }
866
+ end
867
+
868
+ def metrics
869
+ render json: Mudis.metrics
870
+ end
871
+ end
872
+ ```
873
+
874
+ - Test it
875
+
876
+ ```bash
877
+ curl http://localhost:3000/cache/foo
878
+ curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
879
+ curl http://localhost:3000/metrics
880
+
881
+ # Write with namespace
882
+ curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
883
+ -d "value=123&expires_in=60"
884
+
885
+ # Read from namespace
886
+ curl "http://localhost:3000/cache/foo?namespace=orders"
887
+
888
+ # Delete from namespace
889
+ curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
890
+
891
+ ```
892
+
893
+ ---
894
+
895
+ ## Project Philosophy
896
+
897
+ Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
898
+
899
+ - In-process caching
900
+ - Fine-grained memory and namespace control
901
+ - Observability and testing friendliness
902
+ - Minimal external dependencies
903
+ - Configurability without complexity
904
+
905
+ The primary use cases are:
906
+
907
+ - Per-service application caches
908
+ - Short-lived local caching inside background jobs or API layers
909
+
910
+ Mudis is not intended to be a general-purpose, distributed caching platform. You are, however, welcome to build on top of Mudis if you want its functionality in such projects. E.g.,
911
+
912
+ - mudis-web-cache-server expose Mudis via HTTP, web sockets, hooks, etc
913
+ - mudis-broker – distributed key routing layer for coordinating multiple Mudis nodes
914
+ - mudis-activejob-store – adapter for using Mudis in job queues or retry buffers
915
+
916
+ ---
917
+
918
+ ## Roadmap
919
+
920
+ #### API Enhancements
921
+
922
+ - [x] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
923
+
924
+ #### Safety & Policy Controls
925
+
926
+ - [x] max_ttl: Enforce a global upper bound on expires_in to prevent excessively long-lived keys
927
+ - [x] default_ttl: Provide a fallback TTL when one is not specified
928
+
929
+ #### Debugging
930
+
931
+ - [x] clear_namespace(namespace): Remove all keys in a namespace in one call
932
+
933
+ #### Refactor Mudis
934
+
935
+ - [x] Review Mudis for improved readability and reduce complexity in top-level functions
936
+ - [x] Enhanced guards
937
+ - [ ] Refactor main classes for enhanced readability
938
+ - [ ] Review for functionality gaps and enhance as needed
939
+
940
+ ---
941
+
942
+ ## License
943
+
944
+ MIT License © kiebor81
945
+
946
+ ---
947
+
948
+ ## Contributing
949
+
950
+ See [contributor's guide](CONTRIBUTING.md)
951
+
952
+ ---
953
+
954
+ ## Contact
955
+
956
+ For issues, suggestions, or feedback, please open a GitHub issue
957
+
958
+ ---