mudis 0.4.2 → 0.4.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8a43b94197d868d77feb4f2499899faa1c8c38606b166406e7dab9ad4d92b80
4
- data.tar.gz: ce0a499a83d0397766624554949e9eefdd3434ccf763e0fc810613bef911dff4
3
+ metadata.gz: b180000138c976ee0e0c7a7cdc068550c5f33f39f363c63ba6397b84fe8f6cbe
4
+ data.tar.gz: ee6de323fd9dcc5914ec0fde68da5fff7289e6c830395b9d4be4271d389b2c15
5
5
  SHA512:
6
- metadata.gz: 66963f1f1a627647fe7504ada52dd27f872fd06a107ca9dd03102271d33bb7d803e15f93a0a29d95679b9fad83c1e28920a95629074db611295be354e675178a
7
- data.tar.gz: d0fed0c0df073b86f4433338e32860556467fef209f059ec670e293a938029090d625d8ee59f416fe2e807607eece7e4343943285a1c136d1cb82cace813236b
6
+ metadata.gz: e7d2c14ca2b2dce8ed23f5fb371fe73456d92d9613e25d75fb645d1a6903259a21dd31dc7e3acca8f4336dc77990d5f33fce9254e3becdbcc59d7c19c526a302
7
+ data.tar.gz: 6619380f6d7206caec28c0e1f3f5450bb8991817fff6c08d6e7b5ceff2c82edc8a7c7b51b38894e5fb5a09e5f41a0546c50b951d088accfc1dc70a91935d398d
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  ![mudis_signet](design/mudis.png "Mudis")
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1)](https://badge.fury.io/rb/mudis)
3
+ [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1&cachebust=0)](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
 
@@ -305,12 +305,12 @@ end
305
305
  | `Mudis.serializer` | JSON, Marshal, or Oj | `JSON` |
306
306
  | `Mudis.compress` | Enable Zlib compression | `false` |
307
307
  | `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
308
- | `Mudis.buckets` | Number of cache shards (via ENV var) | `32` |
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`|
308
+ | `Mudis.buckets` | Number of cache shards | `32` |
309
+ | `Mudis.start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
310
+ | `Mudis.hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
311
+ | `Mudis.max_bytes` | Maximum allowed cache size | `1GB`|
312
312
 
313
- To customize the number of buckets, set the `MUDIS_BUCKETS` environment variable.
313
+ Buckets can also be set using a `MUDIS_BUCKETS` environment variable.
314
314
 
315
315
  When setting `serializer`, be mindful of the below
316
316
 
@@ -320,21 +320,69 @@ When setting `serializer`, be mindful of the below
320
320
  | `JSON` | Cross-language interoperability |
321
321
  | `Oj` | API-heavy apps using JSON at scale |
322
322
 
323
- #### Benchmarks
323
+ ---
324
+
325
+ ## Benchmarks
326
+
327
+ #### Serializer(s)
324
328
 
325
- Based on 100000 iterations
329
+ _100000 iterations_
326
330
 
327
- | Serializer | Iterations | Total Time (s) | Ops/sec |
328
- |----------------|------------|----------------|---------|
329
- | oj | 100000 | 0.1342 | 745320 |
330
- | marshal | 100000 | 0.3228 | 309824 |
331
- | json | 100000 | 0.9035 | 110682 |
332
- | oj + zlib | 100000 | 1.8050 | 55401 |
333
- | marshal + zlib | 100000 | 1.8057 | 55381 |
334
- | json + zlib | 100000 | 2.7949 | 35780 |
331
+ | Serializer | Total Time (s) | Ops/sec |
332
+ |----------------|------------|----------------|
333
+ | oj | 0.1342 | 745320 |
334
+ | marshal | 0.3228 | 309824 |
335
+ | json | 0.9035 | 110682 |
336
+ | oj + zlib | 1.8050 | 55401 |
337
+ | marshal + zlib | 1.8057 | 55381 |
338
+ | json + zlib | 2.7949 | 35780 |
335
339
 
336
340
  > If opting for OJ, you will need to install the dependency in your project and configure as needed.
337
341
 
342
+ #### Mudis vs Rails.cache
343
+
344
+ Mudis is marginally slower than `Rails.cache` by design; it trades raw speed for control, observability, and safety.
345
+
346
+ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON_
347
+
348
+ | Operation | `Rails.cache` | `Mudis` | Delta |
349
+ | --------- | ------------- | ----------- | --------- |
350
+ | Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
351
+ | Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
352
+
353
+ > 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.
354
+
355
+ ###### **Why this overhead exists**
356
+
357
+ Mudis includes features that MemoryStore doesn’t:
358
+
359
+ | Feature | Mudis | Rails.cache (MemoryStore) |
360
+ | ------------------ | ---------------------- | --------------------------- |
361
+ | Per-key TTL expiry | ✅ | ⚠️ on access |
362
+ | True LRU eviction | ✅ | ❌ |
363
+ | Hard memory limits | ✅ | ❌ |
364
+ | Value compression | ✅ | ❌ |
365
+ | Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
366
+ | Observability | ✅ | ❌ |
367
+ | Namespacing | ✅ | ❌ Manual scoping |
368
+
369
+ It will be down to the developer to decide if a fraction of a millisecond is worth
370
+
371
+ - Predictable eviction
372
+ - Configurable expiry
373
+ - Memory protection
374
+ - Namespace scoping
375
+ - Real-time metrics for hits, misses, evictions, memory usage
376
+
377
+ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)_
378
+
379
+ | Operation | `Rails.cache` | `Mudis` | Delta |
380
+ | --------- | ------------- | ----------- | ------------- |
381
+ | Write | 2.342 ms/op | 0.501 ms/op | **−1.841 ms** |
382
+ | Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
383
+
384
+ With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
385
+
338
386
  ---
