mudis 0.5.0 → 0.6.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: 6207a8171eb9fd9723889d90a5a5e696dafc9a894c29d9b6f99340b7c02ec697
4
- data.tar.gz: a18de4b21e8b4116c321393d0ada12da91c06682ff099907be84b6ac78c6f999
3
+ metadata.gz: aeea820874569fe79af99d53f2a3ae3a7a9b74dc8a2d9dccbfbaa3f4ebab3967
4
+ data.tar.gz: 211d8c786790478e861461aab7444bd4bd1342afa07039ef3608f66dbd401cd6
5
5
  SHA512:
6
- metadata.gz: e44da4c093e85012cee92cc61be4062c48fe94fe46b22210c1433e47b8eef91755359a658f936075ee514a5adcbf5ea2dae184031042532d13907d619ab77876
7
- data.tar.gz: 79c1703d2d03938d801d752c7cc3c7c98ed777933e85b091ece31180d1dd2b9eaf0af8e8b8bd30221750cd89aa5cfb2989e8914aa4ef09fe6d1316a3ee678024
6
+ metadata.gz: b93779c0990a328a3d24f4741b9934833e5524e099e2ddcdd2ae38f297e599f2cd49bd4fbc43d3cd1d1f81d2655220760b2573138ade72b289b785fc718c9bcd
7
+ data.tar.gz: bf25460f6237402793327d4bb04aef416d39f240e80350b8428433aa4245036cac0582f75e25b79c6902d21751403be9def2df01b9ebe54536ee39d229571590
data/README.md CHANGED
@@ -190,6 +190,28 @@ Mudis.least_touched(5)
190
190
  # => returns top 5 least accessed keys
