mudis 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +197 -27
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +51 -5
- data/lib/mudis_config.rb +5 -1
- data/sig/mudis.rbs +5 -1
- data/sig/mudis_config.rbs +8 -6
- data/spec/guardrails_spec.rb +80 -0
- data/spec/mudis_spec.rb +17 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6207a8171eb9fd9723889d90a5a5e696dafc9a894c29d9b6f99340b7c02ec697
|
4
|
+
data.tar.gz: a18de4b21e8b4116c321393d0ada12da91c06682ff099907be84b6ac78c6f999
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e44da4c093e85012cee92cc61be4062c48fe94fe46b22210c1433e47b8eef91755359a658f936075ee514a5adcbf5ea2dae184031042532d13907d619ab77876
|
7
|
+
data.tar.gz: 79c1703d2d03938d801d752c7cc3c7c98ed777933e85b091ece31180d1dd2b9eaf0af8e8b8bd30221750cd89aa5cfb2989e8914aa4ef09fe6d1316a3ee678024
|
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|

|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/mudis)
|
3
|
+
[](https://badge.fury.io/rb/mudis)
|
4
4
|
|
5
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
6
|
|
7
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
8
|
|
9
|
-
Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a Mudis server.
|
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
10
|
|
11
11
|
### Why another Caching Gem?
|
12
12
|
|
@@ -176,6 +176,20 @@ Mudis.metrics # => { hits: 0, misses: 0, ... }
|
|
176
176
|
Mudis.read("key") # => "value" (still cached)
|
177
177
|
```
|
178
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
|
+
|
179
193
|
---
|
180
194
|
|
181
195
|
## Rails Service Integration
|
@@ -277,6 +291,11 @@ Mudis.metrics
|
|
277
291
|
# evictions: 3,
|
278
292
|
# rejected: 0,
|
279
293
|
# total_memory: 45678,
|
294
|
+
# least_touched: [
|
295
|
+
# ["user:1", 0],
|
296
|
+
# ["post:5", 1],
|
297
|
+
# ...
|
298
|
+
# ],
|
280
299
|
# buckets: [
|
281
300
|
# { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
|
282
301
|
# ...
|
@@ -305,11 +324,12 @@ end
|
|
305
324
|
| `Mudis.serializer` | JSON, Marshal, or Oj | `JSON` |
|
306
325
|
| `Mudis.compress` | Enable Zlib compression | `false` |
|
307
326
|
| `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
|
308
|
-
| `Mudis.buckets` | Number of cache shards
|
309
|
-
| `start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
|
310
|
-
| `hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
|
311
|
-
| `max_bytes` | Maximum allowed cache size | `1GB`|
|
312
|
-
| `
|
327
|
+
| `Mudis.buckets` | Number of cache shards | `32` |
|
328
|
+
| `Mudis.start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
|
329
|
+
| `Mudis.hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
|
330
|
+
| `Mudis.max_bytes` | Maximum allowed cache size | `1GB`|
|
331
|
+
| `Mudis.max_ttl` | Set the maximum permitted TTL | `nil` (no limit) |
|
332
|
+
| `Mudis.default_ttl` | Set the default TTL for fallback when none is provided | `nil` |
|
313
333
|
|
314
334
|
Buckets can also be set using a `MUDIS_BUCKETS` environment variable.
|
315
335
|
|
@@ -321,21 +341,69 @@ When setting `serializer`, be mindful of the below
|
|
321
341
|
| `JSON` | Cross-language interoperability |
|
322
342
|
| `Oj` | API-heavy apps using JSON at scale |
|
323
343
|
|
324
|
-
|
344
|
+
---
|
345
|
+
|
346
|
+
## Benchmarks
|
325
347
|
|
326
|
-
|
348
|
+
#### Serializer(s)
|
327
349
|
|
328
|
-
|
329
|
-
|
330
|
-
|
|
331
|
-
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
350
|
+
_100000 iterations_
|
351
|
+
|
352
|
+
| Serializer | Total Time (s) | Ops/sec |
|
353
|
+
|----------------|------------|----------------|
|
354
|
+
| oj | 0.1342 | 745320 |
|
355
|
+
| marshal | 0.3228 | 309824 |
|
356
|
+
| json | 0.9035 | 110682 |
|
357
|
+
| oj + zlib | 1.8050 | 55401 |
|
358
|
+
| marshal + zlib | 1.8057 | 55381 |
|
359
|
+
| json + zlib | 2.7949 | 35780 |
|
336
360
|
|
337
361
|
> If opting for OJ, you will need to install the dependency in your project and configure as needed.
|
338
362
|
|
363
|
+
#### Mudis vs Rails.cache
|
364
|
+
|
365
|
+
Mudis is marginally slower than `Rails.cache` by design; it trades raw speed for control, observability, and safety.
|
366
|
+
|
367
|
+
_10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON_
|
368
|
+
|
369
|
+
| Operation | `Rails.cache` | `Mudis` | Delta |
|
370
|
+
| --------- | ------------- | ----------- | --------- |
|
371
|
+
| Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
|
372
|
+
| Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
|
373
|
+
|
374
|
+
> 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.
|
375
|
+
|
376
|
+
###### **Why this overhead exists**
|
377
|
+
|
378
|
+
Mudis includes features that MemoryStore doesn’t:
|
379
|
+
|
380
|
+
| Feature | Mudis | Rails.cache (MemoryStore) |
|
381
|
+
| ------------------ | ---------------------- | --------------------------- |
|
382
|
+
| Per-key TTL expiry | ✅ | ⚠️ on access |
|
383
|
+
| True LRU eviction | ✅ | ❌ |
|
384
|
+
| Hard memory limits | ✅ | ❌ |
|
385
|
+
| Value compression | ✅ | ❌ |
|
386
|
+
| Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
|
387
|
+
| Observability | ✅ | ❌ |
|
388
|
+
| Namespacing | ✅ | ❌ Manual scoping |
|
389
|
+
|
390
|
+
It will be down to the developer to decide if a fraction of a millisecond is worth
|
391
|
+
|
392
|
+
- Predictable eviction
|
393
|
+
- Configurable expiry
|
394
|
+
- Memory protection
|
395
|
+
- Namespace scoping
|
396
|
+
- Real-time metrics for hits, misses, evictions, memory usage
|
397
|
+
|
398
|
+
_10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)_
|
399
|
+
|
400
|
+
| Operation | `Rails.cache` | `Mudis` | Delta |
|
401
|
+
| --------- | ------------- | ----------- | ------------- |
|
402
|
+
| Write | 2.342 ms/op | 0.501 ms/op | **−1.841 ms** |
|
403
|
+
| Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
|
404
|
+
|
405
|
+
With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
|
406
|
+
|
339
407
|
---
|
340
408
|
|
341
409
|
## Graceful Shutdown
|
@@ -355,6 +423,115 @@ at_exit { Mudis.stop_expiry_thread }
|
|
355
423
|
|
356
424
|
---
|
357
425
|
|
426
|
+
## Create a Mudis Server
|
427
|
+
|
428
|
+
### Minimal Setup
|
429
|
+
|
430
|
+
- Create a new Rails API app:
|
431
|
+
|
432
|
+
```bash
|
433
|
+
rails new mudis-server --api
|
434
|
+
cd mudis-server
|
435
|
+
```
|
436
|
+
|
437
|
+
- Add mudis to your Gemfile
|
438
|
+
- Create Initializer: `config/initializers/mudis.rb`
|
439
|
+
- Define routes
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
Rails.application.routes.draw do
|
443
|
+
get "/cache/:key", to: "cache#show"
|
444
|
+
post "/cache/:key", to: "cache#write"
|
445
|
+
delete "/cache/:key", to: "cache#delete"
|
446
|
+
get "/metrics", to: "cache#metrics"
|
447
|
+
end
|
448
|
+
```
|
449
|
+
|
450
|
+
- Create a `cache_controller` (with optional per caller/consumer namespace)
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
class CacheController < ApplicationController
|
454
|
+
|
455
|
+
def show
|
456
|
+
key = params[:key]
|
457
|
+
ns = params[:namespace]
|
458
|
+
|
459
|
+
value = Mudis.read(key, namespace: ns)
|
460
|
+
if value.nil?
|
461
|
+
render json: { error: "not found" }, status: :not_found
|
462
|
+
else
|
463
|
+
render json: { value: value }
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
def write
|
468
|
+
key = params[:key]
|
469
|
+
ns = params[:namespace]
|
470
|
+
val = params[:value]
|
471
|
+
ttl = params[:expires_in]&.to_i
|
472
|
+
|
473
|
+
Mudis.write(key, val, expires_in: ttl, namespace: ns)
|
474
|
+
render json: { status: "written", key: key }
|
475
|
+
end
|
476
|
+
|
477
|
+
def delete
|
478
|
+
key = params[:key]
|
479
|
+
ns = params[:namespace]
|
480
|
+
|
481
|
+
Mudis.delete(key, namespace: ns)
|
482
|
+
render json: { status: "deleted" }
|
483
|
+
end
|
484
|
+
|
485
|
+
def metrics
|
486
|
+
render json: Mudis.metrics
|
487
|
+
end
|
488
|
+
end
|
489
|
+
```
|
490
|
+
|
491
|
+
- Test it
|
492
|
+
|
493
|
+
```bash
|
494
|
+
curl http://localhost:3000/cache/foo
|
495
|
+
curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
|
496
|
+
curl http://localhost:3000/metrics
|
497
|
+
|
498
|
+
# Write with namespace
|
499
|
+
curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
|
500
|
+
-d "value=123&expires_in=60"
|
501
|
+
|
502
|
+
# Read from namespace
|
503
|
+
curl "http://localhost:3000/cache/foo?namespace=orders"
|
504
|
+
|
505
|
+
# Delete from namespace
|
506
|
+
curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
|
507
|
+
|
508
|
+
```
|
509
|
+
|
510
|
+
---
|
511
|
+
|
512
|
+
## Project Philosophy
|
513
|
+
|
514
|
+
Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
|
515
|
+
|
516
|
+
- In-process caching
|
517
|
+
- Fine-grained memory and namespace control
|
518
|
+
- Observability and testing friendliness
|
519
|
+
- Minimal external dependencies
|
520
|
+
- Configurability without complexity
|
521
|
+
|
522
|
+
The primary use cases are:
|
523
|
+
|
524
|
+
- Per-service application caches
|
525
|
+
- Short-lived local caching inside background jobs or API layers
|
526
|
+
|
527
|
+
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.,
|
528
|
+
|
529
|
+
- mudis-server – expose Mudis via HTTP, web sockets, hooks, etc
|
530
|
+
- mudis-broker – distributed key routing layer for coordinating multiple Mudis nodes
|
531
|
+
- mudis-activejob-store – adapter for using Mudis in job queues or retry buffers
|
532
|
+
|
533
|
+
---
|
534
|
+
|
358
535
|
## Roadmap
|
359
536
|
|
360
537
|
#### API Enhancements
|
@@ -363,8 +540,8 @@ at_exit { Mudis.stop_expiry_thread }
|
|
363
540
|
|
364
541
|
#### Safety & Policy Controls
|
365
542
|
|
366
|
-
- [
|
367
|
-
- [
|
543
|
+
- [x] max_ttl: Enforce a global upper bound on expires_in to prevent excessively long-lived keys
|
544
|
+
- [x] default_ttl: Provide a fallback TTL when one is not specified
|
368
545
|
|
369
546
|
#### Debugging
|
370
547
|
|
@@ -380,14 +557,7 @@ MIT License © kiebor81
|
|
380
557
|
|
381
558
|
## Contributing
|
382
559
|
|
383
|
-
|
384
|
-
|
385
|
-
```bash
|
386
|
-
git clone https://github.com/kiebor81/mudis
|
387
|
-
cd mudis
|
388
|
-
bundle install
|
389
|
-
|
390
|
-
```
|
560
|
+
See [contributor's guide](CONTRIBUTING.md)
|
391
561
|
|
392
562
|
---
|
393
563
|
|
data/lib/mudis/version.rb
CHANGED
data/lib/mudis.rb
CHANGED
@@ -17,9 +17,11 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
17
17
|
@metrics_mutex = Mutex.new # Mutex for synchronizing access to metrics
|
18
18
|
@max_value_bytes = nil # Optional size cap per value
|
19
19
|
@stop_expiry = false # Signal for stopping expiry thread
|
20
|
+
@max_ttl = nil # Optional maximum TTL for cache entries
|
21
|
+
@default_ttl = nil # Default TTL for cache entries if not specified
|
20
22
|
|
21
23
|
class << self
|
22
|
-
attr_accessor :serializer, :compress, :hard_memory_limit
|
24
|
+
attr_accessor :serializer, :compress, :hard_memory_limit, :max_ttl, :default_ttl
|
23
25
|
attr_reader :max_bytes, :max_value_bytes
|
24
26
|
|
25
27
|
# Configures Mudis with a block, allowing customization of settings
|
@@ -34,7 +36,7 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
34
36
|
end
|
35
37
|
|
36
38
|
# Applies the current configuration to Mudis
|
37
|
-
def apply_config!
|
39
|
+
def apply_config! # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
38
40
|
validate_config!
|
39
41
|
|
40
42
|
self.serializer = config.serializer
|
@@ -42,10 +44,17 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
42
44
|
self.max_value_bytes = config.max_value_bytes
|
43
45
|
self.hard_memory_limit = config.hard_memory_limit
|
44
46
|
self.max_bytes = config.max_bytes
|
47
|
+
self.max_ttl = config.max_ttl
|
48
|
+
self.default_ttl = config.default_ttl
|
49
|
+
|
50
|
+
if config.buckets # rubocop:disable Style/GuardClause
|
51
|
+
@buckets = config.buckets
|
52
|
+
reset!
|
53
|
+
end
|
45
54
|
end
|
46
55
|
|
47
56
|
# Validates the current configuration, raising errors for invalid settings
|
48
|
-
def validate_config! # rubocop:disable Metrics/AbcSize
|
57
|
+
def validate_config! # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
49
58
|
if config.max_value_bytes && config.max_value_bytes > config.max_bytes
|
50
59
|
raise ArgumentError,
|
51
60
|
"max_value_bytes cannot exceed max_bytes"
|
@@ -54,6 +63,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
54
63
|
raise ArgumentError, "max_value_bytes must be > 0" if config.max_value_bytes && config.max_value_bytes <= 0
|
55
64
|
|
56
65
|
raise ArgumentError, "buckets must be > 0" if config.buckets && config.buckets <= 0
|
66
|
+
raise ArgumentError, "max_ttl must be > 0" if config.max_ttl && config.max_ttl <= 0
|
67
|
+
raise ArgumentError, "default_ttl must be > 0" if config.default_ttl && config.default_ttl <= 0
|
57
68
|
end
|
58
69
|
|
59
70
|
# Returns a snapshot of metrics (thread-safe)
|
@@ -65,6 +76,7 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
65
76
|
evictions: @metrics[:evictions],
|
66
77
|
rejected: @metrics[:rejected],
|
67
78
|
total_memory: current_memory_bytes,
|
79
|
+
least_touched: least_touched(10),
|
68
80
|
buckets: buckets.times.map do |idx|
|
69
81
|
{
|
70
82
|
index: idx,
|
@@ -186,11 +198,12 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
186
198
|
end
|
187
199
|
|
188
200
|
# Reads and returns the value for a key, updating LRU and metrics
|
189
|
-
def read(key, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
201
|
+
def read(key, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
190
202
|
key = namespaced_key(key, namespace)
|
191
203
|
raw_entry = nil
|
192
204
|
idx = bucket_index(key)
|
193
205
|
mutex = @mutexes[idx]
|
206
|
+
store = @stores[idx]
|
194
207
|
|
195
208
|
mutex.synchronize do
|
196
209
|
raw_entry = @stores[idx][key]
|
@@ -199,6 +212,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
199
212
|
raw_entry = nil
|
200
213
|
end
|
201
214
|
|
215
|
+
store[key][:touches] = (store[key][:touches] || 0) + 1 if store[key]
|
216
|
+
|
202
217
|
metric(:hits) if raw_entry
|
203
218
|
metric(:misses) unless raw_entry
|
204
219
|
end
|
@@ -223,6 +238,9 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
223
238
|
return
|
224
239
|
end
|
225
240
|
|
241
|
+
# Ensure expires_in respects max_ttl and default_ttl
|
242
|
+
expires_in = effective_ttl(expires_in)
|
243
|
+
|
226
244
|
idx = bucket_index(key)
|
227
245
|
mutex = @mutexes[idx]
|
228
246
|
store = @stores[idx]
|
@@ -238,7 +256,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
238
256
|
store[key] = {
|
239
257
|
value: raw,
|
240
258
|
expires_at: expires_in ? Time.now + expires_in : nil,
|
241
|
-
created_at: Time.now
|
259
|
+
created_at: Time.now,
|
260
|
+
touches: 0
|
242
261
|
}
|
243
262
|
|
244
263
|
insert_lru(idx, key)
|
@@ -364,6 +383,24 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
364
383
|
keys
|
365
384
|
end
|
366
385
|
|
386
|
+
# Returns the least-touched keys across all buckets
|
387
|
+
def least_touched(n = 10) # rubocop:disable Metrics/MethodLength,Naming/MethodParameterName
|
388
|
+
keys_with_touches = []
|
389
|
+
|
390
|
+
buckets.times do |idx|
|
391
|
+
mutex = @mutexes[idx]
|
392
|
+
store = @stores[idx]
|
393
|
+
|
394
|
+
mutex.synchronize do
|
395
|
+
store.each do |key, entry|
|
396
|
+
keys_with_touches << [key, entry[:touches] || 0]
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
keys_with_touches.sort_by { |_, count| count }.first(n)
|
402
|
+
end
|
403
|
+
|
367
404
|
# Returns total memory used across all buckets
|
368
405
|
def current_memory_bytes
|
369
406
|
@current_bytes.sum
|
@@ -447,5 +484,14 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
447
484
|
ns = namespace || Thread.current[:mudis_namespace]
|
448
485
|
ns ? "#{ns}:#{key}" : key
|
449
486
|
end
|
487
|
+
|
488
|
+
# Calculates the effective TTL for an entry, respecting max_ttl if set
|
489
|
+
def effective_ttl(expires_in)
|
490
|
+
ttl = expires_in || @default_ttl
|
491
|
+
return nil unless ttl
|
492
|
+
return ttl unless @max_ttl
|
493
|
+
|
494
|
+
[ttl, @max_ttl].min
|
495
|
+
end
|
450
496
|
end
|
451
497
|
end
|
data/lib/mudis_config.rb
CHANGED
@@ -8,7 +8,9 @@ class MudisConfig
|
|
8
8
|
:max_value_bytes,
|
9
9
|
:hard_memory_limit,
|
10
10
|
:max_bytes,
|
11
|
-
:buckets
|
11
|
+
:buckets,
|
12
|
+
:max_ttl,
|
13
|
+
:default_ttl
|
12
14
|
|
13
15
|
def initialize
|
14
16
|
@serializer = JSON # Default serialization strategy
|
@@ -17,5 +19,7 @@ class MudisConfig
|
|
17
19
|
@hard_memory_limit = false # Enforce max_bytes as hard cap
|
18
20
|
@max_bytes = 1_073_741_824 # 1 GB default max cache size
|
19
21
|
@buckets = nil # use nil to signal fallback to ENV or default
|
22
|
+
@max_ttl = nil # Max TTL for cache entries (optional)
|
23
|
+
@default_ttl = nil # Default TTL for cache entries (optional)
|
20
24
|
end
|
21
25
|
end
|
data/sig/mudis.rbs
CHANGED
@@ -6,10 +6,13 @@ class Mudis
|
|
6
6
|
attr_accessor hard_memory_limit : bool
|
7
7
|
attr_reader max_bytes : Integer
|
8
8
|
attr_reader max_value_bytes : Integer?
|
9
|
-
|
9
|
+
attr_accessor max_ttl: Integer?
|
10
|
+
attr_accessor default_ttl: Integer?
|
11
|
+
|
10
12
|
def configure: () { (config: MudisConfig) -> void } -> void
|
11
13
|
def config: () -> MudisConfig
|
12
14
|
def apply_config!: () -> void
|
15
|
+
def validate_config!: () -> void
|
13
16
|
|
14
17
|
def buckets: () -> Integer
|
15
18
|
end
|
@@ -43,6 +46,7 @@ class Mudis
|
|
43
46
|
def self.all_keys: () -> Array[String]
|
44
47
|
def self.current_memory_bytes: () -> Integer
|
45
48
|
def self.max_memory_bytes: () -> Integer
|
49
|
+
def self.least_touched: (?Integer) -> Array[[String, Integer]]
|
46
50
|
|
47
51
|
# State reset
|
48
52
|
def self.reset!: () -> void
|
data/sig/mudis_config.rbs
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
class MudisConfig
|
2
|
-
attr_accessor serializer
|
3
|
-
attr_accessor compress
|
4
|
-
attr_accessor max_value_bytes
|
5
|
-
attr_accessor hard_memory_limit
|
6
|
-
attr_accessor max_bytes
|
7
|
-
attr_accessor
|
2
|
+
attr_accessor serializer: Object
|
3
|
+
attr_accessor compress: bool
|
4
|
+
attr_accessor max_value_bytes: Integer?
|
5
|
+
attr_accessor hard_memory_limit: bool
|
6
|
+
attr_accessor max_bytes: Integer
|
7
|
+
attr_accessor max_ttl: Integer?
|
8
|
+
attr_accessor default_ttl: Integer?
|
9
|
+
attr_accessor buckets: Integer?
|
8
10
|
end
|
data/spec/guardrails_spec.rb
CHANGED
@@ -3,6 +3,86 @@
|
|
3
3
|
require "spec_helper"
|
4
4
|
require "climate_control"
|
5
5
|
|
6
|
+
RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
|
7
|
+
before do
|
8
|
+
Mudis.reset!
|
9
|
+
Mudis.configure do |c|
|
10
|
+
c.max_ttl = 60 # 60 seconds max
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
|
15
|
+
before do
|
16
|
+
Mudis.reset!
|
17
|
+
Mudis.configure do |c|
|
18
|
+
c.default_ttl = 60
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "applies default_ttl when expires_in is nil" do
|
23
|
+
Mudis.write("foo", "bar") # no explicit expires_in
|
24
|
+
meta = Mudis.inspect("foo")
|
25
|
+
expect(meta[:expires_at]).not_to be_nil
|
26
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "respects expires_in if explicitly given" do
|
30
|
+
Mudis.write("short_lived", "bar", expires_in: 10)
|
31
|
+
meta = Mudis.inspect("short_lived")
|
32
|
+
expect(meta[:expires_at]).not_to be_nil
|
33
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "applies max_ttl over default_ttl if both are set" do
|
37
|
+
Mudis.configure do |c|
|
38
|
+
c.default_ttl = 120
|
39
|
+
c.max_ttl = 30
|
40
|
+
end
|
41
|
+
|
42
|
+
Mudis.write("capped", "baz") # no explicit expires_in
|
43
|
+
meta = Mudis.inspect("capped")
|
44
|
+
expect(meta[:expires_at]).not_to be_nil
|
45
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "stores forever if default_ttl and expires_in are nil" do
|
49
|
+
Mudis.configure do |c|
|
50
|
+
c.default_ttl = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
Mudis.write("forever", "ever")
|
54
|
+
meta = Mudis.inspect("forever")
|
55
|
+
expect(meta[:expires_at]).to be_nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "clamps expires_in to max_ttl if it exceeds max_ttl" do
|
60
|
+
Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
|
61
|
+
|
62
|
+
metadata = Mudis.inspect("foo")
|
63
|
+
ttl = metadata[:expires_at] - metadata[:created_at]
|
64
|
+
|
65
|
+
expect(ttl).to be <= 60
|
66
|
+
expect(ttl).to be > 0
|
67
|
+
end
|
68
|
+
|
69
|
+
it "respects expires_in if below max_ttl" do
|
70
|
+
Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
|
71
|
+
|
72
|
+
metadata = Mudis.inspect("bar")
|
73
|
+
ttl = metadata[:expires_at] - metadata[:created_at]
|
74
|
+
|
75
|
+
expect(ttl).to be_within(1).of(30)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "allows nil expires_in (no expiry) if not required" do
|
79
|
+
Mudis.write("baz", "no expiry")
|
80
|
+
|
81
|
+
metadata = Mudis.inspect("baz")
|
82
|
+
expect(metadata[:expires_at]).to be_nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
6
86
|
RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
|
7
87
|
after { Mudis.reset! }
|
8
88
|
|
data/spec/mudis_spec.rb
CHANGED
@@ -239,6 +239,23 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
239
239
|
end
|
240
240
|
end
|
241
241
|
|
242
|
+
describe ".least_touched" do
|
243
|
+
it "returns keys with lowest read access counts" do
|
244
|
+
Mudis.reset!
|
245
|
+
Mudis.write("a", 1)
|
246
|
+
Mudis.write("b", 2)
|
247
|
+
Mudis.write("c", 3)
|
248
|
+
|
249
|
+
Mudis.read("a")
|
250
|
+
Mudis.read("a")
|
251
|
+
Mudis.read("b")
|
252
|
+
|
253
|
+
least = Mudis.least_touched(2)
|
254
|
+
expect(least.map(&:first)).to include("c") # Never read
|
255
|
+
expect(least.first.last).to eq(0)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
242
259
|
describe ".configure" do
|
243
260
|
it "applies configuration settings correctly" do
|
244
261
|
Mudis.configure do |c|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mudis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- kiebor81
|
@@ -38,8 +38,9 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.12'
|
41
|
-
description:
|
42
|
-
|
41
|
+
description: Mudis is a fast, thread-safe, in-memory, sharded LRU cache for Ruby applications.
|
42
|
+
Inspired by Redis, it provides value serialization, optional compression, per-key
|
43
|
+
expiry, and metric tracking in a lightweight, dependency-free package.
|
43
44
|
email:
|
44
45
|
executables: []
|
45
46
|
extensions: []
|
@@ -77,7 +78,8 @@ requirements: []
|
|
77
78
|
rubygems_version: 3.5.17
|
78
79
|
signing_key:
|
79
80
|
specification_version: 4
|
80
|
-
summary: A fast in-memory Ruby LRU cache with compression
|
81
|
+
summary: A fast in-memory, thread-safe and high performance Ruby LRU cache with compression
|
82
|
+
and auto-expiry.
|
81
83
|
test_files:
|
82
84
|
- spec/guardrails_spec.rb
|
83
85
|
- spec/mudis_spec.rb
|