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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05a589eca123a045badb19d25119bbc9e51c48e6afec22c490615f656c73f65f
4
- data.tar.gz: 478553d02b5c62f08d9194355136b0e284c257d47184ba74d79c2d9a26553460
3
+ metadata.gz: bdaf73fb205686d551ea90a70ffe82e93287c9fcb2e9209e25e626f200c84768
4
+ data.tar.gz: 778faad72b00b0f2b477df2ec53e107aba064d2cfe4e0a8b53a8d4623521d1ce
5
5
  SHA512:
6
- metadata.gz: bbae5238c153bfc4d29e0efd795c01e6c473fd17a69b28e145dc4fe8ada0faa286fce117cb53a0eaba0df8c67560c08814eb15305fe5bf01e405751b56a6f218
7
- data.tar.gz: 1807351fac67cc35c66dc898264807b9542871c2e708baaa2e3fc91bd28b9cda1913257a9083e3115da341b836bd7ce3ca15ef23aae8cf998da4fbc65e6f0e6d
6
+ metadata.gz: e860b5e07f84e20481463eb4bf36984e094b2f95c70d1be4c072318b7e089858b7355ca8e44476495354676ce403b4e4e5cf5e796376c1d155bb47bc60e9ea21
7
+ data.tar.gz: 902113917b4b1a376d850a6a9d2d54994bebdf52c4834680524a43d9407f71343899a07c2b206080d9de9e522d3538b3288e74f065152e3458ee4617fc8eb293
data/README.md CHANGED
@@ -1,12 +1,46 @@
1
1
  ![mudis_signet](design/mudis.png "Mudis")
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/mudis.svg)](https://rubygems.org/gems/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
 
@@ -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 (TODO: add more useful abstraction once DSL and namespacing is introduced):
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
- def initialize(cache_key)
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:#{current_user.id}")
134
- cache.write({ preferences: "dark" }, expires_in: 3600)
135
- cache.read # => { "preferences" => "dark" }
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
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- MUDIS_VERSION = "0.2.0"
3
+ MUDIS_VERSION = "0.3.1"
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 behavior
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)
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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kiebor81