mudis 0.6.0 → 0.7.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,590 +1,695 @@
1
- ![mudis_signet](design/mudis.png "Mudis")
2
-
3
- [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1&cachebust=0)](https://badge.fury.io/rb/mudis)
4
-
5
- **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.
6
-
7
- 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.
8
-
9
- Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a [Mudis server](#create-a-mudis-server).
10
-
11
- ### Why another Caching Gem?
12
-
13
- 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.
14
-
15
- #### Similar Gems
16
-
17
- - [FastCache](https://github.com/swoop-inc/fast_cache)
18
- - [EasyCache](https://github.com/malvads/easycache)
19
- - [MiniCache](https://github.com/derrickreimer/mini_cache)
20
- - [Zache](https://github.com/yegor256/zache)
21
-
22
- #### Feature / Function Comparison
23
-
24
- | **Feature** | **Mudis** | **MemoryStore** (`Rails.cache`) | **FastCache** | **Zache** | **EasyCache** | **MiniCache** |
25
- | -------------------------------------- | ---------------- | ------------------------------- | -------------- | ------------- | ------------- | -------------- |
26
- | **LRU eviction strategy** | Per-bucket | Global | Global | | | Simplistic |
27
- | **TTL expiry support** | ✅ | ✅ | ✅ | | | ✅ |
28
- | **Background expiry cleanup thread** | ✅ | ❌ (only on access) | | ✅ | | |
29
- | **Thread safety** | ✅ Bucketed | ⚠️ Global lock | ✅ Fine-grained | ✅ | ⚠️ | ⚠️ |
30
- | **Sharding (buckets)** | ✅ | | ✅ | | | |
31
- | **Custom serializers** | ✅ | | | ❌ | ❌ | ❌ |
32
- | **Compression (Zlib)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
33
- | **Hard memory cap** | ✅ | | ❌ | ❌ | ❌ | ❌ |
34
- | **Max value size enforcement** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
35
- | **Metrics (hits, misses, evictions)** | ✅ | ⚠️ Partial | ❌ | ❌ | ❌ | ❌ |
36
- | **Fetch/update pattern** | Full | ✅ Standard | ⚠️ Partial | Basic | ✅ Basic | ✅ Basic |
37
- | **Namespacing** | ✅ | ✅ | | | | |
38
- | **Replace (if exists)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
39
- | **Clear/delete method** | ✅ | ✅ | | | | |
40
- | **Key inspection with metadata** | ✅ | | | | | |
41
- | **Concurrency model** | ✅ | ❌ | | ❌ | ❌ | ❌ |
42
- | **Maintenance level** | ✅ | | ✅ | ⚠️ | ⚠️ | ⚠️ |
43
- | **Suitable for APIs or microservices** | ✅ | ⚠️ Limited | ✅ | ⚠️ Small apps | ⚠️ Small apps | |
44
-
45
- ---
46
-
47
- ## Design
48
-
49
- #### Internal Structure and Behaviour
50
-
51
- ![mudis_flow](design/mudis_obj.png "Mudis Internals")
52
-
53
- #### Write - Read - Eviction
54
-
55
- ![mudis_flow](design/mudis_flow.png "Write - Read - Eviction")
56
-
57
- #### Cache Key Lifecycle
58
-
59
- ![mudis_lru](design/mudis_lru.png "Mudis Cache Key Lifecycle")
60
-
61
- ---
62
-
63
- ## Features
64
-
65
- - **Thread-safe**: Uses per-bucket mutexes for high concurrency.
66
- - **Sharded**: Buckets data across multiple internal stores to minimize lock contention.
67
- - **LRU Eviction**: Automatically evicts least recently used items as memory fills up.
68
- - **Expiry Support**: Optional TTL per key with background cleanup thread.
69
- - **Compression**: Optional Zlib compression for large values.
70
- - **Metrics**: Tracks hits, misses, and evictions.
71
-
72
- ---
73
-
74
- ## Installation
75
-
76
- Add this line to your Gemfile:
77
-
78
- ```ruby
79
- gem 'mudis'
80
- ```
81
-
82
- Or install it manually:
83
-
84
- ```bash
85
- gem install mudis
86
- ```
87
-
88
- ---
89
-
90
- ## Configuration (Rails)
91
-
92
- In your Rails app, create an initializer:
93
-
94
- ```ruby
95
- # config/initializers/mudis.rb
96
- Mudis.configure do |c|
97
- c.serializer = JSON # or Marshal | Oj
98
- c.compress = true # Compress values using Zlib
99
- c.max_value_bytes = 2_000_000 # Reject values > 2MB
100
- c.hard_memory_limit = true # enforce hard memory limits
101
- c.max_bytes = 1_073_741_824 # set maximum cache size
102
- end
103
-
104
- Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
105
-
106
- at_exit do
107
- Mudis.stop_expiry_thread
108
- end
109
- ```
110
-
111
- Or with direct setters:
112
-
113
- ```ruby
114
- Mudis.serializer = JSON # or Marshal | Oj
115
- Mudis.compress = true # Compress values using Zlib
116
- Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
117
- Mudis.hard_memory_limit = true # enforce hard memory limits
118
- Mudis.max_bytes = 1_073_741_824 # set maximum cache size
119
-
120
- Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
121
-
122
- ## set at exit hook
123
- ```
124
-
125
- ---
126
-
127
- ## Basic Usage
128
-
129
- ```ruby
130
- require 'mudis'
131
-
132
- # Write a value with optional TTL
133
- Mudis.write('user:123', { name: 'Alice' }, expires_in: 600)
134
-
135
- # Read it back
136
- Mudis.read('user:123') # => { "name" => "Alice" }
137
-
138
- # Check if it exists
139
- Mudis.exists?('user:123') # => true
140
-
141
- # Atomically update
142
- Mudis.update('user:123') { |data| data.merge(age: 30) }
143
-
144
- # Delete a key
145
- Mudis.delete('user:123')
146
- ```
147
-
148
- ### Developer Utilities
149
-
150
- Mudis provides utility methods to help with test environments, console debugging, and dev tool resets.
151
-
152
- #### `Mudis.reset!`
153
- Clears the internal cache state. Including all keys, memory tracking, and metrics. Also stops the expiry thread.
154
-
155
- ```ruby
156
- Mudis.write("foo", "bar")
157
- Mudis.reset!
158
- Mudis.read("foo") # => nil
159
- ```
160
-
161
- - Wipe all buckets (@stores, @lru_nodes, @current_bytes)
162
- - Reset all metrics (:hits, :misses, :evictions, :rejected)
163
- - Stop any running background expiry thread
164
-
165
- #### `Mudis.reset_metrics!`
166
-
167
- Clears only the metric counters and preserves all cached values.
168
-
169
- ```ruby
170
- Mudis.write("key", "value")
171
- Mudis.read("key") # => "value"
172
- Mudis.metrics # => { hits: 1, misses: 0, ... }
173
-
174
- Mudis.reset_metrics!
175
- Mudis.metrics # => { hits: 0, misses: 0, ... }
176
- Mudis.read("key") # => "value" (still cached)
177
- ```
178
-
179
- #### `Mudis.least_touched`
180
-
181
- 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.
182
-
183
- Each result includes the full key and its access count.
184
-
185
- ```ruby
186
- Mudis.least_touched
187
- # => [["foo", 0], ["user:42", 1], ["product:123", 2], ...]
188
-
189
- Mudis.least_touched(5)
190
- # => returns top 5 least accessed keys
191
- ```
192
-
193
- #### `Mudis.keys(namespace:)`
194
-
195
- Returns all keys for a given namespace.
196
-
197
- ```ruby
198
- Mudis.write("u1", "alpha", namespace: "users")
199
- Mudis.write("u2", "beta", namespace: "users")
200
-
201
- Mudis.keys(namespace: "users")
202
- # => ["u1", "u2"]
203
-
204
- ```
205
-
206
- #### `Mudis.clear_namespace(namespace:)`
207
-
208
- Deletes all keys within a namespace.
209
-
210
- ```ruby
211
- Mudis.clear_namespace("users")
212
- Mudis.read("u1", namespace: "users") # => nil
213
- ```
214
-
215
- ---
216
-
217
- ## Rails Service Integration
218
-
219
- For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
220
-
221
- ```ruby
222
- class MudisService
223
- attr_reader :cache_key, :namespace
224
-
225
- # Initialize the service with a cache key and optional namespace
226
- #
227
- # @param cache_key [String] the base key to use
228
- # @param namespace [String, nil] optional logical namespace
229
- def initialize(cache_key, namespace: nil)
230
- @cache_key = cache_key
231
- @namespace = namespace
232
- end
233
-
234
- # Write a value to the cache
235
- #
236
- # @param data [Object] the value to cache
237
- # @param expires_in [Integer, nil] optional TTL in seconds
238
- def write(data, expires_in: nil)
239
- Mudis.write(cache_key, data, expires_in: expires_in, namespace: namespace)
240
- end
241
-
242
- # Read the cached value or return default
243
- #
244
- # @param default [Object] fallback value if key is not present
245
- def read(default: nil)
246
- Mudis.read(cache_key, namespace: namespace) || default
247
- end
248
-
249
- # Update the cached value using a block
250
- #
251
- # @yieldparam current [Object] the current value
252
- # @yieldreturn [Object] the updated value
253
- def update
254
- Mudis.update(cache_key, namespace: namespace) { |current| yield(current) }
255
- end
256
-
257
- # Delete the key from cache
258
- def delete
259
- Mudis.delete(cache_key, namespace: namespace)
260
- end
261
-
262
- # Return true if the key exists in cache
263
- def exists?
264
- Mudis.exists?(cache_key, namespace: namespace)
265
- end
266
-
267
- # Fetch from cache or compute and store it
268
- #
269
- # @param expires_in [Integer, nil] optional TTL
270
- # @param force [Boolean] force recomputation
271
- # @yield return value if key is missing
272
- def fetch(expires_in: nil, force: false)
273
- Mudis.fetch(cache_key, expires_in: expires_in, force: force, namespace: namespace) do
274
- yield
275
- end
276
- end
277
-
278
- # Inspect metadata for the current key
279
- #
280
- # @return [Hash, nil] metadata including :expires_at, :created_at, :size_bytes, etc.
281
- def inspect_meta
282
- Mudis.inspect(cache_key, namespace: namespace)
283
- end
284
- end
285
-
286
- ```
287
-
288
- Use it like:
289
-
290
- ```ruby
291
- cache = MudisService.new("user:42:profile", namespace: "users")
292
-
293
- cache.write({ name: "Alice" }, expires_in: 300)
294
- cache.read # => { "name" => "Alice" }
295
- cache.exists? # => true
296
-
297
- cache.update { |data| data.merge(age: 30) }
298
- cache.fetch(expires_in: 60) { expensive_query }
299
- cache.inspect_meta # => { key: "users:user:42:profile", ... }
300
- ```
301
-
302
- ---
303
-
304
- ## Metrics
305
-
306
- Track cache effectiveness and performance:
307
-
308
- ```ruby
309
- Mudis.metrics
310
- # => {
311
- # hits: 15,
312
- # misses: 5,
313
- # evictions: 3,
314
- # rejected: 0,
315
- # total_memory: 45678,
316
- # least_touched: [
317
- # ["user:1", 0],
318
- # ["post:5", 1],
319
- # ...
320
- # ],
321
- # buckets: [
322
- # { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
323
- # ...
324
- # ]
325
- # }
326
-
327
- ```
328
-
329
- Optionally, return these metrics from a controller for remote analysis and monitoring if using Rails.
330
-
331
- ```ruby
332
- class MudisController < ApplicationController
333
- def metrics
334
- render json: { mudis: Mudis.metrics }
335
- end
336
-
337
- end
338
- ```
339
-
340
- ---
341
-
342
- ## Advanced Configuration
343
-
344
- | Setting | Description | Default |
345
- |--------------------------|---------------------------------------------|--------------------|
346
- | `Mudis.serializer` | JSON, Marshal, or Oj | `JSON` |
347
- | `Mudis.compress` | Enable Zlib compression | `false` |
348
- | `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
349
- | `Mudis.buckets` | Number of cache shards | `32` |
350
- | `Mudis.start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
351
- | `Mudis.hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
352
- | `Mudis.max_bytes` | Maximum allowed cache size | `1GB`|
353
- | `Mudis.max_ttl` | Set the maximum permitted TTL | `nil` (no limit) |
354
- | `Mudis.default_ttl` | Set the default TTL for fallback when none is provided | `nil` |
355
-
356
- Buckets can also be set using a `MUDIS_BUCKETS` environment variable.
357
-
358
- When setting `serializer`, be mindful of the below
359
-
360
- | Serializer | Recommended for |
361
- | ---------- | ------------------------------------- |
362
- | `Marshal` | Ruby-only apps, speed-sensitive logic |
363
- | `JSON` | Cross-language interoperability |
364
- | `Oj` | API-heavy apps using JSON at scale |
365
-
366
- ---
367
-
368
- ## Benchmarks
369
-
370
- #### Serializer(s)
371
-
372
- _100000 iterations_
373
-
374
- | Serializer | Total Time (s) | Ops/sec |
375
- |----------------|------------|----------------|
376
- | oj | 0.1342 | 745320 |
377
- | marshal | 0.3228 | 309824 |
378
- | json | 0.9035 | 110682 |
379
- | oj + zlib | 1.8050 | 55401 |
380
- | marshal + zlib | 1.8057 | 55381 |
381
- | json + zlib | 2.7949 | 35780 |
382
-
383
- > If opting for OJ, you will need to install the dependency in your project and configure as needed.
384
-
385
- #### Mudis vs Rails.cache
386
-
387
- Mudis is marginally slower than `Rails.cache` by design; it trades raw speed for control, observability, and safety.
388
-
389
- _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON_
390
-
391
- | Operation | `Rails.cache` | `Mudis` | Delta |
392
- | --------- | ------------- | ----------- | --------- |
393
- | Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
394
- | Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
395
-
396
- > 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.
397
-
398
- ###### **Why this overhead exists**
399
-
400
- Mudis includes features that MemoryStore doesn’t:
401
-
402
- | Feature | Mudis | Rails.cache (MemoryStore) |
403
- | ------------------ | ---------------------- | --------------------------- |
404
- | Per-key TTL expiry | ✅ | ⚠️ on access |
405
- | True LRU eviction | ✅ | |
406
- | Hard memory limits | ✅ | ❌ |
407
- | Value compression | ✅ | ❌ |
408
- | Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
409
- | Observability | ✅ | |
410
- | Namespacing | ✅ | ❌ Manual scoping |
411
-
412
- It will be down to the developer to decide if a fraction of a millisecond is worth
413
-
414
- - Predictable eviction
415
- - Configurable expiry
416
- - Memory protection
417
- - Namespace scoping
418
- - Real-time metrics for hits, misses, evictions, memory usage
419
-
420
- _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)_
421
-
422
- | Operation | `Rails.cache` | `Mudis` | Delta |
423
- | --------- | ------------- | ----------- | ------------- |
424
- | Write | 2.342 ms/op | 0.501 ms/op | **−1.841 ms** |
425
- | Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
426
-
427
- With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
428
-
429
- ---
430
-
431
- ## Graceful Shutdown
432
-
433
- Don’t forget to stop the expiry thread when your app exits:
434
-
435
- ```ruby
436
- at_exit { Mudis.stop_expiry_thread }
437
- ```
438
-
439
- ---
440
-
441
- ## Known Limitations
442
-
443
- - Data is **non-persistent**.
444
- - Compression introduces CPU overhead.
445
-
446
- ---
447
-
448
- ## Create a Mudis Server
449
-
450
- ### Minimal Setup
451
-
452
- - Create a new Rails API app:
453
-
454
- ```bash
455
- rails new mudis-server --api
456
- cd mudis-server
457
- ```
458
-
459
- - Add mudis to your Gemfile
460
- - Create Initializer: `config/initializers/mudis.rb`
461
- - Define routes
462
-
463
- ```ruby
464
- Rails.application.routes.draw do
465
- get "/cache/:key", to: "cache#show"
466
- post "/cache/:key", to: "cache#write"
467
- delete "/cache/:key", to: "cache#delete"
468
- get "/metrics", to: "cache#metrics"
469
- end
470
- ```
471
-
472
- - Create a `cache_controller` (with optional per caller/consumer namespace)
473
-
474
- ```ruby
475
- class CacheController < ApplicationController
476
-
477
- def show
478
- key = params[:key]
479
- ns = params[:namespace]
480
-
481
- value = Mudis.read(key, namespace: ns)
482
- if value.nil?
483
- render json: { error: "not found" }, status: :not_found
484
- else
485
- render json: { value: value }
486
- end
487
- end
488
-
489
- def write
490
- key = params[:key]
491
- ns = params[:namespace]
492
- val = params[:value]
493
- ttl = params[:expires_in]&.to_i
494
-
495
- Mudis.write(key, val, expires_in: ttl, namespace: ns)
496
- render json: { status: "written", key: key }
497
- end
498
-
499
- def delete
500
- key = params[:key]
501
- ns = params[:namespace]
502
-
503
- Mudis.delete(key, namespace: ns)
504
- render json: { status: "deleted" }
505
- end
506
-
507
- def metrics
508
- render json: Mudis.metrics
509
- end
510
- end
511
- ```
512
-
513
- - Test it
514
-
515
- ```bash
516
- curl http://localhost:3000/cache/foo
517
- curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
518
- curl http://localhost:3000/metrics
519
-
520
- # Write with namespace
521
- curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
522
- -d "value=123&expires_in=60"
523
-
524
- # Read from namespace
525
- curl "http://localhost:3000/cache/foo?namespace=orders"
526
-
527
- # Delete from namespace
528
- curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
529
-
530
- ```
531
-
532
- ---
533
-
534
- ## Project Philosophy
535
-
536
- Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
537
-
538
- - In-process caching
539
- - Fine-grained memory and namespace control
540
- - Observability and testing friendliness
541
- - Minimal external dependencies
542
- - Configurability without complexity
543
-
544
- The primary use cases are:
545
-
546
- - Per-service application caches
547
- - Short-lived local caching inside background jobs or API layers
548
-
549
- 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.,
550
-
551
- - mudis-server expose Mudis via HTTP, web sockets, hooks, etc
552
- - mudis-broker – distributed key routing layer for coordinating multiple Mudis nodes
553
- - mudis-activejob-store – adapter for using Mudis in job queues or retry buffers
554
-
555
- ---
556
-
557
- ## Roadmap
558
-
559
- #### API Enhancements
560
-
561
- - [x] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
562
-
563
- #### Safety & Policy Controls
564
-
565
- - [x] max_ttl: Enforce a global upper bound on expires_in to prevent excessively long-lived keys
566
- - [x] default_ttl: Provide a fallback TTL when one is not specified
567
-
568
- #### Debugging
569
-
570
- - [x] clear_namespace(namespace): Remove all keys in a namespace in one call
571
-
572
- ---
573
-
574
- ## License
575
-
576
- MIT License © kiebor81
577
-
578
- ---
579
-
580
- ## Contributing
581
-
582
- See [contributor's guide](CONTRIBUTING.md)
583
-
584
- ---
585
-
586
- ## Contact
587
-
588
- For issues, suggestions, or feedback, please open a GitHub issue
589
-
590
- ---
1
+ ![mudis_signet](design/mudis.png "Mudis")
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1&cachebust=0)](https://badge.fury.io/rb/mudis)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ **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.
7
+
8
+ 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.
9
+
10
+ Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a [Mudis server](#create-a-mudis-server).
11
+
12
+ ### Why another Caching Gem?
13
+
14
+ 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.
15
+
16
+ #### Similar Gems
17
+
18
+ - [FastCache](https://github.com/swoop-inc/fast_cache)
19
+ - [EasyCache](https://github.com/malvads/easycache)
20
+ - [MiniCache](https://github.com/derrickreimer/mini_cache)
21
+ - [Zache](https://github.com/yegor256/zache)
22
+
23
+ #### Feature / Function Comparison
24
+
25
+ | **Feature** | **Mudis** | **MemoryStore** (`Rails.cache`) | **FastCache** | **Zache** | **EasyCache** | **MiniCache** |
26
+ | -------------------------------------- | ---------------- | ------------------------------- | -------------- | ------------- | ------------- | -------------- |
27
+ | **LRU eviction strategy** | ✅ Per-bucket | ✅ Global | ✅ Global | | | ✅ Simplistic |
28
+ | **TTL expiry support** | ✅ | | | ✅ | | |
29
+ | **Background expiry cleanup thread** | ✅ | (only on access) | | ✅ | | |
30
+ | **Thread safety** | ✅ Bucketed | ⚠️ Global lock | ✅ Fine-grained | | ⚠️ | ⚠️ |
31
+ | **Sharding (buckets)** | ✅ | | | ❌ | ❌ | ❌ |
32
+ | **Custom serializers** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
33
+ | **Compression (Zlib)** | ✅ | | ❌ | ❌ | ❌ | ❌ |
34
+ | **Hard memory cap** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
35
+ | **Max value size enforcement** | ✅ | | ❌ | ❌ | ❌ | ❌ |
36
+ | **Metrics (hits, misses, evictions)** | ✅ | ⚠️ Partial | ❌ | | | |
37
+ | **Fetch/update pattern** | ✅ Full | ✅ Standard | ⚠️ Partial | ✅ Basic | ✅ Basic | ✅ Basic |
38
+ | **Namespacing** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
39
+ | **Replace (if exists)** | ✅ | ✅ | | | | |
40
+ | **Clear/delete method** | ✅ | | | | | |
41
+ | **Key inspection with metadata** | ✅ | ❌ | | ❌ | ❌ | ❌ |
42
+ | **Concurrency model** | ✅ | | ✅ | | | |
43
+ | **Maintenance level** | ✅ | | ✅ | ⚠️ | ⚠️ | ⚠️ |
44
+ | **Suitable for APIs or microservices** | ✅ | ⚠️ Limited | ✅ | ⚠️ Small apps | ⚠️ Small apps | ❌ |
45
+
46
+ ---
47
+
48
+ ## Design
49
+
50
+ #### Internal Structure and Behaviour
51
+
52
+ ![mudis_flow](design/mudis_obj.png "Mudis Internals")
53
+
54
+ #### Write - Read - Eviction
55
+
56
+ ![mudis_flow](design/mudis_flow.png "Write - Read - Eviction")
57
+
58
+ #### Cache Key Lifecycle
59
+
60
+ ![mudis_lru](design/mudis_lru.png "Mudis Cache Key Lifecycle")
61
+
62
+ ---
63
+
64
+ ## Features
65
+
66
+ - **Thread-safe**: Uses per-bucket mutexes for high concurrency.
67
+ - **Sharded**: Buckets data across multiple internal stores to minimize lock contention.
68
+ - **LRU Eviction**: Automatically evicts least recently used items as memory fills up.
69
+ - **Expiry Support**: Optional TTL per key with background cleanup thread.
70
+ - **Compression**: Optional Zlib compression for large values.
71
+ - **Metrics**: Tracks hits, misses, and evictions.
72
+
73
+ ---
74
+
75
+ ## Installation
76
+
77
+ Add this line to your Gemfile:
78
+
79
+ ```ruby
80
+ gem 'mudis'
81
+ ```
82
+
83
+ Or install it manually:
84
+
85
+ ```bash
86
+ gem install mudis
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Configuration (Rails)
92
+
93
+ In your Rails app, create an initializer:
94
+
95
+ ```ruby
96
+ # config/initializers/mudis.rb
97
+ Mudis.configure do |c|
98
+ c.serializer = JSON # or Marshal | Oj
99
+ c.compress = true # Compress values using Zlib
100
+ c.max_value_bytes = 2_000_000 # Reject values > 2MB
101
+ c.hard_memory_limit = true # enforce hard memory limits
102
+ c.max_bytes = 1_073_741_824 # set maximum cache size
103
+ end
104
+
105
+ Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
106
+
107
+ at_exit do
108
+ Mudis.stop_expiry_thread
109
+ end
110
+ ```
111
+
112
+ Or with direct setters:
113
+
114
+ ```ruby
115
+ Mudis.serializer = JSON # or Marshal | Oj
116
+ Mudis.compress = true # Compress values using Zlib
117
+ Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
118
+ Mudis.hard_memory_limit = true # enforce hard memory limits
119
+ Mudis.max_bytes = 1_073_741_824 # set maximum cache size
120
+
121
+ Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
122
+
123
+ ## set at exit hook
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Basic Usage
129
+
130
+ ```ruby
131
+ require 'mudis'
132
+
133
+ # Write a value with optional TTL
134
+ Mudis.write('user:123', { name: 'Alice' }, expires_in: 600)
135
+
136
+ # Read it back
137
+ Mudis.read('user:123') # => { "name" => "Alice" }
138
+
139
+ # Check if it exists
140
+ Mudis.exists?('user:123') # => true
141
+
142
+ # Atomically update
143
+ Mudis.update('user:123') { |data| data.merge(age: 30) }
144
+
145
+ # Delete a key
146
+ Mudis.delete('user:123')
147
+ ```
148
+
149
+ ### Developer Utilities
150
+
151
+ Mudis provides utility methods to help with test environments, console debugging, and dev tool resets.
152
+
153
+ #### `Mudis.reset!`
154
+ Clears the internal cache state. Including all keys, memory tracking, and metrics. Also stops the expiry thread.
155
+
156
+ ```ruby
157
+ Mudis.write("foo", "bar")
158
+ Mudis.reset!
159
+ Mudis.read("foo") # => nil
160
+ ```
161
+
162
+ - Wipe all buckets (@stores, @lru_nodes, @current_bytes)
163
+ - Reset all metrics (:hits, :misses, :evictions, :rejected)
164
+ - Stop any running background expiry thread
165
+
166
+ #### `Mudis.reset_metrics!`
167
+
168
+ Clears only the metric counters and preserves all cached values.
169
+
170
+ ```ruby
171
+ Mudis.write("key", "value")
172
+ Mudis.read("key") # => "value"
173
+ Mudis.metrics # => { hits: 1, misses: 0, ... }
174
+
175
+ Mudis.reset_metrics!
176
+ Mudis.metrics # => { hits: 0, misses: 0, ... }
177
+ Mudis.read("key") # => "value" (still cached)
178
+ ```
179
+
180
+ #### `Mudis.least_touched`
181
+
182
+ 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.
183
+
184
+ Each result includes the full key and its access count.
185
+
186
+ ```ruby
187
+ Mudis.least_touched
188
+ # => [["foo", 0], ["user:42", 1], ["product:123", 2], ...]
189
+
190
+ Mudis.least_touched(5)
191
+ # => returns top 5 least accessed keys
192
+ ```
193
+
194
+ #### `Mudis.keys(namespace:)`
195
+
196
+ Returns all keys for a given namespace.
197
+
198
+ ```ruby
199
+ Mudis.write("u1", "alpha", namespace: "users")
200
+ Mudis.write("u2", "beta", namespace: "users")
201
+
202
+ Mudis.keys(namespace: "users")
203
+ # => ["u1", "u2"]
204
+
205
+ ```
206
+
207
+ #### `Mudis.clear_namespace(namespace:)`
208
+
209
+ Deletes all keys within a namespace.
210
+
211
+ ```ruby
212
+ Mudis.clear_namespace("users")
213
+ Mudis.read("u1", namespace: "users") # => nil
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Rails Service Integration
219
+
220
+ For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
221
+
222
+ ```ruby
223
+ class MudisService
224
+ attr_reader :cache_key, :namespace
225
+
226
+ # Initialize the service with a cache key and optional namespace
227
+ #
228
+ # @param cache_key [String] the base key to use
229
+ # @param namespace [String, nil] optional logical namespace
230
+ def initialize(cache_key, namespace: nil)
231
+ @cache_key = cache_key
232
+ @namespace = namespace
233
+ end
234
+
235
+ # Write a value to the cache
236
+ #
237
+ # @param data [Object] the value to cache
238
+ # @param expires_in [Integer, nil] optional TTL in seconds
239
+ def write(data, expires_in: nil)
240
+ Mudis.write(cache_key, data, expires_in: expires_in, namespace: namespace)
241
+ end
242
+
243
+ # Read the cached value or return default
244
+ #
245
+ # @param default [Object] fallback value if key is not present
246
+ def read(default: nil)
247
+ Mudis.read(cache_key, namespace: namespace) || default
248
+ end
249
+
250
+ # Update the cached value using a block
251
+ #
252
+ # @yieldparam current [Object] the current value
253
+ # @yieldreturn [Object] the updated value
254
+ def update
255
+ Mudis.update(cache_key, namespace: namespace) { |current| yield(current) }
256
+ end
257
+
258
+ # Delete the key from cache
259
+ def delete
260
+ Mudis.delete(cache_key, namespace: namespace)
261
+ end
262
+
263
+ # Return true if the key exists in cache
264
+ def exists?
265
+ Mudis.exists?(cache_key, namespace: namespace)
266
+ end
267
+
268
+ # Fetch from cache or compute and store it
269
+ #
270
+ # @param expires_in [Integer, nil] optional TTL
271
+ # @param force [Boolean] force recomputation
272
+ # @yield return value if key is missing
273
+ def fetch(expires_in: nil, force: false)
274
+ Mudis.fetch(cache_key, expires_in: expires_in, force: force, namespace: namespace) do
275
+ yield
276
+ end
277
+ end
278
+
279
+ # Inspect metadata for the current key
280
+ #
281
+ # @return [Hash, nil] metadata including :expires_at, :created_at, :size_bytes, etc.
282
+ def inspect_meta
283
+ Mudis.inspect(cache_key, namespace: namespace)
284
+ end
285
+ end
286
+
287
+ ```
288
+
289
+ Use it like:
290
+
291
+ ```ruby
292
+ cache = MudisService.new("user:42:profile", namespace: "users")
293
+
294
+ cache.write({ name: "Alice" }, expires_in: 300)
295
+ cache.read # => { "name" => "Alice" }
296
+ cache.exists? # => true
297
+
298
+ cache.update { |data| data.merge(age: 30) }
299
+ cache.fetch(expires_in: 60) { expensive_query }
300
+ cache.inspect_meta # => { key: "users:user:42:profile", ... }
301
+ ```
302
+
303
+ ---
304
+
305
+ ## Metrics
306
+
307
+ Track cache effectiveness and performance:
308
+
309
+ ```ruby
310
+ Mudis.metrics
311
+ # => {
312
+ # hits: 15,
313
+ # misses: 5,
314
+ # evictions: 3,
315
+ # rejected: 0,
316
+ # total_memory: 45678,
317
+ # least_touched: [
318
+ # ["user:1", 0],
319
+ # ["post:5", 1],
320
+ # ...
321
+ # ],
322
+ # buckets: [
323
+ # { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
324
+ # ...
325
+ # ]
326
+ # }
327
+
328
+ ```
329
+
330
+ Optionally, return these metrics from a controller for remote analysis and monitoring if using Rails.
331
+
332
+ ```ruby
333
+ class MudisController < ApplicationController
334
+ def metrics
335
+ render json: { mudis: Mudis.metrics }
336
+ end
337
+
338
+ end
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Advanced Configuration
344
+
345
+ | Setting | Description | Default |
346
+ |--------------------------|---------------------------------------------|--------------------|
347
+ | `Mudis.serializer` | JSON, Marshal, or Oj | `JSON` |
348
+ | `Mudis.compress` | Enable Zlib compression | `false` |
349
+ | `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
350
+ | `Mudis.buckets` | Number of cache shards | `32` |
351
+ | `Mudis.start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
352
+ | `Mudis.hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
353
+ | `Mudis.max_bytes` | Maximum allowed cache size | `1GB`|
354
+ | `Mudis.max_ttl` | Set the maximum permitted TTL | `nil` (no limit) |
355
+ | `Mudis.default_ttl` | Set the default TTL for fallback when none is provided | `nil` |
356
+
357
+ Buckets can also be set using a `MUDIS_BUCKETS` environment variable.
358
+
359
+ When setting `serializer`, be mindful of the below
360
+
361
+ | Serializer | Recommended for |
362
+ | ---------- | ------------------------------------- |
363
+ | `Marshal` | Ruby-only apps, speed-sensitive logic |
364
+ | `JSON` | Cross-language interoperability |
365
+ | `Oj` | API-heavy apps using JSON at scale |
366
+
367
+ ---
368
+
369
+ ## Benchmarks
370
+
371
+ #### Serializer(s)
372
+
373
+ _100000 iterations_
374
+
375
+ | Serializer | Total Time (s) | Ops/sec |
376
+ |----------------|------------|----------------|
377
+ | oj | 0.1342 | 745320 |
378
+ | marshal | 0.3228 | 309824 |
379
+ | json | 0.9035 | 110682 |
380
+ | oj + zlib | 1.8050 | 55401 |
381
+ | marshal + zlib | 1.8057 | 55381 |
382
+ | json + zlib | 2.7949 | 35780 |
383
+
384
+ > If opting for OJ, you will need to install the dependency in your project and configure as needed.
385
+
386
+ #### Mudis vs Rails.cache
387
+
388
+ Mudis is marginally slower than `Rails.cache` by design; it trades raw speed for control, observability, and safety.
389
+
390
+ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON_
391
+
392
+ | Operation | `Rails.cache` | `Mudis` | Delta |
393
+ | --------- | ------------- | ----------- | --------- |
394
+ | Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
395
+ | Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
396
+
397
+ > 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.
398
+
399
+ #### **Why this overhead exists**
400
+
401
+ Mudis includes features that MemoryStore doesn’t:
402
+
403
+ | Feature | Mudis | Rails.cache (MemoryStore) |
404
+ | ------------------ | ---------------------- | --------------------------- |
405
+ | Per-key TTL expiry | ✅ | ⚠️ on access |
406
+ | True LRU eviction | ✅ | ❌ |
407
+ | Hard memory limits | ✅ | ❌ |
408
+ | Value compression | ✅ | |
409
+ | Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
410
+ | Observability | ✅ | ❌ |
411
+ | Namespacing | ✅ | ❌ Manual scoping |
412
+
413
+ It will be down to the developer to decide if a fraction of a millisecond is worth
414
+
415
+ - Predictable eviction
416
+ - Configurable expiry
417
+ - Memory protection
418
+ - Namespace scoping
419
+ - Real-time metrics for hits, misses, evictions, memory usage
420
+
421
+ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)_
422
+
423
+ | Operation | `Rails.cache` | `Mudis` | Delta |
424
+ | --------- | ------------- | ----------- | ------------- |
425
+ | Write | 2.342 ms/op | 0.501 ms/op | **−1.841 ms** |
426
+ | Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
427
+
428
+ With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
429
+
430
+ #### Other Benchmarks
431
+
432
+ _10000 iterations of 512KB, JSON, compression OFF (to match MemoryStore default)_
433
+
434
+ | Operation | `Rails.cache` | `Mudis` | Delta |
435
+ | --------- | ------------- | ----------- | ------------- |
436
+ | Write | 1.291 ms/op | 0.32 ms/op | **−0.971 ms** |
437
+ | Read | 0.011 ms/op | 0.048 ms/op | +0.037 ms |
438
+
439
+ _10000 iterations of 512KB, JSON, compression ON_
440
+
441
+ | Operation | `Rails.cache` | `Mudis` | Delta |
442
+ | --------- | ------------- | ----------- | ------------- |
443
+ | Write | 1.11 ms/op | 1.16 ms/op | +0.05 ms |
444
+ | Read | 0.07 ms/op | 0.563 ms/op | +0.493 ms |
445
+
446
+ ---
447
+
448
+ ## Graceful Shutdown
449
+
450
+ Don’t forget to stop the expiry thread when your app exits:
451
+
452
+ ```ruby
453
+ at_exit { Mudis.stop_expiry_thread }
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Known Limitations
459
+
460
+ - Data is **non-persistent**.
461
+ - Compression introduces CPU overhead.
462
+
463
+ ---
464
+
465
+ ## Inter-Process Caching (IPC Mode)
466
+
467
+ 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.
468
+
469
+ ### Overview
470
+
471
+ In IPC mode, Mudis runs as a singleton server within the master process.
472
+
473
+ Each worker connects to that server through a lightweight client (`MudisClient`) using a local UNIX domain socket (default: `/tmp/mudis.sock`).
474
+ All cache operations, e.g., read, write, delete, fetch, etc., are transparently proxied to the master process, which holds the authoritative cache state.
475
+
476
+ This design allows multiple workers to share the same cache without duplicating memory or losing synchronization, while retaining Mudis’ performance, configurability, and thread safety.
477
+
478
+ | **Benefit** | **Description** |
479
+ | --------------------------------- | ---------------------------------------------------------------------------------------- |
480
+ | **Shared Cache Across Processes** | All Puma workers share one Mudis instance via IPC. |
481
+ | **Zero External Dependencies** | No Redis, Memcached, or separate daemon required. |
482
+ | **Memory Efficient** | Cache data stored only once, not duplicated per worker. |
483
+ | **Full Feature Support** | All Mudis features (TTL, compression, metrics, etc.) work transparently. |
484
+ | **Safe & Local** | Communication is limited to the host system’s UNIX socket, ensuring isolation and speed. |
485
+
486
+ ![mudis_ipc](design/mudis_ipc.png "Mudis IPC")
487
+
488
+ ### Setup (Puma / Rails)
489
+
490
+ Enable IPC mode by adding the following to your Puma configuration:
491
+
492
+ ```ruby
493
+ # config/puma.rb
494
+ preload_app!
495
+
496
+ before_fork do
497
+ require "mudis"
498
+ require "mudis_server"
499
+
500
+ # Your typical Mudis configuration (previously in a Rails initializer)
501
+ Mudis.configure do |c|
502
+ c.serializer = JSON
503
+ c.compress = true
504
+ c.max_value_bytes = 2_000_000
505
+ c.hard_memory_limit = true
506
+ c.max_bytes = 1_073_741_824
507
+ end
508
+
509
+ Mudis.start_expiry_thread(interval: 60)
510
+ MudisServer.start!
511
+
512
+ at_exit { Mudis.stop_expiry_thread }
513
+ end
514
+
515
+ on_worker_boot do
516
+ require "mudis_client"
517
+ $mudis = MudisClient.new
518
+ end
519
+ ```
520
+
521
+ Adding this Proxy to initializers allows seamless use of the API as documented.
522
+
523
+ ```ruby
524
+ # config/initializers/mudis_proxy.rb
525
+ unless defined?(MudisServer)
526
+ class Mudis
527
+ def self.read(*a, **k) = $mudis.read(*a, **k)
528
+ def self.write(*a, **k) = $mudis.write(*a, **k)
529
+ def self.delete(*a, **k) = $mudis.delete(*a, **k)
530
+ def self.fetch(*a, **k, &b) = $mudis.fetch(*a, **k, &b)
531
+ def self.metrics = $mudis.metrics
532
+ def self.reset_metrics! = $mudis.reset_metrics!
533
+ def self.reset! = $mudis.reset!
534
+ end
535
+
536
+ end
537
+ ```
538
+
539
+ **Use IPC mode when:**
540
+
541
+ - Running Rails or Rack apps under Puma cluster or multi-process background job workers.
542
+ - You need cache consistency and memory efficiency without standing up Redis.
543
+ - You want to preserve Mudis’s observability, configurability, and in-process simplicity at a larger scale.
544
+
545
+ ---
546
+
547
+ ## Create a Mudis Server
548
+
549
+ ### Minimal Setup
550
+
551
+ - Create a new Rails API app:
552
+
553
+ ```bash
554
+ rails new mudis-server --api
555
+ cd mudis-server
556
+ ```
557
+
558
+ - Add mudis to your Gemfile
559
+ - Create Initializer: `config/initializers/mudis.rb`
560
+ - Define routes
561
+
562
+ ```ruby
563
+ Rails.application.routes.draw do
564
+ get "/cache/:key", to: "cache#show"
565
+ post "/cache/:key", to: "cache#write"
566
+ delete "/cache/:key", to: "cache#delete"
567
+ get "/metrics", to: "cache#metrics"
568
+ end
569
+ ```
570
+
571
+ - Create a `cache_controller` (with optional per caller/consumer namespace)
572
+
573
+ ```ruby
574
+ class CacheController < ApplicationController
575
+
576
+ def show
577
+ key = params[:key]
578
+ ns = params[:namespace]
579
+
580
+ value = Mudis.read(key, namespace: ns)
581
+ if value.nil?
582
+ render json: { error: "not found" }, status: :not_found
583
+ else
584
+ render json: { value: value }
585
+ end
586
+ end
587
+
588
+ def write
589
+ key = params[:key]
590
+ ns = params[:namespace]
591
+ val = params[:value]
592
+ ttl = params[:expires_in]&.to_i
593
+
594
+ Mudis.write(key, val, expires_in: ttl, namespace: ns)
595
+ render json: { status: "written", key: key }
596
+ end
597
+
598
+ def delete
599
+ key = params[:key]
600
+ ns = params[:namespace]
601
+
602
+ Mudis.delete(key, namespace: ns)
603
+ render json: { status: "deleted" }
604
+ end
605
+
606
+ def metrics
607
+ render json: Mudis.metrics
608
+ end
609
+ end
610
+ ```
611
+
612
+ - Test it
613
+
614
+ ```bash
615
+ curl http://localhost:3000/cache/foo
616
+ curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
617
+ curl http://localhost:3000/metrics
618
+
619
+ # Write with namespace
620
+ curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
621
+ -d "value=123&expires_in=60"
622
+
623
+ # Read from namespace
624
+ curl "http://localhost:3000/cache/foo?namespace=orders"
625
+
626
+ # Delete from namespace
627
+ curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
628
+
629
+ ```
630
+
631
+ ---
632
+
633
+ ## Project Philosophy
634
+
635
+ Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
636
+
637
+ - In-process caching
638
+ - Fine-grained memory and namespace control
639
+ - Observability and testing friendliness
640
+ - Minimal external dependencies
641
+ - Configurability without complexity
642
+
643
+ The primary use cases are:
644
+
645
+ - Per-service application caches
646
+ - Short-lived local caching inside background jobs or API layers
647
+
648
+ 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.,
649
+
650
+ - mudis-server – expose Mudis via HTTP, web sockets, hooks, etc
651
+ - mudis-broker – distributed key routing layer for coordinating multiple Mudis nodes
652
+ - mudis-activejob-store – adapter for using Mudis in job queues or retry buffers
653
+
654
+ ---
655
+
656
+ ## Roadmap
657
+
658
+ #### API Enhancements
659
+
660
+ - [x] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
661
+
662
+ #### Safety & Policy Controls
663
+
664
+ - [x] max_ttl: Enforce a global upper bound on expires_in to prevent excessively long-lived keys
665
+ - [x] default_ttl: Provide a fallback TTL when one is not specified
666
+
667
+ #### Debugging
668
+
669
+ - [x] clear_namespace(namespace): Remove all keys in a namespace in one call
670
+
671
+ #### Refactor Mudis
672
+
673
+ - [ ] Review Mudis for improved readability and reduce complexity in top-level functions
674
+ - [ ] Enhanced guards
675
+ - [ ] Review for functionality gaps and enhance as needed
676
+
677
+ ---
678
+
679
+ ## License
680
+
681
+ MIT License © kiebor81
682
+
683
+ ---
684
+
685
+ ## Contributing
686
+
687
+ See [contributor's guide](CONTRIBUTING.md)
688
+
689
+ ---
690
+
691
+ ## Contact
692
+
693
+ For issues, suggestions, or feedback, please open a GitHub issue
694
+
695
+ ---