339
387
 
340
388
  ## Graceful Shutdown
@@ -354,6 +402,115 @@ at_exit { Mudis.stop_expiry_thread }
354
402
 
355
403
  ---
356
404
 
405
+ ## Create a Mudis Server
406
+
407
+ ### Minimal Setup
408
+
409
+ - Create a new Rails API app:
410
+
411
+ ```bash
412
+ rails new mudis-server --api
413
+ cd mudis-server
414
+ ```
415
+
416
+ - Add mudis to your Gemfile
417
+ - Create Initializer: `config/initializers/mudis.rb`
418
+ - Define routes
419
+
420
+ ```ruby
421
+ Rails.application.routes.draw do
422
+ get "/cache/:key", to: "cache#show"
423
+ post "/cache/:key", to: "cache#write"
424
+ delete "/cache/:key", to: "cache#delete"
425
+ get "/metrics", to: "cache#metrics"
426
+ end
427
+ ```
428
+
429
+ - Create a `cache_controller` (with optional per caller/consumer namespace)
430
+
431
+ ```ruby
432
+ class CacheController < ApplicationController
433
+
434
+ def show
435
+ key = params[:key]
436
+ ns = params[:namespace]
437
+
438
+ value = Mudis.read(key, namespace: ns)
439
+ if value.nil?
440
+ render json: { error: "not found" }, status: :not_found
441
+ else
442
+ render json: { value: value }
443
+ end
444
+ end
445
+
446
+ def write
447
+ key = params[:key]
448
+ ns = params[:namespace]
449
+ val = params[:value]
450
+ ttl = params[:expires_in]&.to_i
451
+
452
+ Mudis.write(key, val, expires_in: ttl, namespace: ns)
453
+ render json: { status: "written", key: key }
454
+ end
455
+
456
+ def delete
457
+ key = params[:key]
458
+ ns = params[:namespace]
459
+
460
+ Mudis.delete(key, namespace: ns)
461
+ render json: { status: "deleted" }
462
+ end
463
+
464
+ def metrics
465
+ render json: Mudis.metrics
466
+ end
467
+ end
468
+ ```
469
+
470
+ - Test it
471
+
472
+ ```bash
473
+ curl http://localhost:3000/cache/foo
474
+ curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
475
+ curl http://localhost:3000/metrics
476
+
477
+ # Write with namespace
478
+ curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
479
+ -d "value=123&expires_in=60"
480
+
481
+ # Read from namespace
482
+ curl "http://localhost:3000/cache/foo?namespace=orders"
483
+
484
+ # Delete from namespace
485
+ curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
486
+
487
+ ```
488
+
489
+ ---
490
+
491
+ ## Project Philosophy
492
+
493
+ Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
494
+
495
+ - In-process caching
496
+ - Fine-grained memory and namespace control
497
+ - Observability and testing friendliness
498
+ - Minimal external dependencies
499
+ - Configurability without complexity
500
+
501
+ The primary use cases are:
502
+
503
+ - Per-service application caches
504
+ - Short-lived local caching inside background jobs or API layers
505
+
506
+ 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.,
507
+
508
+ - mudis-server – expose Mudis via HTTP, web sockets, hooks, etc
509
+ - mudis-broker – distributed key routing layer for coordinating multiple Mudis nodes
510
+ - mudis-activejob-store – adapter for using Mudis in job queues or retry buffers
511
+
512
+ ---
513
+
357
514
  ## Roadmap
