mudis 0.3.1 → 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 +4 -4
- data/README.md +83 -17
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +65 -2
- data/lib/mudis_config.rb +19 -0
- data/spec/mudis_spec.rb +67 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a107a0fdb170a8979e0a0a8ae241c6a2bb632f50a314443124b48e82488a72f3
|
4
|
+
data.tar.gz: 438e08ddede26d761cc490d5e83e363142f264b67e67d8c2b5395ef8a7a18ff6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e261bbf2112035136d4967fc4064aed07c07918bc5322841e9c70f1761b50b2070265ae6453e5a0447b34dbfb355d5f5fd8f419c8da70808f822441ad780926
|
7
|
+
data.tar.gz: 3a622667d7000e9897ce301f8d40a202a10ba86ab0bb1178ed0b6fe250113b6b68942f1a39348572b8c9d607b852e58026e945aecbb23e3001e05d60fff34348
|
data/README.md
CHANGED
@@ -46,6 +46,10 @@ There are plenty out there, in various states of maintenance and in many shapes
|
|
46
46
|
|
47
47
|
## Design
|
48
48
|
|
49
|
+
#### Internal Structure and Behaviour
|
50
|
+
|
51
|
+

|
52
|
+
|
49
53
|
#### Write - Read - Eviction
|
50
54
|
|
51
55
|

|
@@ -89,20 +93,34 @@ In your Rails app, create an initializer:
|
|
89
93
|
|
90
94
|
```ruby
|
91
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
|
92
103
|
|
93
|
-
Mudis.serializer = JSON # or Marshal | Oj
|
94
|
-
Mudis.compress = true # Compress values using Zlib
|
95
|
-
Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
|
96
104
|
Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
|
97
|
-
Mudis.hard_memory_limit = true # enforce hard memory limits
|
98
|
-
Mudis.max_bytes = 1_073_741_824 # set maximum cache size
|
99
105
|
|
100
106
|
at_exit do
|
101
107
|
Mudis.stop_expiry_thread
|
102
108
|
end
|
103
109
|
```
|
104
110
|
|
105
|
-
|
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
|
+
```
|
106
124
|
|
107
125
|
---
|
108
126
|
|
@@ -127,6 +145,37 @@ Mudis.update('user:123') { |data| data.merge(age: 30) }
|
|
127
145
|
Mudis.delete('user:123')
|
128
146
|
```
|
129
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
|
+
|
130
179
|
---
|
131
180
|
|
132
181
|
## Rails Service Integration
|
@@ -222,25 +271,29 @@ Track cache effectiveness and performance:
|
|
222
271
|
|
223
272
|
```ruby
|
224
273
|
Mudis.metrics
|
225
|
-
# => {
|
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
|
+
|
226
286
|
```
|
227
287
|
|
228
|
-
Optionally, return these metrics from a controller for remote analysis and monitoring if using
|
288
|
+
Optionally, return these metrics from a controller for remote analysis and monitoring if using Rails.
|
229
289
|
|
230
290
|
```ruby
|
231
291
|
class MudisController < ApplicationController
|
232
|
-
|
233
292
|
def metrics
|
234
|
-
render json: {
|
235
|
-
mudis_metrics: Mudis.metrics,
|
236
|
-
memory_used_bytes: Mudis.current_memory_bytes,
|
237
|
-
memory_max_bytes: Mudis.max_memory_bytes,
|
238
|
-
keys: Mudis.all_keys.size
|
239
|
-
}
|
293
|
+
render json: { mudis: Mudis.metrics }
|
240
294
|
end
|
241
295
|
|
242
296
|
end
|
243
|
-
|
244
297
|
```
|
245
298
|
|
246
299
|
---
|
@@ -303,7 +356,18 @@ at_exit { Mudis.stop_expiry_thread }
|
|
303
356
|
|
304
357
|
## Roadmap
|
305
358
|
|
306
|
-
|
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
|
307
371
|
|
308
372
|
---
|
309
373
|
|
@@ -311,6 +375,8 @@ at_exit { Mudis.stop_expiry_thread }
|
|
311
375
|
|
312
376
|
MIT License © kiebor81
|
313
377
|
|
378
|
+
---
|
379
|
+
|
314
380
|
## Contributing
|
315
381
|
|
316
382
|
PRs are welcome! To get started:
|
data/lib/mudis/version.rb
CHANGED
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
|
@@ -20,11 +22,72 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
20
22
|
attr_accessor :serializer, :compress, :max_value_bytes, :hard_memory_limit
|
21
23
|
attr_reader :max_bytes
|
22
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
|
44
|
+
|
23
45
|
# Returns a snapshot of metrics (thread-safe)
|
24
|
-
def metrics
|
25
|
-
@metrics_mutex.synchronize
|
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!
|
26
88
|
end
|
27
89
|
|
90
|
+
# Sets the maximum size for a single value in bytes
|
28
91
|
def max_bytes=(value)
|
29
92
|
@max_bytes = value
|
30
93
|
@threshold_bytes = (@max_bytes * 0.9).to_i
|
data/lib/mudis_config.rb
ADDED
@@ -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
@@ -227,4 +227,71 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
227
227
|
expect(Mudis.current_memory_bytes).to be > 0
|
228
228
|
end
|
229
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
|
230
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.
|
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
|