191
191
  ```
192
192
 
193
+ #### `Mudis.keys(namespace:)`
194
+
195
+ Returns all keys for a given namespace.
196
+
197
+ ```ruby
198
+ Mudis.write("u1", "alpha", namespace: "users")
199
+ Mudis.write("u2", "beta", namespace: "users")
200
+
201
+ Mudis.keys(namespace: "users")
202
+ # => ["u1", "u2"]
203
+
204
+ ```
205
+
206
+ #### `Mudis.clear_namespace(namespace:)`
207
+
208
+ Deletes all keys within a namespace.
209
+
210
+ ```ruby
211
+ Mudis.clear_namespace("users")
212
+ Mudis.read("u1", namespace: "users") # => nil
213
+ ```
214
+
193
215
  ---
194
216
 
195
217
  ## Rails Service Integration
@@ -536,7 +558,7 @@ Mudis is not intended to be a general-purpose, distributed caching platform. You
536
558
 
537
559
  #### API Enhancements
538
560
 
539
- - [ ] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
561
+ - [x] bulk_read(keys, namespace:): Batch retrieval of multiple keys with a single method call
540
562
 
541
563
  #### Safety & Policy Controls
542
564
 
@@ -545,7 +567,7 @@ Mudis is not intended to be a general-purpose, distributed caching platform. You
545
567
 
546
568
  #### Debugging
547
569
 
548
- - [ ] clear_namespace(namespace): Remove all keys in a namespace in one call
570
+ - [x] clear_namespace(namespace): Remove all keys in a namespace in one call
549
571
 
550
572
  ---
551
573
 
data/lib/mudis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- MUDIS_VERSION = "0.5.0"
3
+ MUDIS_VERSION = "0.6.0"
data/lib/mudis.rb CHANGED
@@ -383,6 +383,30 @@ class Mudis # rubocop:disable Metrics/ClassLength
383
383
  keys
384
384
  end
385
385
 
386
+ # Returns all keys in a specific namespace
387
+ def keys(namespace:)
388
+ raise ArgumentError, "namespace is required" unless namespace
389
+
390
+ prefix = "#{namespace}:"
391
+ all_keys.select { |key| key.start_with?(prefix) }.map { |key| key.delete_prefix(prefix) }
392
+ end
393
+
394
+ # Clears all keys in a specific namespace
395
+ def clear_namespace(namespace:)
396
+ raise ArgumentError, "namespace is required" unless namespace
397
+
398
+ prefix = "#{namespace}:"
399
+ buckets.times do |idx|
400
+ mutex = @mutexes[idx]
401
+ store = @stores[idx]
402
+
403
+ mutex.synchronize do
404
+ keys_to_delete = store.keys.select { |key| key.start_with?(prefix) }
405
+ keys_to_delete.each { |key| evict_key(idx, key) }
406
+ end
407
+ end
408
+ end
409
+
386
410
  # Returns the least-touched keys across all buckets
387
411
  def least_touched(n = 10) # rubocop:disable Metrics/MethodLength,Naming/MethodParameterName
388
412
  keys_with_touches = []
data/sig/mudis.rbs CHANGED
@@ -39,6 +39,8 @@ class Mudis
39
39
  def self.clear: (String, ?namespace: String) -> void
40
40
  def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
41
41
  def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
42
+ def self.keys: (?namespace: String) -> Array[String]
43
+ def self.clear_namespace: (?namespace: String) -> void
42
44
 
43
45
  # Introspection & management
44
46
  def self.metrics: () -> Hash[Symbol, untyped]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis LRU Eviction" do
6
+ before do
7
+ Mudis.reset!
8
+ Mudis.stop_expiry_thread
9
+
10
+ Mudis.instance_variable_set(:@buckets, 1)
11
+ Mudis.instance_variable_set(:@stores, [{}])
12
+ Mudis.instance_variable_set(:@mutexes, [Mutex.new])
13
+ Mudis.instance_variable_set(:@lru_heads, [nil])
14
+ Mudis.instance_variable_set(:@lru_tails, [nil])
15
+ Mudis.instance_variable_set(:@lru_nodes, [{}])
16
+ Mudis.instance_variable_set(:@current_bytes, [0])
17
+ Mudis.hard_memory_limit = false
18
+ Mudis.instance_variable_set(:@threshold_bytes, 60)
19
+ Mudis.max_value_bytes = 100
20
+ end
21
+
22
+ it "evicts old entries when size limit is reached" do
23
+ Mudis.write("a", "a" * 50)
24
+ Mudis.write("b", "b" * 50)
25
+
26
+ expect(Mudis.read("a")).to be_nil
27
+ expect(Mudis.read("b")).not_to be_nil
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis Memory Guardrails" do
6
+ before do
7
+ Mudis.reset!
8
+ Mudis.stop_expiry_thread
9
+ Mudis.instance_variable_set(:@buckets, 1)
10
+ Mudis.instance_variable_set(:@stores, [{}])
11
+ Mudis.instance_variable_set(:@mutexes, [Mutex.new])
12
+ Mudis.instance_variable_set(:@lru_heads, [nil])
13
+ Mudis.instance_variable_set(:@lru_tails, [nil])
14
+ Mudis.instance_variable_set(:@lru_nodes, [{}])
15
+ Mudis.instance_variable_set(:@current_bytes, [0])
16
+
17
+ Mudis.max_value_bytes = nil
18
+ Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
19
+ Mudis.hard_memory_limit = true
20
+ Mudis.instance_variable_set(:@max_bytes, 100)
21
+ end
22
+
23
+ it "rejects writes that exceed max memory" do
24
+ big_value = "a" * 90
25
+ Mudis.write("a", big_value)
26
+ expect(Mudis.read("a")).to eq(big_value)
27
+
28
+ big_value2 = "b" * 90
29
+ Mudis.write("b", big_value2)
30
+ expect(Mudis.read("b")).to be_nil
31
+ expect(Mudis.metrics[:rejected]).to be > 0
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
6
+ it "tracks hits and misses" do
7
+ Mudis.write("hit_me", "value")
8
+ Mudis.read("hit_me")
9
+ Mudis.read("miss_me")
10
+ metrics = Mudis.metrics
11
+ expect(metrics[:hits]).to eq(1)
12
+ expect(metrics[:misses]).to eq(1)
13
+ end
14
+
15
+ it "includes per-bucket stats" do
16
+ Mudis.write("a", "x" * 50)
17
+ metrics = Mudis.metrics
18
+ expect(metrics).to include(:buckets)
19
+ expect(metrics[:buckets]).to be_an(Array)
20
+ expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
21
+ end
22
+
23
+ it "resets only the metrics without clearing cache" do
24
+ Mudis.write("metrics_key", "value")
25
+ Mudis.read("metrics_key")
26
+ Mudis.read("missing_key")
27
+ expect(Mudis.metrics[:hits]).to eq(1)
28
+ expect(Mudis.metrics[:misses]).to eq(1)
29
+ Mudis.reset_metrics!
30
+ expect(Mudis.metrics[:hits]).to eq(0)
31
+ expect(Mudis.metrics[:misses]).to eq(0)
32
+ expect(Mudis.read("metrics_key")).to eq("value")
33
+ end
34
+ end
data/spec/mudis_spec.rb CHANGED
@@ -114,24 +114,6 @@ 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
-
135
117
  describe "expiry handling" do
136
118
  it "expires values after specified time" do
137
119
  Mudis.write("short_lived", "gone soon", expires_in: 1)
@@ -140,74 +122,6 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
140
122
  end
141
123
  end
142
124
 
143
- describe ".metrics" do
144
- it "tracks hits and misses" do
145
- Mudis.write("hit_me", "value")
146
- Mudis.read("hit_me") # hit
147
- Mudis.read("miss_me") # miss
148
- metrics = Mudis.metrics
149
- expect(metrics[:hits]).to eq(1)
150
- expect(metrics[:misses]).to eq(1)
151
- end
152
- end
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
-
183
- describe "LRU eviction" do
184
- it "evicts old entries when size limit is reached" do
185
- Mudis.stop_expiry_thread
186
-
187
- # Force one bucket
188
- Mudis.instance_variable_set(:@buckets, 1)
189
- Mudis.instance_variable_set(:@stores, [{}])
190
- Mudis.instance_variable_set(:@mutexes, [Mutex.new])
191
- Mudis.instance_variable_set(:@lru_heads, [nil])
192
- Mudis.instance_variable_set(:@lru_tails, [nil])
193
- Mudis.instance_variable_set(:@lru_nodes, [{}])
194
- Mudis.instance_variable_set(:@current_bytes, [0])
195
- Mudis.hard_memory_limit = false
196
- # Set very small threshold
197
- Mudis.instance_variable_set(:@threshold_bytes, 60)
198
- Mudis.max_value_bytes = 100
199
-
200
- big_val1 = "a" * 50
201
- big_val2 = "b" * 50
202
-
203
- Mudis.write("a", big_val1)
204
- Mudis.write("b", big_val2)
205
-
206
- expect(Mudis.read("a")).to be_nil
207
- expect(Mudis.read("b")).not_to be_nil
208
- end
209
- end
210
-
211
125
  describe ".all_keys" do
212
126
  it "lists all stored keys" do
213
127
  Mudis.write("k1", 1)
@@ -223,22 +137,16 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
223
137
 
224
138
  describe ".current_memory_bytes" do
225
139
  it "returns a non-zero byte count after writes" do
140
+ Mudis.configure do |c|
141
+ c.max_value_bytes = nil
142
+ c.hard_memory_limit = false
143
+ end
144
+
226
145
  Mudis.write("size_test", "a" * 100)
227
146
  expect(Mudis.current_memory_bytes).to be > 0
228
147
  end
229
148
  end
230
149
 
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
150
  describe ".least_touched" do
243
151
  it "returns keys with lowest read access counts" do
244
152
  Mudis.reset!
@@ -272,43 +180,4 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
272
180
  expect(Mudis.max_bytes).to eq(987_654)
273
181
  end
274
182
  end
275
-
276
- describe ".reset!" do
277
- it "clears all stores, memory, and metrics" do
278
- Mudis.write("reset_key", "value")
279
- expect(Mudis.read("reset_key")).to eq("value")
280
- expect(Mudis.current_memory_bytes).to be > 0
281
- expect(Mudis.metrics[:hits]).to be >= 0
282
-
283
- Mudis.reset!
284
-
285
- metrics = Mudis.metrics
286
- expect(metrics[:hits]).to eq(0)
287
- expect(metrics[:misses]).to eq(0)
288
- expect(metrics[:evictions]).to eq(0)
289
- expect(metrics[:rejected]).to eq(0)
290
- expect(Mudis.current_memory_bytes).to eq(0)
291
- expect(Mudis.all_keys).to be_empty
292
-
293
- # Optionally confirm reset_key is now gone
294
- expect(Mudis.read("reset_key")).to be_nil
295
- end
296
- end
297
-
298
- describe ".reset_metrics!" do
299
- it "resets only the metrics without clearing cache" do
300
- Mudis.write("metrics_key", "value")
301
- Mudis.read("metrics_key") # generates :hits
302
- Mudis.read("missing_key") # generates :misses
303
-
304
- expect(Mudis.metrics[:hits]).to eq(1)
305
- expect(Mudis.metrics[:misses]).to eq(1)
306
-
307
- Mudis.reset_metrics!
308
-
309
- expect(Mudis.metrics[:hits]).to eq(0)
310
- expect(Mudis.metrics[:misses]).to eq(0)
311
- expect(Mudis.read("metrics_key")).to eq("value") # still exists
312
- end
313
- end
314
183
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis Namespace Operations" do # rubocop:disable Metrics/BlockLength
6
+ before(:each) do
7
+ Mudis.reset!
8
+ end
9
+
10
+ it "uses thread-local namespace in block" do
11
+ Mudis.with_namespace("test") do
12
+ Mudis.write("foo", "bar")
13
+ end
14
+ expect(Mudis.read("foo", namespace: "test")).to eq("bar")
15
+ expect(Mudis.read("foo")).to be_nil
16
+ end
17
+
18
+ it "supports explicit namespace override" do
19
+ Mudis.write("x", 1, namespace: "alpha")
20
+ Mudis.write("x", 2, namespace: "beta")
21
+ expect(Mudis.read("x", namespace: "alpha")).to eq(1)
22
+ expect(Mudis.read("x", namespace: "beta")).to eq(2)
23
+ expect(Mudis.read("x")).to be_nil
24
+ end
25
+
26
+ describe ".keys" do
27
+ it "returns only keys for the given namespace" do
28
+ Mudis.write("user:1", "Alice", namespace: "users")
29
+ Mudis.write("user:2", "Bob", namespace: "users")
30
+ Mudis.write("admin:1", "Charlie", namespace: "admins")
31
+
32
+ result = Mudis.keys(namespace: "users")
33
+ expect(result).to contain_exactly("user:1", "user:2")
34
+ end
35
+
36
+ it "returns an empty array if no keys exist for namespace" do
37
+ expect(Mudis.keys(namespace: "nonexistent")).to eq([])
38
+ end
39
+
40
+ it "raises an error if namespace is missing" do
41
+ expect { Mudis.keys(namespace: nil) }.to raise_error(ArgumentError, /namespace is required/)
42
+ end
43
+ end
44
+
45
+ describe ".clear_namespace" do
46
+ it "deletes all keys in the given namespace" do
47
+ Mudis.write("a", 1, namespace: "ns1")
48
+ Mudis.write("b", 2, namespace: "ns1")
49
+ Mudis.write("x", 9, namespace: "ns2")
50
+
51
+ expect(Mudis.read("a", namespace: "ns1")).to eq(1)
52
+ expect(Mudis.read("b", namespace: "ns1")).to eq(2)
53
+
54
+ Mudis.clear_namespace(namespace: "ns1")
55
+
56
+ expect(Mudis.read("a", namespace: "ns1")).to be_nil
57
+ expect(Mudis.read("b", namespace: "ns1")).to be_nil
58
+ expect(Mudis.read("x", namespace: "ns2")).to eq(9)
59
+ end
60
+
61
+ it "does nothing if namespace has no keys" do
62
+ expect { Mudis.clear_namespace(namespace: "ghost") }.not_to raise_error
63
+ end
64
+
65
+ it "raises an error if namespace is nil" do
66
+ expect { Mudis.clear_namespace(namespace: nil) }.to raise_error(ArgumentError, /namespace is required/)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis Reset Features" do
6
+ before { Mudis.reset! }
7
+
8
+ describe ".reset!" do
9
+ it "clears all stores, memory, and metrics" do
10
+ Mudis.write("reset_key", "value")
11
+ expect(Mudis.read("reset_key")).to eq("value")
12
+ Mudis.reset!
13
+ expect(Mudis.metrics[:hits]).to eq(0)
14
+ expect(Mudis.all_keys).to be_empty
15
+ expect(Mudis.read("reset_key")).to be_nil
16
+ end
17
+ end
18
+
19
+ describe ".reset_metrics!" do
20
+ it "resets only the metrics without clearing cache" do
21
+ Mudis.write("metrics_key", "value")
22
+ Mudis.read("metrics_key")
23
+ Mudis.read("missing_key")
24
+ expect(Mudis.metrics[:hits]).to eq(1)
25
+ expect(Mudis.metrics[:misses]).to eq(1)
26
+ Mudis.reset_metrics!
27
+ expect(Mudis.metrics[:hits]).to eq(0)
28
+ expect(Mudis.read("metrics_key")).to eq("value")
29
+ end
30
+ end
31
+ 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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kiebor81
@@ -54,8 +54,13 @@ files:
54
54
  - lib/mudis_config.rb
55
55
  - sig/mudis.rbs
56
56
  - sig/mudis_config.rbs
57
+ - spec/eviction_spec.rb
57
58
  - spec/guardrails_spec.rb
59
+ - spec/memory_guard_spec.rb
60
+ - spec/metrics_spec.rb
58
61
  - spec/mudis_spec.rb
62
+ - spec/namespace_spec.rb
63
+ - spec/reset_spec.rb
59
64
  homepage: https://github.com/kiebor81/mudis
60
65
  licenses:
61
66
  - MIT
@@ -81,5 +86,10 @@ specification_version: 4
81
86
  summary: A fast in-memory, thread-safe and high performance Ruby LRU cache with compression
82
87
  and auto-expiry.
83
88
  test_files:
89
+ - spec/eviction_spec.rb
84
90
  - spec/guardrails_spec.rb
91
+ - spec/memory_guard_spec.rb
92
+ - spec/metrics_spec.rb
85
93
  - spec/mudis_spec.rb
94
+ - spec/namespace_spec.rb
95
+ - spec/reset_spec.rb