358
515
 
359
516
  #### API Enhancements
@@ -379,14 +536,7 @@ MIT License © kiebor81
379
536
 
380
537
  ## Contributing
381
538
 
382
- PRs are welcome! To get started:
383
-
384
- ```bash
385
- git clone https://github.com/kiebor81/mudis
386
- cd mudis
387
- bundle install
388
-
389
- ```
539
+ See [contributor's guide](CONTRIBUTING.md)
390
540
 
391
541
  ---
392
542
 
data/lib/mudis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- MUDIS_VERSION = "0.4.2"
3
+ MUDIS_VERSION = "0.4.4"
data/lib/mudis.rb CHANGED
@@ -1,4 +1,4 @@
1
- # lib/mudis.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
4
  require "thread" # rubocop:disable Lint/RedundantRequireStatement
@@ -19,8 +19,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
19
19
  @stop_expiry = false # Signal for stopping expiry thread
20
20
 
21
21
  class << self
22
- attr_accessor :serializer, :compress, :max_value_bytes, :hard_memory_limit
23
- attr_reader :max_bytes
22
+ attr_accessor :serializer, :compress, :hard_memory_limit
23
+ attr_reader :max_bytes, :max_value_bytes
24
24
 
25
25
  # Configures Mudis with a block, allowing customization of settings
26
26
  def configure
@@ -35,6 +35,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
35
35
 
36
36
  # Applies the current configuration to Mudis
37
37
  def apply_config!
38
+ validate_config!
39
+
38
40
  self.serializer = config.serializer
39
41
  self.compress = config.compress
40
42
  self.max_value_bytes = config.max_value_bytes
@@ -42,6 +44,18 @@ class Mudis # rubocop:disable Metrics/ClassLength
42
44
  self.max_bytes = config.max_bytes
43
45
  end
44
46
 
47
+ # Validates the current configuration, raising errors for invalid settings
48
+ def validate_config! # rubocop:disable Metrics/AbcSize
49
+ if config.max_value_bytes && config.max_value_bytes > config.max_bytes
50
+ raise ArgumentError,
51
+ "max_value_bytes cannot exceed max_bytes"
52
+ end
53
+
54
+ raise ArgumentError, "max_value_bytes must be > 0" if config.max_value_bytes && config.max_value_bytes <= 0
55
+
56
+ raise ArgumentError, "buckets must be > 0" if config.buckets && config.buckets <= 0
57
+ end
58
+
45
59
  # Returns a snapshot of metrics (thread-safe)
46
60
  def metrics # rubocop:disable Metrics/MethodLength
47
61
  @metrics_mutex.synchronize do
@@ -89,9 +103,18 @@ class Mudis # rubocop:disable Metrics/ClassLength
89
103
 
90
104
  # Sets the maximum size for a single value in bytes
91
105
  def max_bytes=(value)
106
+ raise ArgumentError, "max_bytes must be > 0" if value.to_i <= 0
107
+
92
108
  @max_bytes = value
93
109
  @threshold_bytes = (@max_bytes * 0.9).to_i
94
110
  end
111
+
112
+ # Sets the maximum size for a single value in bytes, raising an error if invalid
113
+ def max_value_bytes=(value)
114
+ raise ArgumentError, "max_value_bytes must be > 0" if value && value.to_i <= 0
115
+
116
+ @max_value_bytes = value
117
+ end
95
118
  end
96
119
 
97
120
  # Node structure for the LRU doubly-linked list
@@ -107,7 +130,12 @@ class Mudis # rubocop:disable Metrics/ClassLength
107
130
 
108
131
  # Number of cache buckets (shards). Default: 32
109
132
  def self.buckets
110
- @buckets ||= (ENV["MUDIS_BUCKETS"]&.to_i || 32) # rubocop:disable Style/RedundantParentheses
133
+ return @buckets if @buckets
134
+
135
+ val = config.buckets || ENV["MUDIS_BUCKETS"]&.to_i || 32
136
+ raise ArgumentError, "bucket count must be > 0" if val <= 0
137
+
138
+ @buckets = val
111
139
  end
112
140
 
113
141
  # --- Internal Structures ---
data/lib/mudis_config.rb CHANGED
@@ -7,7 +7,8 @@ class MudisConfig
7
7
  :compress,
8
8
  :max_value_bytes,
9
9
  :hard_memory_limit,
10
- :max_bytes
10
+ :max_bytes,
11
+ :buckets
11
12
 
12
13
  def initialize
13
14
  @serializer = JSON # Default serialization strategy
@@ -15,5 +16,6 @@ class MudisConfig
15
16
  @max_value_bytes = nil # Max size per value (optional)
16
17
  @hard_memory_limit = false # Enforce max_bytes as hard cap
17
18
  @max_bytes = 1_073_741_824 # 1 GB default max cache size
19
+ @buckets = nil # use nil to signal fallback to ENV or default
18
20
  end
19
21
  end
data/sig/mudis.rbs CHANGED
@@ -3,14 +3,15 @@ class Mudis
3
3
  class << self
4
4
  attr_accessor serializer : Object
5
5
  attr_accessor compress : bool
6
- attr_accessor max_value_bytes : Integer?
7
6
  attr_accessor hard_memory_limit : bool
8
7
  attr_reader max_bytes : Integer
9
- def max_bytes=: (Integer) -> void
8
+ attr_reader max_value_bytes : Integer?
10
9
 
11
10
  def configure: () { (config: MudisConfig) -> void } -> void
12
11
  def config: () -> MudisConfig
13
12
  def apply_config!: () -> void
13
+
14
+ def buckets: () -> Integer
14
15
  end
15
16
 
16
17
  # Lifecycle
data/sig/mudis_config.rbs CHANGED
@@ -4,4 +4,5 @@ class MudisConfig
4
4
  attr_accessor max_value_bytes : Integer?
5
5
  attr_accessor hard_memory_limit : bool
6
6
  attr_accessor max_bytes : Integer
7
+ attr_accessor buckets : Integer?
7
8
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "climate_control"
5
+
6
+ RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
7
+ after { Mudis.reset! }
8
+
9
+ describe "bucket configuration" do
10
+ it "defaults to 32 buckets if ENV is nil" do
11
+ Mudis.instance_variable_set(:@buckets, nil) # force recomputation
12
+ ClimateControl.modify(MUDIS_BUCKETS: nil) do
13
+ expect(Mudis.send(:buckets)).to eq(32)
14
+ end
15
+ end
16
+
17
+ it "raises if MUDIS_BUCKETS is 0 or less" do
18
+ expect do
19
+ Mudis.instance_variable_set(:@buckets, nil) # force recomputation
20
+ ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
21
+ end.to raise_error(ArgumentError, /bucket count must be > 0/)
22
+
23
+ expect do
24
+ Mudis.instance_variable_set(:@buckets, nil) # force recomputation
25
+ ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
26
+ end.to raise_error(ArgumentError, /bucket count must be > 0/)
27
+ end
28
+ end
29
+
30
+ describe "memory configuration" do
31
+ it "raises if max_bytes is set to 0 or less" do
32
+ expect do
33
+ Mudis.max_bytes = 0
34
+ end.to raise_error(ArgumentError, /max_bytes must be > 0/)
35
+
36
+ expect do
37
+ Mudis.max_bytes = -1
38
+ end.to raise_error(ArgumentError, /max_bytes must be > 0/)
39
+ end
40
+
41
+ it "raises if max_value_bytes is 0 or less via config" do
42
+ expect do
43
+ Mudis.configure do |c|
44
+ c.max_value_bytes = 0
45
+ end
46
+ end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
47
+ end
48
+
49
+ it "raises if max_value_bytes exceeds max_bytes" do
50
+ expect do
51
+ Mudis.configure do |c|
52
+ c.max_bytes = 1_000_000
53
+ c.max_value_bytes = 2_000_000
54
+ end
55
+ end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
56
+ end
57
+ end
58
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mudis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - kiebor81
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-16 00:00:00.000000000 Z
11
+ date: 2025-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: climate_control
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,8 +38,9 @@ dependencies:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
40
  version: '3.12'
27
- description: Thread-safe, bucketed, in-process cache for Ruby apps. Drop-in replacement
28
- for Kredis in some scenarios.
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.
29
44
  email:
30
45
  executables: []
31
46
  extensions: []
@@ -39,6 +54,7 @@ files:
39
54
  - lib/mudis_config.rb
40
55
  - sig/mudis.rbs
41
56
  - sig/mudis_config.rbs
57
+ - spec/guardrails_spec.rb
42
58
  - spec/mudis_spec.rb
43
59
  homepage: https://github.com/kiebor81/mudis
44
60
  licenses:
@@ -62,6 +78,8 @@ requirements: []
62
78
  rubygems_version: 3.5.17
63
79
  signing_key:
64
80
  specification_version: 4
65
- summary: A fast in-memory Ruby LRU cache with compression and expiry.
81
+ summary: A fast in-memory, thread-safe and high performance Ruby LRU cache with compression
82
+ and auto-expiry.
66
83
  test_files:
84
+ - spec/guardrails_spec.rb
67
85
  - spec/mudis_spec.rb