mudis 0.2.0 → 0.3.1
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 +121 -20
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +48 -14
- data/spec/mudis_spec.rb +54 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bdaf73fb205686d551ea90a70ffe82e93287c9fcb2e9209e25e626f200c84768
|
4
|
+
data.tar.gz: 778faad72b00b0f2b477df2ec53e107aba064d2cfe4e0a8b53a8d4623521d1ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e860b5e07f84e20481463eb4bf36984e094b2f95c70d1be4c072318b7e089858b7355ca8e44476495354676ce403b4e4e5cf5e796376c1d155bb47bc60e9ea21
|
7
|
+
data.tar.gz: 902113917b4b1a376d850a6a9d2d54994bebdf52c4834680524a43d9407f71343899a07c2b206080d9de9e522d3538b3288e74f065152e3458ee4617fc8eb293
|
data/README.md
CHANGED
@@ -1,12 +1,46 @@
|
|
1
1
|

|
2
2
|
|
3
|
-
[](https://
|
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
|
-
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
|
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
|
|
@@ -60,6 +94,8 @@ Mudis.serializer = JSON # or Marshal | Oj
|
|
60
94
|
Mudis.compress = true # Compress values using Zlib
|
61
95
|
Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
|
62
96
|
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
|
63
99
|
|
64
100
|
at_exit do
|
65
101
|
Mudis.stop_expiry_thread
|
@@ -95,44 +131,87 @@ Mudis.delete('user:123')
|
|
95
131
|
|
96
132
|
## Rails Service Integration
|
97
133
|
|
98
|
-
For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class
|
134
|
+
For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
|
99
135
|
|
100
136
|
```ruby
|
101
137
|
class MudisService
|
102
|
-
attr_reader :cache_key
|
138
|
+
attr_reader :cache_key, :namespace
|
103
139
|
|
104
|
-
|
140
|
+
# Initialize the service with a cache key and optional namespace
|
141
|
+
#
|
142
|
+
# @param cache_key [String] the base key to use
|
143
|
+
# @param namespace [String, nil] optional logical namespace
|
144
|
+
def initialize(cache_key, namespace: nil)
|
105
145
|
@cache_key = cache_key
|
146
|
+
@namespace = namespace
|
106
147
|
end
|
107
148
|
|
149
|
+
# Write a value to the cache
|
150
|
+
#
|
151
|
+
# @param data [Object] the value to cache
|
152
|
+
# @param expires_in [Integer, nil] optional TTL in seconds
|
108
153
|
def write(data, expires_in: nil)
|
109
|
-
Mudis.write(cache_key, data, expires_in: expires_in)
|
154
|
+
Mudis.write(cache_key, data, expires_in: expires_in, namespace: namespace)
|
110
155
|
end
|
111
156
|
|
157
|
+
# Read the cached value or return default
|
158
|
+
#
|
159
|
+
# @param default [Object] fallback value if key is not present
|
112
160
|
def read(default: nil)
|
113
|
-
Mudis.read(cache_key) || default
|
161
|
+
Mudis.read(cache_key, namespace: namespace) || default
|
114
162
|
end
|
115
163
|
|
164
|
+
# Update the cached value using a block
|
165
|
+
#
|
166
|
+
# @yieldparam current [Object] the current value
|
167
|
+
# @yieldreturn [Object] the updated value
|
116
168
|
def update
|
117
|
-
Mudis.update(cache_key) { |current| yield(current) }
|
169
|
+
Mudis.update(cache_key, namespace: namespace) { |current| yield(current) }
|
118
170
|
end
|
119
171
|
|
172
|
+
# Delete the key from cache
|
120
173
|
def delete
|
121
|
-
Mudis.delete(cache_key)
|
174
|
+
Mudis.delete(cache_key, namespace: namespace)
|
122
175
|
end
|
123
176
|
|
177
|
+
# Return true if the key exists in cache
|
124
178
|
def exists?
|
125
|
-
Mudis.exists?(cache_key)
|
179
|
+
Mudis.exists?(cache_key, namespace: namespace)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Fetch from cache or compute and store it
|
183
|
+
#
|
184
|
+
# @param expires_in [Integer, nil] optional TTL
|
185
|
+
# @param force [Boolean] force recomputation
|
186
|
+
# @yield return value if key is missing
|
187
|
+
def fetch(expires_in: nil, force: false)
|
188
|
+
Mudis.fetch(cache_key, expires_in: expires_in, force: force, namespace: namespace) do
|
189
|
+
yield
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Inspect metadata for the current key
|
194
|
+
#
|
195
|
+
# @return [Hash, nil] metadata including :expires_at, :created_at, :size_bytes, etc.
|
196
|
+
def inspect_meta
|
197
|
+
Mudis.inspect(cache_key, namespace: namespace)
|
126
198
|
end
|
127
199
|
end
|
200
|
+
|
128
201
|
```
|
129
202
|
|
130
203
|
Use it like:
|
131
204
|
|
132
205
|
```ruby
|
133
|
-
cache = MudisService.new("user
|
134
|
-
|
135
|
-
cache.
|
206
|
+
cache = MudisService.new("user:42:profile", namespace: "users")
|
207
|
+
|
208
|
+
cache.write({ name: "Alice" }, expires_in: 300)
|
209
|
+
cache.read # => { "name" => "Alice" }
|
210
|
+
cache.exists? # => true
|
211
|
+
|
212
|
+
cache.update { |data| data.merge(age: 30) }
|
213
|
+
cache.fetch(expires_in: 60) { expensive_query }
|
214
|
+
cache.inspect_meta # => { key: "users:user:42:profile", ... }
|
136
215
|
```
|
137
216
|
|
138
217
|
---
|
@@ -143,7 +222,7 @@ Track cache effectiveness and performance:
|
|
143
222
|
|
144
223
|
```ruby
|
145
224
|
Mudis.metrics
|
146
|
-
# => { hits: 15, misses: 5, evictions: 3 }
|
225
|
+
# => { hits: 15, misses: 5, evictions: 3, rejections: 0 }
|
147
226
|
```
|
148
227
|
|
149
228
|
Optionally, return these metrics from a controller for remote analysis and monitoring if using rails.
|
@@ -175,9 +254,34 @@ end
|
|
175
254
|
| `Mudis.max_value_bytes` | Max allowed size in bytes for a value | `nil` (no limit) |
|
176
255
|
| `Mudis.buckets` | Number of cache shards (via ENV var) | `32` |
|
177
256
|
| `start_expiry_thread` | Background TTL cleanup loop (every N sec) | Disabled by default|
|
257
|
+
| `hard_memory_limit` | Enforce hard memory limits on key size and reject if exceeded | `false`|
|
258
|
+
| `max_bytes` | Maximum allowed cache size | `1GB`|
|
178
259
|
|
179
260
|
To customize the number of buckets, set the `MUDIS_BUCKETS` environment variable.
|
180
261
|
|
262
|
+
When setting `serializer`, be mindful of the below
|
263
|
+
|
264
|
+
| Serializer | Recommended for |
|
265
|
+
| ---------- | ------------------------------------- |
|
266
|
+
| `Marshal` | Ruby-only apps, speed-sensitive logic |
|
267
|
+
| `JSON` | Cross-language interoperability |
|
268
|
+
| `Oj` | API-heavy apps using JSON at scale |
|
269
|
+
|
270
|
+
#### Benchmarks
|
271
|
+
|
272
|
+
Based on 100000 iterations
|
273
|
+
|
274
|
+
| Serializer | Iterations | Total Time (s) | Ops/sec |
|
275
|
+
|----------------|------------|----------------|---------|
|
276
|
+
| oj | 100000 | 0.1342 | 745320 |
|
277
|
+
| marshal | 100000 | 0.3228 | 309824 |
|
278
|
+
| json | 100000 | 0.9035 | 110682 |
|
279
|
+
| oj + zlib | 100000 | 1.8050 | 55401 |
|
280
|
+
| marshal + zlib | 100000 | 1.8057 | 55381 |
|
281
|
+
| json + zlib | 100000 | 2.7949 | 35780 |
|
282
|
+
|
283
|
+
> If opting for OJ, you will need to install the dependency in your project and configure as needed.
|
284
|
+
|
181
285
|
---
|
182
286
|
|
183
287
|
## Graceful Shutdown
|
@@ -193,16 +297,13 @@ at_exit { Mudis.stop_expiry_thread }
|
|
193
297
|
## Known Limitations
|
194
298
|
|
195
299
|
- Data is **non-persistent**.
|
196
|
-
- Keys are globally scoped (no namespacing by default). Namespaving must be handled by the caller/consumer using scoped keys.
|
197
300
|
- Compression introduces CPU overhead.
|
198
301
|
|
199
302
|
---
|
200
303
|
|
201
304
|
## Roadmap
|
202
305
|
|
203
|
-
- [ ] Namespaced cache keys
|
204
306
|
- [ ] Stats per bucket
|
205
|
-
- [ ] Optional max memory cap per bucket
|
206
307
|
|
207
308
|
---
|
208
309
|
|
@@ -210,8 +311,6 @@ at_exit { Mudis.stop_expiry_thread }
|
|
210
311
|
|
211
312
|
MIT License © kiebor81
|
212
313
|
|
213
|
-
---
|
214
|
-
|
215
314
|
## Contributing
|
216
315
|
|
217
316
|
PRs are welcome! To get started:
|
@@ -228,3 +327,5 @@ bundle install
|
|
228
327
|
## Contact
|
229
328
|
|
230
329
|
For issues, suggestions, or feedback, please open a GitHub issue
|
330
|
+
|
331
|
+
---
|
data/lib/mudis/version.rb
CHANGED
data/lib/mudis.rb
CHANGED
@@ -11,18 +11,24 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
11
11
|
|
12
12
|
@serializer = JSON # Default serializer (can be changed to Marshal or Oj)
|
13
13
|
@compress = false # Whether to compress values with Zlib
|
14
|
-
@metrics = { hits: 0, misses: 0, evictions: 0 } # Metrics tracking read/write
|
14
|
+
@metrics = { hits: 0, misses: 0, evictions: 0, rejected: 0 } # Metrics tracking read/write behaviour
|
15
15
|
@metrics_mutex = Mutex.new # Mutex for synchronizing access to metrics
|
16
16
|
@max_value_bytes = nil # Optional size cap per value
|
17
17
|
@stop_expiry = false # Signal for stopping expiry thread
|
18
18
|
|
19
19
|
class << self
|
20
|
-
attr_accessor :serializer, :compress, :max_value_bytes
|
20
|
+
attr_accessor :serializer, :compress, :max_value_bytes, :hard_memory_limit
|
21
|
+
attr_reader :max_bytes
|
21
22
|
|
22
23
|
# Returns a snapshot of metrics (thread-safe)
|
23
24
|
def metrics
|
24
25
|
@metrics_mutex.synchronize { @metrics.dup }
|
25
26
|
end
|
27
|
+
|
28
|
+
def max_bytes=(value)
|
29
|
+
@max_bytes = value
|
30
|
+
@threshold_bytes = (@max_bytes * 0.9).to_i
|
31
|
+
end
|
26
32
|
end
|
27
33
|
|
28
34
|
# Node structure for the LRU doubly-linked list
|
@@ -52,6 +58,7 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
52
58
|
@max_bytes = 1_073_741_824 # 1 GB global max cache size
|
53
59
|
@threshold_bytes = (@max_bytes * 0.9).to_i # Eviction threshold at 90%
|
54
60
|
@expiry_thread = nil # Background thread for expiry cleanup
|
61
|
+
@hard_memory_limit = false # Whether to enforce hard memory cap
|
55
62
|
|
56
63
|
class << self
|
57
64
|
# Starts a thread that periodically removes expired entries
|
@@ -82,12 +89,14 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
82
89
|
end
|
83
90
|
|
84
91
|
# Checks if a key exists and is not expired
|
85
|
-
def exists?(key)
|
92
|
+
def exists?(key, namespace: nil)
|
93
|
+
key = namespaced_key(key, namespace)
|
86
94
|
!!read(key)
|
87
95
|
end
|
88
96
|
|
89
97
|
# Reads and returns the value for a key, updating LRU and metrics
|
90
|
-
def read(key) # rubocop:disable Metrics/MethodLength
|
98
|
+
def read(key, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
99
|
+
key = namespaced_key(key, namespace)
|
91
100
|
raw_entry = nil
|
92
101
|
idx = bucket_index(key)
|
93
102
|
mutex = @mutexes[idx]
|
@@ -111,12 +120,18 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
111
120
|
end
|
112
121
|
|
113
122
|
# Writes a value to the cache with optional expiry and LRU tracking
|
114
|
-
def write(key, value, expires_in: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize
|
123
|
+
def write(key, value, expires_in: nil, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
124
|
+
key = namespaced_key(key, namespace)
|
115
125
|
raw = serializer.dump(value)
|
116
126
|
raw = Zlib::Deflate.deflate(raw) if compress
|
117
127
|
size = key.bytesize + raw.bytesize
|
118
128
|
return if max_value_bytes && raw.bytesize > max_value_bytes
|
119
129
|
|
130
|
+
if hard_memory_limit && current_memory_bytes + size > max_memory_bytes
|
131
|
+
metric(:rejected)
|
132
|
+
return
|
133
|
+
end
|
134
|
+
|
120
135
|
idx = bucket_index(key)
|
121
136
|
mutex = @mutexes[idx]
|
122
137
|
store = @stores[idx]
|
@@ -141,7 +156,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
141
156
|
end
|
142
157
|
|
143
158
|
# Atomically updates the value for a key using a block
|
144
|
-
def update(key) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
159
|
+
def update(key, namespace: nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
160
|
+
key = namespaced_key(key, namespace)
|
145
161
|
idx = bucket_index(key)
|
146
162
|
mutex = @mutexes[idx]
|
147
163
|
store = @stores[idx]
|
@@ -167,7 +183,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
167
183
|
end
|
168
184
|
|
169
185
|
# Deletes a key from the cache
|
170
|
-
def delete(key)
|
186
|
+
def delete(key, namespace: nil)
|
187
|
+
key = namespaced_key(key, namespace)
|
171
188
|
idx = bucket_index(key)
|
172
189
|
mutex = @mutexes[idx]
|
173
190
|
|
@@ -180,7 +197,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
180
197
|
# The block is executed to generate the value if it doesn't exist
|
181
198
|
# Optionally accepts an expiration time
|
182
199
|
# If force is true, it always fetches and writes the value
|
183
|
-
def fetch(key, expires_in: nil, force: false)
|
200
|
+
def fetch(key, expires_in: nil, force: false, namespace: nil)
|
201
|
+
key = namespaced_key(key, namespace)
|
184
202
|
unless force
|
185
203
|
cached = read(key)
|
186
204
|
return cached if cached
|
@@ -194,22 +212,23 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
194
212
|
# Clears a specific key from the cache, a semantic synonym for delete
|
195
213
|
# This method is provided for clarity in usage
|
196
214
|
# It behaves the same as delete
|
197
|
-
def clear(key)
|
198
|
-
delete(key)
|
215
|
+
def clear(key, namespace: nil)
|
216
|
+
delete(key, namespace: namespace)
|
199
217
|
end
|
200
218
|
|
201
219
|
# Replaces the value for a key if it exists, otherwise does nothing
|
202
220
|
# This is useful for updating values without needing to check existence first
|
203
221
|
# It will write the new value and update the expiration if provided
|
204
222
|
# If the key does not exist, it will not create a new entry
|
205
|
-
def replace(key, value, expires_in: nil)
|
206
|
-
return unless exists?(key)
|
223
|
+
def replace(key, value, expires_in: nil, namespace: nil)
|
224
|
+
return unless exists?(key, namespace: namespace)
|
207
225
|
|
208
|
-
write(key, value, expires_in: expires_in)
|
226
|
+
write(key, value, expires_in: expires_in, namespace: namespace)
|
209
227
|
end
|
210
228
|
|
211
229
|
# Inspects a key and returns all meta data for it
|
212
|
-
def inspect(key) # rubocop:disable Metrics/MethodLength
|
230
|
+
def inspect(key, namespace: nil) # rubocop:disable Metrics/MethodLength
|
231
|
+
key = namespaced_key(key, namespace)
|
213
232
|
idx = bucket_index(key)
|
214
233
|
store = @stores[idx]
|
215
234
|
mutex = @mutexes[idx]
|
@@ -264,6 +283,15 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
264
283
|
@max_bytes
|
265
284
|
end
|
266
285
|
|
286
|
+
# Executes a block with a specific namespace, restoring the old namespace afterwards
|
287
|
+
def with_namespace(namespace)
|
288
|
+
old_ns = Thread.current[:mudis_namespace]
|
289
|
+
Thread.current[:mudis_namespace] = namespace
|
290
|
+
yield
|
291
|
+
ensure
|
292
|
+
Thread.current[:mudis_namespace] = old_ns
|
293
|
+
end
|
294
|
+
|
267
295
|
private
|
268
296
|
|
269
297
|
# Decompresses and deserializes a raw value
|
@@ -322,5 +350,11 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
322
350
|
@lru_tails[idx] = node.prev
|
323
351
|
end
|
324
352
|
end
|
353
|
+
|
354
|
+
# Namespaces a key with an optional namespace
|
355
|
+
def namespaced_key(key, namespace = nil)
|
356
|
+
ns = namespace || Thread.current[:mudis_namespace]
|
357
|
+
ns ? "#{ns}:#{key}" : key
|
358
|
+
end
|
325
359
|
end
|
326
360
|
end
|
data/spec/mudis_spec.rb
CHANGED
@@ -12,7 +12,7 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
12
12
|
Mudis.instance_variable_set(:@lru_tails, Array.new(Mudis.buckets) { nil })
|
13
13
|
Mudis.instance_variable_set(:@lru_nodes, Array.new(Mudis.buckets) { {} })
|
14
14
|
Mudis.instance_variable_set(:@current_bytes, Array.new(Mudis.buckets, 0))
|
15
|
-
Mudis.instance_variable_set(:@metrics, { hits: 0, misses: 0, evictions: 0 })
|
15
|
+
Mudis.instance_variable_set(:@metrics, { hits: 0, misses: 0, evictions: 0, rejected: 0 })
|
16
16
|
Mudis.serializer = JSON
|
17
17
|
Mudis.compress = false
|
18
18
|
Mudis.max_value_bytes = nil
|
@@ -114,6 +114,24 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
117
|
+
describe "namespacing" do
|
118
|
+
it "uses thread-local namespace in block" do
|
119
|
+
Mudis.with_namespace("test") do
|
120
|
+
Mudis.write("foo", "bar")
|
121
|
+
end
|
122
|
+
expect(Mudis.read("foo", namespace: "test")).to eq("bar")
|
123
|
+
expect(Mudis.read("foo")).to be_nil
|
124
|
+
end
|
125
|
+
|
126
|
+
it "supports explicit namespace override" do
|
127
|
+
Mudis.write("x", 1, namespace: "alpha")
|
128
|
+
Mudis.write("x", 2, namespace: "beta")
|
129
|
+
expect(Mudis.read("x", namespace: "alpha")).to eq(1)
|
130
|
+
expect(Mudis.read("x", namespace: "beta")).to eq(2)
|
131
|
+
expect(Mudis.read("x")).to be_nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
117
135
|
describe "expiry handling" do
|
118
136
|
it "expires values after specified time" do
|
119
137
|
Mudis.write("short_lived", "gone soon", expires_in: 1)
|
@@ -133,6 +151,35 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
133
151
|
end
|
134
152
|
end
|
135
153
|
|
154
|
+
describe "memory guards" do
|
155
|
+
before do
|
156
|
+
Mudis.stop_expiry_thread
|
157
|
+
Mudis.instance_variable_set(:@buckets, 1)
|
158
|
+
Mudis.instance_variable_set(:@stores, [{}])
|
159
|
+
Mudis.instance_variable_set(:@mutexes, [Mutex.new])
|
160
|
+
Mudis.instance_variable_set(:@lru_heads, [nil])
|
161
|
+
Mudis.instance_variable_set(:@lru_tails, [nil])
|
162
|
+
Mudis.instance_variable_set(:@lru_nodes, [{}])
|
163
|
+
Mudis.instance_variable_set(:@current_bytes, [0])
|
164
|
+
|
165
|
+
Mudis.max_value_bytes = nil
|
166
|
+
Mudis.instance_variable_set(:@threshold_bytes, 1_000_000) # optional
|
167
|
+
Mudis.hard_memory_limit = true
|
168
|
+
Mudis.instance_variable_set(:@max_bytes, 100) # artificially low
|
169
|
+
end
|
170
|
+
|
171
|
+
it "rejects writes that exceed max memory" do
|
172
|
+
big_value = "a" * 90
|
173
|
+
Mudis.write("a", big_value)
|
174
|
+
expect(Mudis.read("a")).to eq(big_value)
|
175
|
+
|
176
|
+
big_value_2 = "b" * 90 # rubocop:disable Naming/VariableNumber
|
177
|
+
Mudis.write("b", big_value_2)
|
178
|
+
expect(Mudis.read("b")).to be_nil
|
179
|
+
expect(Mudis.metrics[:rejected]).to be > 0
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
136
183
|
describe "LRU eviction" do
|
137
184
|
it "evicts old entries when size limit is reached" do
|
138
185
|
Mudis.stop_expiry_thread
|
@@ -145,7 +192,7 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
145
192
|
Mudis.instance_variable_set(:@lru_tails, [nil])
|
146
193
|
Mudis.instance_variable_set(:@lru_nodes, [{}])
|
147
194
|
Mudis.instance_variable_set(:@current_bytes, [0])
|
148
|
-
|
195
|
+
Mudis.hard_memory_limit = false
|
149
196
|
# Set very small threshold
|
150
197
|
Mudis.instance_variable_set(:@threshold_bytes, 60)
|
151
198
|
Mudis.max_value_bytes = 100
|
@@ -169,6 +216,11 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
169
216
|
end
|
170
217
|
end
|
171
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
|
+
|
172
224
|
describe ".current_memory_bytes" do
|
173
225
|
it "returns a non-zero byte count after writes" do
|
174
226
|
Mudis.write("size_test", "a" * 100)
|