mudis 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdff64ca287bd83eaa37c48afbc375e4303b7d3cc8d368b80c1d996d9ad0e3c0
4
- data.tar.gz: 20d5d5fbc3afdd79d5a6d826a4a62419d1af60532e4b2515d4d9b2c0956e6174
3
+ metadata.gz: a107a0fdb170a8979e0a0a8ae241c6a2bb632f50a314443124b48e82488a72f3
4
+ data.tar.gz: 438e08ddede26d761cc490d5e83e363142f264b67e67d8c2b5395ef8a7a18ff6
5
5
  SHA512:
6
- metadata.gz: 7de9f04b093e0ae1fadbd7f967fdf843aff8b7d530e766454848ac43afae8bce58228f4d8d3a05349d4d4983c7163b9063a31181de9eb0d30ac0fc3cac7da6b3
7
- data.tar.gz: '097252131a51b2dc34db4b8da6dc442ccaa01eb8c49334cfdd658bef9321330f7d8c75037e61abce08ffd6491673d92c75f7438419a964b022c6b53749159936'
6
+ metadata.gz: 4e261bbf2112035136d4967fc4064aed07c07918bc5322841e9c70f1761b50b2070265ae6453e5a0447b34dbfb355d5f5fd8f419c8da70808f822441ad780926
7
+ data.tar.gz: 3a622667d7000e9897ce301f8d40a202a10ba86ab0bb1178ed0b6fe250113b6b68942f1a39348572b8c9d607b852e58026e945aecbb23e3001e05d60fff34348
data/README.md CHANGED
@@ -1,17 +1,55 @@
1
1
  ![mudis_signet](design/mudis.png "Mudis")
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/mudis)
3
+ [![Gem Version](https://badge.fury.io/rb/mudis.svg?icon=si%3Arubygems&refresh=1)](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
- 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.
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.
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 v0.3.0** | **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 | ❌ |
10
44
 
11
45
  ---
12
46
 
13
47
  ## Design
14
48
 
49
+ #### Internal Structure and Behaviour
50
+
51
+ ![mudis_flow](design/mudis_obj.png "Mudis Internals")
52
+
15
53
  #### Write - Read - Eviction
16
54
 
17
55
  ![mudis_flow](design/mudis_flow.png "Write - Read - Eviction")
@@ -55,19 +93,34 @@ In your Rails app, create an initializer:
55
93
 
56
94
  ```ruby
57
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
58
103
 
59
- Mudis.serializer = JSON # or Marshal | Oj
60
- Mudis.compress = true # Compress values using Zlib
61
- Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
62
104
  Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
63
- Mudis.hard_memory_limit = true # enforce hard memory limits
64
105
 
65
106
  at_exit do
66
107
  Mudis.stop_expiry_thread
67
108
  end
68
109
  ```
69
110
 
70
- > If your `lib/` folder isn't eager loaded, explicitly `require 'mudis'` in this file.
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
+ ```
71
124
 
72
125
  ---
73
126
 
@@ -92,6 +145,37 @@ Mudis.update('user:123') { |data| data.merge(age: 30) }
92
145
  Mudis.delete('user:123')
93
146
  ```
94
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
+
95
179
  ---
96
180
 
97
181
  ## Rails Service Integration
@@ -187,25 +271,29 @@ Track cache effectiveness and performance:
187
271
 
188
272
  ```ruby
189
273
  Mudis.metrics
190
- # => { hits: 15, misses: 5, evictions: 3, rejections: 0 }
274
+ # => {
275
+ # hits: 15,
276
+ # misses: 5,
277
+ # evictions: 3,
278
+ # rejected: 0,
279
+ # total_memory: 45678,
280
+ # buckets: [
281
+ # { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
282
+ # ...
283
+ # ]
284
+ # }
285
+
191
286
  ```
192
287
 
193
- Optionally, return these metrics from a controller for remote analysis and monitoring if using rails.
288
+ Optionally, return these metrics from a controller for remote analysis and monitoring if using Rails.
194
289
 
195
290
  ```ruby
196
291
  class MudisController < ApplicationController
197
-
198
292
  def metrics
199
- render json: {
200
- mudis_metrics: Mudis.metrics,
201
- memory_used_bytes: Mudis.current_memory_bytes,
202
- memory_max_bytes: Mudis.max_memory_bytes,
203
- keys: Mudis.all_keys.size
204
- }
293
+ render json: { mudis: Mudis.metrics }
205
294
  end
206
295
 
207
296
  end
208
-
209
297
  ```
210
298
 
211
299
  ---
@@ -219,7 +307,8 @@ end
219
307
  | `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
220
308
  | `Mudis.buckets` | Number of cache shards (via ENV var) | `32` |
221
309
  | `start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
222
- | `hard_memory_limit ` | Enfirce hard memory limits on key size and reject if exceeded | `false`|
310
+ | `hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
311
+ | `max_bytes` | Maximum allowed cache size | `1GB`|
223
312
 
224
313
  To customize the number of buckets, set the `MUDIS_BUCKETS` environment variable.
225
314
 
@@ -244,7 +333,7 @@ Based on 100000 iterations
244
333
  | marshal + zlib | 100000 | 1.8057 | 55381 |
245
334
  | json + zlib | 100000 | 2.7949 | 35780 |
246
335
 
247
- > If opting for OJ, you will need to install the dependncy in your project and configure as needed.
336
+ > If opting for OJ, you will need to install the dependency in your project and configure as needed.
248
337
 
249
338
  ---
250
339
 
@@ -267,7 +356,18 @@ at_exit { Mudis.stop_expiry_thread }
267
356
 
268
357
  ## Roadmap
269
358
 
270
- - [ ] Stats per bucket
359
+ #### API Enhancements
360
+
361
+ - [ ] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
362
+
363
+ #### Safety & Policy Controls
364
+
365
+ - [ ] max_ttl: Enforce a global upper bound on expires_in to prevent excessively long-lived keys
366
+ - [ ] default_ttl: Provide a fallback TTL when one is not specified
367
+
368
+ #### Debugging
369
+
370
+ - [ ] clear_namespace(namespace): Remove all keys in a namespace in one call
271
371
 
272
372
  ---
273
373
 
@@ -293,3 +393,5 @@ bundle install
293
393
  ## Contact
294
394
 
295
395
  For issues, suggestions, or feedback, please open a GitHub issue
396
+
397
+ ---
data/lib/mudis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- MUDIS_VERSION = "0.3.0"
3
+ MUDIS_VERSION = "0.4.0"
data/lib/mudis.rb CHANGED
@@ -4,6 +4,8 @@ require "json"
4
4
  require "thread" # rubocop:disable Lint/RedundantRequireStatement
5
5
  require "zlib"
6
6
 
7
+ require_relative "mudis_config"
8
+
7
9
  # Mudis is a thread-safe, in-memory, sharded, LRU cache with optional compression and expiry.
8
10
  # It is designed for high concurrency and performance within a Ruby application.
9
11
  class Mudis # rubocop:disable Metrics/ClassLength
@@ -18,10 +20,77 @@ class Mudis # rubocop:disable Metrics/ClassLength
18
20
 
19
21
  class << self
20
22
  attr_accessor :serializer, :compress, :max_value_bytes, :hard_memory_limit
23
+ attr_reader :max_bytes
24
+
25
+ # Configures Mudis with a block, allowing customization of settings
26
+ def configure
27
+ yield(config)
28
+ apply_config!
29
+ end
30
+
31
+ # Returns the current configuration object
32
+ def config
33
+ @config ||= MudisConfig.new
34
+ end
35
+
36
+ # Applies the current configuration to Mudis
37
+ def apply_config!
38
+ self.serializer = config.serializer
39
+ self.compress = config.compress
40
+ self.max_value_bytes = config.max_value_bytes
41
+ self.hard_memory_limit = config.hard_memory_limit
42
+ self.max_bytes = config.max_bytes
43
+ end
21
44
 
22
45
  # Returns a snapshot of metrics (thread-safe)
23
- def metrics
24
- @metrics_mutex.synchronize { @metrics.dup }
46
+ def metrics # rubocop:disable Metrics/MethodLength
47
+ @metrics_mutex.synchronize do
48
+ {
49
+ hits: @metrics[:hits],
50
+ misses: @metrics[:misses],
51
+ evictions: @metrics[:evictions],
52
+ rejected: @metrics[:rejected],
53
+ total_memory: current_memory_bytes,
54
+ buckets: buckets.times.map do |idx|
55
+ {
56
+ index: idx,
57
+ keys: @stores[idx].size,
58
+ memory_bytes: @current_bytes[idx],
59
+ lru_size: @lru_nodes[idx].size
60
+ }
61
+ end
62
+ }
63
+ end
64
+ end
65
+
66
+ # Resets metric counters (thread-safe)
67
+ def reset_metrics!
68
+ @metrics_mutex.synchronize do
69
+ @metrics = { hits: 0, misses: 0, evictions: 0, rejected: 0 }
70
+ end
71
+ end
72
+
73
+ # Fully resets all internal state (except config)
74
+ def reset!
75
+ stop_expiry_thread
76
+
77
+ @buckets = nil
78
+ b = buckets
79
+
80
+ @stores = Array.new(b) { {} }
81
+ @mutexes = Array.new(b) { Mutex.new }
82
+ @lru_heads = Array.new(b) { nil }
83
+ @lru_tails = Array.new(b) { nil }
84
+ @lru_nodes = Array.new(b) { {} }
85
+ @current_bytes = Array.new(b, 0)
86
+
87
+ reset_metrics!
88
+ end
89
+
90
+ # Sets the maximum size for a single value in bytes
91
+ def max_bytes=(value)
92
+ @max_bytes = value
93
+ @threshold_bytes = (@max_bytes * 0.9).to_i
25
94
  end
26
95
  end
27
96
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MudisConfig holds all configuration values for Mudis,
4
+ # and provides defaults that can be overridden via Mudis.configure.
5
+ class MudisConfig
6
+ attr_accessor :serializer,
7
+ :compress,
8
+ :max_value_bytes,
9
+ :hard_memory_limit,
10
+ :max_bytes
11
+
12
+ def initialize
13
+ @serializer = JSON # Default serialization strategy
14
+ @compress = false # Whether to compress values with Zlib
15
+ @max_value_bytes = nil # Max size per value (optional)
16
+ @hard_memory_limit = false # Enforce max_bytes as hard cap
17
+ @max_bytes = 1_073_741_824 # 1 GB default max cache size
18
+ end
19
+ end
data/spec/mudis_spec.rb CHANGED
@@ -216,10 +216,82 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
216
216
  end
217
217
  end
218
218
 
219
+ it "respects max_bytes when updated externally" do
220
+ Mudis.max_bytes = 100
221
+ expect(Mudis.send(:max_bytes)).to eq(100)
222
+ end
223
+
219
224
  describe ".current_memory_bytes" do
220
225
  it "returns a non-zero byte count after writes" do
221
226
  Mudis.write("size_test", "a" * 100)
222
227
  expect(Mudis.current_memory_bytes).to be > 0
223
228
  end
224
229
  end
230
+
231
+ describe ".metrics" do
232
+ it "includes per-bucket stats" do
233
+ Mudis.write("a", "x" * 50)
234
+ metrics = Mudis.metrics
235
+
236
+ expect(metrics).to include(:buckets)
237
+ expect(metrics[:buckets]).to be_an(Array)
238
+ expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
239
+ end
240
+ end
241
+
242
+ describe ".configure" do
243
+ it "applies configuration settings correctly" do
244
+ Mudis.configure do |c|
245
+ c.serializer = JSON
246
+ c.compress = true
247
+ c.max_value_bytes = 12_345
248
+ c.hard_memory_limit = true
249
+ c.max_bytes = 987_654
250
+ end
251
+
252
+ expect(Mudis.compress).to eq(true)
253
+ expect(Mudis.max_value_bytes).to eq(12_345)
254
+ expect(Mudis.hard_memory_limit).to eq(true)
255
+ expect(Mudis.max_bytes).to eq(987_654)
256
+ end
257
+ end
258
+
259
+ describe ".reset!" do
260
+ it "clears all stores, memory, and metrics" do
261
+ Mudis.write("reset_key", "value")
262
+ expect(Mudis.read("reset_key")).to eq("value")
263
+ expect(Mudis.current_memory_bytes).to be > 0
264
+ expect(Mudis.metrics[:hits]).to be >= 0
265
+
266
+ Mudis.reset!
267
+
268
+ metrics = Mudis.metrics
269
+ expect(metrics[:hits]).to eq(0)
270
+ expect(metrics[:misses]).to eq(0)
271
+ expect(metrics[:evictions]).to eq(0)
272
+ expect(metrics[:rejected]).to eq(0)
273
+ expect(Mudis.current_memory_bytes).to eq(0)
274
+ expect(Mudis.all_keys).to be_empty
275
+
276
+ # Optionally confirm reset_key is now gone
277
+ expect(Mudis.read("reset_key")).to be_nil
278
+ end
279
+ end
280
+
281
+ describe ".reset_metrics!" do
282
+ it "resets only the metrics without clearing cache" do
283
+ Mudis.write("metrics_key", "value")
284
+ Mudis.read("metrics_key") # generates :hits
285
+ Mudis.read("missing_key") # generates :misses
286
+
287
+ expect(Mudis.metrics[:hits]).to eq(1)
288
+ expect(Mudis.metrics[:misses]).to eq(1)
289
+
290
+ Mudis.reset_metrics!
291
+
292
+ expect(Mudis.metrics[:hits]).to eq(0)
293
+ expect(Mudis.metrics[:misses]).to eq(0)
294
+ expect(Mudis.read("metrics_key")).to eq("value") # still exists
295
+ end
296
+ end
225
297
  end
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kiebor81
@@ -35,6 +35,7 @@ files:
35
35
  - README.md
36
36
  - lib/mudis.rb
37
37
  - lib/mudis/version.rb
38
+ - lib/mudis_config.rb
38
39
  - sig/mudis.rbs
39
40
  - spec/mudis_spec.rb
40
41
  homepage: https://github.com/kiebor81/mudis