mudis 0.9.0 → 0.9.4
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 +114 -16
- data/lib/mudis/bound.rb +128 -0
- data/lib/mudis/metrics.rb +21 -2
- data/lib/mudis/persistence.rb +3 -1
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +119 -26
- data/lib/mudis_client.rb +69 -31
- data/lib/mudis_config.rb +2 -0
- data/lib/mudis_ipc_config.rb +10 -0
- data/lib/mudis_server.rb +14 -9
- data/sig/mudis.rbs +19 -4
- data/sig/mudis_bound.rbs +25 -0
- data/sig/mudis_client.rbs +15 -5
- data/sig/mudis_config.rbs +5 -0
- data/sig/mudis_expiry.rbs +1 -1
- data/sig/mudis_ipc_config.rbs +8 -0
- data/sig/mudis_lru.rbs +1 -1
- data/sig/mudis_metrics.rbs +1 -1
- data/sig/mudis_persistence.rbs +4 -4
- data/sig/mudis_server.rbs +3 -3
- data/spec/api_compatibility_spec.rb +3 -0
- data/spec/bound_spec.rb +89 -0
- data/spec/guardrails_spec.rb +14 -0
- data/spec/memory_guard_spec.rb +9 -0
- data/spec/metrics_spec.rb +21 -0
- data/spec/modules/metrics_spec.rb +4 -1
- data/spec/modules/persistence_spec.rb +24 -26
- data/spec/mudis_client_spec.rb +84 -10
- data/spec/mudis_server_spec.rb +57 -16
- data/spec/mudis_spec.rb +37 -0
- data/spec/namespace_spec.rb +23 -0
- metadata +8 -3
data/lib/mudis.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "mudis/persistence"
|
|
|
10
10
|
require_relative "mudis/metrics"
|
|
11
11
|
require_relative "mudis/namespace"
|
|
12
12
|
require_relative "mudis/expiry"
|
|
13
|
+
require_relative "mudis/bound"
|
|
13
14
|
|
|
14
15
|
# Mudis is a thread-safe, in-memory, sharded, LRU cache with optional compression and expiry.
|
|
15
16
|
# It is designed for high concurrency and performance within a Ruby application.
|
|
@@ -26,6 +27,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
26
27
|
@compress = false # Whether to compress values with Zlib
|
|
27
28
|
@metrics = { hits: 0, misses: 0, evictions: 0, rejected: 0 } # Metrics tracking read/write behaviour
|
|
28
29
|
@metrics_mutex = Mutex.new # Mutex for synchronizing access to metrics
|
|
30
|
+
@metrics_by_namespace = {} # Per-namespace metrics
|
|
31
|
+
@metrics_by_namespace_mutex = Mutex.new # Mutex for synchronizing namespace metrics
|
|
29
32
|
@max_value_bytes = nil # Optional size cap per value
|
|
30
33
|
@stop_expiry = false # Signal for stopping expiry thread
|
|
31
34
|
@max_ttl = nil # Optional maximum TTL for cache entries
|
|
@@ -34,13 +37,20 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
34
37
|
# --- Configuration Management ---
|
|
35
38
|
|
|
36
39
|
class << self
|
|
37
|
-
attr_accessor :serializer, :compress, :hard_memory_limit, :max_ttl, :default_ttl
|
|
40
|
+
attr_accessor :serializer, :compress, :hard_memory_limit, :max_ttl, :default_ttl, :eviction_threshold
|
|
38
41
|
attr_reader :max_bytes, :max_value_bytes
|
|
39
42
|
|
|
40
43
|
# Configures Mudis with a block, allowing customization of settings
|
|
41
44
|
def configure
|
|
42
|
-
|
|
45
|
+
new_config = config.dup
|
|
46
|
+
yield(new_config)
|
|
47
|
+
|
|
48
|
+
old_config = @config
|
|
49
|
+
@config = new_config
|
|
43
50
|
apply_config!
|
|
51
|
+
rescue StandardError
|
|
52
|
+
@config = old_config
|
|
53
|
+
raise
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
# Returns the current configuration object
|
|
@@ -48,6 +58,15 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
48
58
|
@config ||= MudisConfig.new
|
|
49
59
|
end
|
|
50
60
|
|
|
61
|
+
def bind(namespace:, default_ttl: nil, max_ttl: nil, max_value_bytes: nil)
|
|
62
|
+
Bound.new(
|
|
63
|
+
namespace: namespace,
|
|
64
|
+
default_ttl: default_ttl,
|
|
65
|
+
max_ttl: max_ttl,
|
|
66
|
+
max_value_bytes: max_value_bytes
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
51
70
|
# Applies the current configuration to Mudis
|
|
52
71
|
def apply_config! # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
53
72
|
validate_config!
|
|
@@ -57,6 +76,7 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
57
76
|
self.max_value_bytes = config.max_value_bytes
|
|
58
77
|
self.hard_memory_limit = config.hard_memory_limit
|
|
59
78
|
self.max_bytes = config.max_bytes
|
|
79
|
+
self.eviction_threshold = config.eviction_threshold
|
|
60
80
|
self.max_ttl = config.max_ttl
|
|
61
81
|
self.default_ttl = config.default_ttl
|
|
62
82
|
|
|
@@ -85,6 +105,9 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
85
105
|
raise ArgumentError, "max_value_bytes must be > 0" if config.max_value_bytes && config.max_value_bytes <= 0
|
|
86
106
|
|
|
87
107
|
raise ArgumentError, "buckets must be > 0" if config.buckets && config.buckets <= 0
|
|
108
|
+
if config.eviction_threshold && (config.eviction_threshold <= 0 || config.eviction_threshold > 1)
|
|
109
|
+
raise ArgumentError, "eviction_threshold must be > 0 and <= 1"
|
|
110
|
+
end
|
|
88
111
|
raise ArgumentError, "max_ttl must be > 0" if config.max_ttl && config.max_ttl <= 0
|
|
89
112
|
raise ArgumentError, "default_ttl must be > 0" if config.default_ttl && config.default_ttl <= 0
|
|
90
113
|
end
|
|
@@ -102,6 +125,7 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
102
125
|
@lru_tails = Array.new(b) { nil }
|
|
103
126
|
@lru_nodes = Array.new(b) { {} }
|
|
104
127
|
@current_bytes = Array.new(b, 0)
|
|
128
|
+
@inflight_mutexes = {}
|
|
105
129
|
|
|
106
130
|
reset_metrics!
|
|
107
131
|
end
|
|
@@ -111,7 +135,16 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
111
135
|
raise ArgumentError, "max_bytes must be > 0" if value.to_i <= 0
|
|
112
136
|
|
|
113
137
|
@max_bytes = value
|
|
114
|
-
|
|
138
|
+
threshold = @eviction_threshold || 0.9
|
|
139
|
+
@threshold_bytes = (@max_bytes * threshold).to_i
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def eviction_threshold=(value)
|
|
143
|
+
return @eviction_threshold = nil if value.nil?
|
|
144
|
+
raise ArgumentError, "eviction_threshold must be > 0 and <= 1" if value <= 0 || value > 1
|
|
145
|
+
|
|
146
|
+
@eviction_threshold = value
|
|
147
|
+
@threshold_bytes = (@max_bytes * @eviction_threshold).to_i
|
|
115
148
|
end
|
|
116
149
|
|
|
117
150
|
# Sets the maximum size for a single value in bytes, raising an error if invalid
|
|
@@ -141,9 +174,12 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
141
174
|
@lru_nodes = Array.new(buckets) { {} } # Map of key => LRU node
|
|
142
175
|
@current_bytes = Array.new(buckets, 0) # Memory usage per bucket
|
|
143
176
|
@max_bytes = 1_073_741_824 # 1 GB global max cache size
|
|
144
|
-
@
|
|
177
|
+
@eviction_threshold = 0.9
|
|
178
|
+
@threshold_bytes = (@max_bytes * @eviction_threshold).to_i # Eviction threshold at 90%
|
|
145
179
|
@expiry_thread = nil # Background thread for expiry cleanup
|
|
146
180
|
@hard_memory_limit = false # Whether to enforce hard memory cap
|
|
181
|
+
@inflight_mutexes_lock = Mutex.new
|
|
182
|
+
@inflight_mutexes = {}
|
|
147
183
|
|
|
148
184
|
# --- Core Cache Operations ---
|
|
149
185
|
|
|
@@ -155,13 +191,13 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
155
191
|
|
|
156
192
|
# Checks if a key exists and is not expired
|
|
157
193
|
def exists?(key, namespace: nil)
|
|
158
|
-
|
|
159
|
-
!!read(key)
|
|
194
|
+
!!read(key, namespace: namespace)
|
|
160
195
|
end
|
|
161
196
|
|
|
162
197
|
# Reads and returns the value for a key, updating LRU and metrics
|
|
163
198
|
def read(key, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
164
199
|
key = namespaced_key(key, namespace)
|
|
200
|
+
ns = namespace || Thread.current[:mudis_namespace]
|
|
165
201
|
raw_entry = nil
|
|
166
202
|
idx = bucket_index(key)
|
|
167
203
|
mutex = @mutexes[idx]
|
|
@@ -174,29 +210,31 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
174
210
|
raw_entry = nil
|
|
175
211
|
end
|
|
176
212
|
|
|
177
|
-
|
|
213
|
+
if store[key]
|
|
214
|
+
store[key][:touches] = (store[key][:touches] || 0) + 1
|
|
215
|
+
promote_lru(idx, key)
|
|
216
|
+
end
|
|
178
217
|
|
|
179
|
-
metric(:hits) if raw_entry
|
|
180
|
-
metric(:misses) unless raw_entry
|
|
218
|
+
metric(:hits, namespace: ns) if raw_entry
|
|
219
|
+
metric(:misses, namespace: ns) unless raw_entry
|
|
181
220
|
end
|
|
182
221
|
|
|
183
222
|
return nil unless raw_entry
|
|
184
223
|
|
|
185
|
-
|
|
186
|
-
promote_lru(idx, key)
|
|
187
|
-
value
|
|
224
|
+
decompress_and_deserialize(raw_entry[:value])
|
|
188
225
|
end
|
|
189
226
|
|
|
190
227
|
# Writes a value to the cache with optional expiry and LRU tracking
|
|
191
228
|
def write(key, value, expires_in: nil, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
192
229
|
key = namespaced_key(key, namespace)
|
|
230
|
+
ns = namespace || Thread.current[:mudis_namespace]
|
|
193
231
|
raw = serializer.dump(value)
|
|
194
232
|
raw = Zlib::Deflate.deflate(raw) if compress
|
|
195
233
|
size = key.bytesize + raw.bytesize
|
|
196
234
|
return if max_value_bytes && raw.bytesize > max_value_bytes
|
|
197
235
|
|
|
198
236
|
if hard_memory_limit && current_memory_bytes + size > max_memory_bytes
|
|
199
|
-
metric(:rejected)
|
|
237
|
+
metric(:rejected, namespace: ns)
|
|
200
238
|
return
|
|
201
239
|
end
|
|
202
240
|
|
|
@@ -211,15 +249,18 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
211
249
|
evict_key(idx, key) if store[key]
|
|
212
250
|
|
|
213
251
|
while @current_bytes[idx] + size > (@threshold_bytes / buckets) && @lru_tails[idx]
|
|
214
|
-
|
|
215
|
-
|
|
252
|
+
evict_key_name = @lru_tails[idx].key
|
|
253
|
+
evict_ns = store[evict_key_name] && store[evict_key_name][:namespace]
|
|
254
|
+
evict_key(idx, evict_key_name)
|
|
255
|
+
metric(:evictions, namespace: evict_ns)
|
|
216
256
|
end
|
|
217
257
|
|
|
218
258
|
store[key] = {
|
|
219
259
|
value: raw,
|
|
220
260
|
expires_at: expires_in ? Time.now + expires_in : nil,
|
|
221
261
|
created_at: Time.now,
|
|
222
|
-
touches: 0
|
|
262
|
+
touches: 0,
|
|
263
|
+
namespace: ns
|
|
223
264
|
}
|
|
224
265
|
|
|
225
266
|
insert_lru(idx, key)
|
|
@@ -244,11 +285,38 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
244
285
|
new_value = yield(value)
|
|
245
286
|
new_raw = serializer.dump(new_value)
|
|
246
287
|
new_raw = Zlib::Deflate.deflate(new_raw) if compress
|
|
288
|
+
return if max_value_bytes && new_raw.bytesize > max_value_bytes
|
|
247
289
|
|
|
248
290
|
mutex.synchronize do
|
|
249
|
-
|
|
291
|
+
current_entry = store[key]
|
|
292
|
+
return nil unless current_entry
|
|
293
|
+
|
|
294
|
+
old_size = key.bytesize + current_entry[:value].bytesize
|
|
250
295
|
new_size = key.bytesize + new_raw.bytesize
|
|
296
|
+
|
|
297
|
+
ns = current_entry[:namespace]
|
|
298
|
+
|
|
299
|
+
if hard_memory_limit && (current_memory_bytes - old_size + new_size) > max_memory_bytes
|
|
300
|
+
metric(:rejected, namespace: ns)
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
while (@current_bytes[idx] - old_size + new_size) > (@threshold_bytes / buckets) && @lru_tails[idx]
|
|
305
|
+
break if @lru_tails[idx].key == key
|
|
306
|
+
|
|
307
|
+
evict_key_name = @lru_tails[idx].key
|
|
308
|
+
evict_ns = store[evict_key_name] && store[evict_key_name][:namespace]
|
|
309
|
+
evict_key(idx, evict_key_name)
|
|
310
|
+
metric(:evictions, namespace: evict_ns)
|
|
311
|
+
end
|
|
312
|
+
|
|
251
313
|
store[key][:value] = new_raw
|
|
314
|
+
|
|
315
|
+
# Refresh TTL on update. If no TTL applies, keep existing expiry (possibly nil).
|
|
316
|
+
now = Time.now
|
|
317
|
+
ttl = current_entry[:expires_at] ? (current_entry[:expires_at] - current_entry[:created_at]) : nil
|
|
318
|
+
store[key][:created_at] = now
|
|
319
|
+
store[key][:expires_at] = ttl ? now + ttl : nil
|
|
252
320
|
@current_bytes[idx] += (new_size - old_size)
|
|
253
321
|
promote_lru(idx, key)
|
|
254
322
|
end
|
|
@@ -269,16 +337,13 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
269
337
|
# The block is executed to generate the value if it doesn't exist
|
|
270
338
|
# Optionally accepts an expiration time
|
|
271
339
|
# If force is true, it always fetches and writes the value
|
|
272
|
-
def fetch(key, expires_in: nil, force: false, namespace: nil)
|
|
273
|
-
|
|
274
|
-
unless force
|
|
275
|
-
cached = read(key)
|
|
276
|
-
return cached if cached
|
|
277
|
-
end
|
|
340
|
+
def fetch(key, expires_in: nil, force: false, namespace: nil, singleflight: false) # rubocop:disable Metrics/MethodLength
|
|
341
|
+
return fetch_without_lock(key, expires_in:, force:, namespace:) { yield } unless singleflight
|
|
278
342
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
343
|
+
lock_key = namespaced_key(key, namespace)
|
|
344
|
+
with_inflight_lock(lock_key) do
|
|
345
|
+
fetch_without_lock(key, expires_in:, force:, namespace:) { yield }
|
|
346
|
+
end
|
|
282
347
|
end
|
|
283
348
|
|
|
284
349
|
# Clears a specific key from the cache, a semantic synonym for delete
|
|
@@ -366,5 +431,33 @@ class Mudis # rubocop:disable Metrics/ClassLength
|
|
|
366
431
|
val = compress ? Zlib::Inflate.inflate(raw) : raw
|
|
367
432
|
serializer.load(val)
|
|
368
433
|
end
|
|
434
|
+
|
|
435
|
+
def fetch_without_lock(key, expires_in:, force:, namespace:)
|
|
436
|
+
unless force
|
|
437
|
+
cached = read(key, namespace: namespace)
|
|
438
|
+
return cached if cached
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
value = yield
|
|
442
|
+
write(key, value, expires_in: expires_in, namespace: namespace)
|
|
443
|
+
value
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def with_inflight_lock(lock_key)
|
|
447
|
+
entry = nil
|
|
448
|
+
@inflight_mutexes_lock.synchronize do
|
|
449
|
+
entry = (@inflight_mutexes[lock_key] ||= { mutex: Mutex.new, count: 0 })
|
|
450
|
+
entry[:count] += 1
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
entry[:mutex].synchronize { yield }
|
|
454
|
+
ensure
|
|
455
|
+
@inflight_mutexes_lock.synchronize do
|
|
456
|
+
next unless entry
|
|
457
|
+
|
|
458
|
+
entry[:count] -= 1
|
|
459
|
+
@inflight_mutexes.delete(lock_key) if entry[:count] <= 0
|
|
460
|
+
end
|
|
461
|
+
end
|
|
369
462
|
end
|
|
370
463
|
end
|
data/lib/mudis_client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
4
|
require "json"
|
|
5
|
+
require "timeout"
|
|
5
6
|
require_relative "mudis_ipc_config"
|
|
6
7
|
|
|
7
8
|
# Thread-safe client for communicating with the MudisServer
|
|
@@ -25,28 +26,41 @@ class MudisClient
|
|
|
25
26
|
# Send a request to the MudisServer and return the response
|
|
26
27
|
# @param payload [Hash] The request payload
|
|
27
28
|
# @return [Object] The response value from the server
|
|
28
|
-
def request(payload) # rubocop:disable Metrics/MethodLength
|
|
29
|
+
def request(payload) # rubocop:disable Metrics/MethodLength
|
|
29
30
|
@mutex.synchronize do
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
attempts = 0
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
attempts += 1
|
|
35
|
+
response = nil
|
|
36
|
+
|
|
37
|
+
Timeout.timeout(MudisIPCConfig.timeout) do
|
|
38
|
+
sock = open_connection
|
|
39
|
+
sock.puts(JSON.dump(payload))
|
|
40
|
+
response = sock.gets
|
|
41
|
+
sock.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return nil unless response
|
|
45
|
+
|
|
46
|
+
res = JSON.parse(response, symbolize_names: true)
|
|
47
|
+
raise res[:error] unless res[:ok]
|
|
48
|
+
|
|
49
|
+
res[:value]
|
|
50
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED, Timeout::Error
|
|
51
|
+
if attempts <= MudisIPCConfig.retries
|
|
52
|
+
retry
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
warn "[MudisClient] Cannot connect to MudisServer. Is it running?"
|
|
56
|
+
nil
|
|
57
|
+
rescue JSON::ParserError
|
|
58
|
+
warn "[MudisClient] Invalid JSON response from server"
|
|
59
|
+
nil
|
|
60
|
+
rescue IOError, SystemCallError => e
|
|
61
|
+
warn "[MudisClient] Connection error: #{e.message}"
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
50
64
|
end
|
|
51
65
|
end
|
|
52
66
|
|
|
@@ -82,19 +96,44 @@ class MudisClient
|
|
|
82
96
|
new_val
|
|
83
97
|
end
|
|
84
98
|
|
|
85
|
-
#
|
|
86
|
-
def
|
|
87
|
-
command("
|
|
99
|
+
# Inspect metadata for a key
|
|
100
|
+
def inspect(key, namespace: nil)
|
|
101
|
+
command("inspect", key:, namespace:)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Return keys for a namespace
|
|
105
|
+
def keys(namespace:)
|
|
106
|
+
command("keys", namespace:)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Clear keys in a namespace
|
|
110
|
+
def clear_namespace(namespace:)
|
|
111
|
+
command("clear_namespace", namespace:)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Return least touched keys
|
|
115
|
+
def least_touched(limit = 10)
|
|
116
|
+
command("least_touched", limit:)
|
|
88
117
|
end
|
|
89
118
|
|
|
90
|
-
#
|
|
91
|
-
def
|
|
92
|
-
command("
|
|
119
|
+
# Return all keys
|
|
120
|
+
def all_keys
|
|
121
|
+
command("all_keys")
|
|
93
122
|
end
|
|
94
123
|
|
|
95
|
-
#
|
|
96
|
-
def
|
|
97
|
-
command("
|
|
124
|
+
# Return current memory usage
|
|
125
|
+
def current_memory_bytes
|
|
126
|
+
command("current_memory_bytes")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Return max memory configured
|
|
130
|
+
def max_memory_bytes
|
|
131
|
+
command("max_memory_bytes")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Retrieve metrics from the Mudis server
|
|
135
|
+
def metrics
|
|
136
|
+
command("metrics")
|
|
98
137
|
end
|
|
99
138
|
|
|
100
139
|
private
|
|
@@ -103,5 +142,4 @@ class MudisClient
|
|
|
103
142
|
def command(cmd, **opts)
|
|
104
143
|
request({ cmd:, **opts })
|
|
105
144
|
end
|
|
106
|
-
|
|
107
145
|
end
|
data/lib/mudis_config.rb
CHANGED
|
@@ -8,6 +8,7 @@ class MudisConfig
|
|
|
8
8
|
:max_value_bytes,
|
|
9
9
|
:hard_memory_limit,
|
|
10
10
|
:max_bytes,
|
|
11
|
+
:eviction_threshold,
|
|
11
12
|
:buckets,
|
|
12
13
|
:max_ttl,
|
|
13
14
|
:default_ttl,
|
|
@@ -23,6 +24,7 @@ class MudisConfig
|
|
|
23
24
|
@max_value_bytes = nil # Max size per value (optional)
|
|
24
25
|
@hard_memory_limit = false # Enforce max_bytes as hard cap
|
|
25
26
|
@max_bytes = 1_073_741_824 # 1 GB default max cache size
|
|
27
|
+
@eviction_threshold = 0.9 # Evict when bucket exceeds threshold
|
|
26
28
|
@buckets = nil # use nil to signal fallback to ENV or default
|
|
27
29
|
@max_ttl = nil # Max TTL for cache entries (optional)
|
|
28
30
|
@default_ttl = nil # Default TTL for cache entries (optional)
|
data/lib/mudis_ipc_config.rb
CHANGED
|
@@ -5,9 +5,19 @@ module MudisIPCConfig
|
|
|
5
5
|
SOCKET_PATH = "/tmp/mudis.sock"
|
|
6
6
|
TCP_HOST = "127.0.0.1"
|
|
7
7
|
TCP_PORT = 9876
|
|
8
|
+
DEFAULT_TIMEOUT = 1
|
|
9
|
+
DEFAULT_RETRIES = 1
|
|
8
10
|
|
|
9
11
|
# Check if TCP mode should be used (Windows or forced via ENV)
|
|
10
12
|
def self.use_tcp?
|
|
11
13
|
ENV["MUDIS_FORCE_TCP"] == "true" || Gem.win_platform?
|
|
12
14
|
end
|
|
15
|
+
|
|
16
|
+
def self.timeout
|
|
17
|
+
(ENV["MUDIS_IPC_TIMEOUT"] || DEFAULT_TIMEOUT).to_f
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.retries
|
|
21
|
+
(ENV["MUDIS_IPC_RETRIES"] || DEFAULT_RETRIES).to_i
|
|
22
|
+
end
|
|
13
23
|
end
|
data/lib/mudis_server.rb
CHANGED
|
@@ -13,14 +13,19 @@ class MudisServer
|
|
|
13
13
|
# Define command handlers mapping
|
|
14
14
|
# Each command maps to a lambda that takes a request hash and performs the corresponding Mudis operation.
|
|
15
15
|
COMMANDS = {
|
|
16
|
-
"read"
|
|
17
|
-
"write"
|
|
18
|
-
"delete"
|
|
19
|
-
"exists"
|
|
20
|
-
"fetch"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
16
|
+
"read" => ->(r) { Mudis.read(r[:key], namespace: r[:namespace]) },
|
|
17
|
+
"write" => ->(r) { Mudis.write(r[:key], r[:value], expires_in: r[:ttl], namespace: r[:namespace]) },
|
|
18
|
+
"delete" => ->(r) { Mudis.delete(r[:key], namespace: r[:namespace]) },
|
|
19
|
+
"exists" => ->(r) { Mudis.exists?(r[:key], namespace: r[:namespace]) },
|
|
20
|
+
"fetch" => ->(r) { Mudis.fetch(r[:key], expires_in: r[:ttl], namespace: r[:namespace]) { r[:fallback] } },
|
|
21
|
+
"inspect" => ->(r) { Mudis.inspect(r[:key], namespace: r[:namespace]) },
|
|
22
|
+
"keys" => ->(r) { Mudis.keys(namespace: r[:namespace]) },
|
|
23
|
+
"clear_namespace" => ->(r) { Mudis.clear_namespace(namespace: r[:namespace]) },
|
|
24
|
+
"least_touched" => ->(r) { Mudis.least_touched(r[:limit]) },
|
|
25
|
+
"all_keys" => ->(_) { Mudis.all_keys },
|
|
26
|
+
"current_memory_bytes" => ->(_) { Mudis.current_memory_bytes },
|
|
27
|
+
"max_memory_bytes" => ->(_) { Mudis.max_memory_bytes },
|
|
28
|
+
"metrics" => ->(_) { Mudis.metrics }
|
|
24
29
|
}.freeze
|
|
25
30
|
|
|
26
31
|
# Start the MudisServer
|
|
@@ -43,7 +48,7 @@ class MudisServer
|
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# Start UNIX socket server (production mode for Linux/macOS)
|
|
46
|
-
def self.start_unix_server!
|
|
51
|
+
def self.start_unix_server!
|
|
47
52
|
File.unlink(SOCKET_PATH) if File.exist?(SOCKET_PATH)
|
|
48
53
|
server = UNIXServer.new(SOCKET_PATH)
|
|
49
54
|
server.listen(128)
|
data/sig/mudis.rbs
CHANGED
|
@@ -14,6 +14,7 @@ class Mudis
|
|
|
14
14
|
attr_reader max_value_bytes : Integer?
|
|
15
15
|
attr_accessor max_ttl: Integer?
|
|
16
16
|
attr_accessor default_ttl: Integer?
|
|
17
|
+
attr_accessor eviction_threshold: Float?
|
|
17
18
|
|
|
18
19
|
def configure: () { (config: MudisConfig) -> void } -> void
|
|
19
20
|
def config: () -> MudisConfig
|
|
@@ -21,6 +22,13 @@ class Mudis
|
|
|
21
22
|
def validate_config!: () -> void
|
|
22
23
|
|
|
23
24
|
def buckets: () -> Integer
|
|
25
|
+
|
|
26
|
+
def bind: (
|
|
27
|
+
namespace: String,
|
|
28
|
+
?default_ttl: Integer?,
|
|
29
|
+
?max_ttl: Integer?,
|
|
30
|
+
?max_value_bytes: Integer?
|
|
31
|
+
) -> Mudis::Bound
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
# Lifecycle
|
|
@@ -39,17 +47,19 @@ class Mudis
|
|
|
39
47
|
String,
|
|
40
48
|
?expires_in: Integer,
|
|
41
49
|
?force: bool,
|
|
42
|
-
?namespace: String
|
|
50
|
+
?namespace: String,
|
|
51
|
+
?singleflight: bool
|
|
43
52
|
) { () -> untyped } -> untyped
|
|
44
53
|
|
|
45
54
|
def self.clear: (String, ?namespace: String) -> void
|
|
46
55
|
def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
|
|
47
56
|
def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
|
|
48
|
-
def self.keys: (
|
|
49
|
-
def self.clear_namespace: (
|
|
57
|
+
def self.keys: (namespace: String) -> Array[String]
|
|
58
|
+
def self.clear_namespace: (namespace: String) -> void
|
|
59
|
+
def self.with_namespace: (namespace: String) { () -> untyped } -> untyped
|
|
50
60
|
|
|
51
61
|
# Introspection & management
|
|
52
|
-
def self.metrics: () -> Hash[Symbol, untyped]
|
|
62
|
+
def self.metrics: (?namespace: String) -> Hash[Symbol, untyped]
|
|
53
63
|
def self.cleanup_expired!: () -> void
|
|
54
64
|
def self.all_keys: () -> Array[String]
|
|
55
65
|
def self.current_memory_bytes: () -> Integer
|
|
@@ -59,4 +69,9 @@ class Mudis
|
|
|
59
69
|
# State reset
|
|
60
70
|
def self.reset!: () -> void
|
|
61
71
|
def self.reset_metrics!: () -> void
|
|
72
|
+
|
|
73
|
+
# Persistence
|
|
74
|
+
def self.save_snapshot!: () -> void
|
|
75
|
+
def self.load_snapshot!: () -> void
|
|
76
|
+
def self.install_persistence_hook!: () -> void
|
|
62
77
|
end
|
data/sig/mudis_bound.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Mudis
|
|
2
|
+
class Bound
|
|
3
|
+
attr_reader namespace: String
|
|
4
|
+
|
|
5
|
+
def initialize: (
|
|
6
|
+
namespace: String,
|
|
7
|
+
?default_ttl: Integer?,
|
|
8
|
+
?max_ttl: Integer?,
|
|
9
|
+
?max_value_bytes: Integer?
|
|
10
|
+
) -> void
|
|
11
|
+
|
|
12
|
+
def read: (String) -> untyped?
|
|
13
|
+
def write: (String, untyped, ?expires_in: Integer?) -> void
|
|
14
|
+
def update: (String) { (untyped) -> untyped } -> void
|
|
15
|
+
def delete: (String) -> void
|
|
16
|
+
def exists?: (String) -> bool
|
|
17
|
+
def fetch: (String, ?expires_in: Integer?, ?force: bool, ?singleflight: bool) { () -> untyped } -> untyped?
|
|
18
|
+
def clear: (String) -> void
|
|
19
|
+
def replace: (String, untyped, ?expires_in: Integer?) -> void
|
|
20
|
+
def inspect: (String) -> Hash[Symbol, untyped]?
|
|
21
|
+
def keys: () -> Array[String]
|
|
22
|
+
def metrics: () -> Hash[Symbol, untyped]
|
|
23
|
+
def clear_namespace: () -> void
|
|
24
|
+
end
|
|
25
|
+
end
|
data/sig/mudis_client.rbs
CHANGED
|
@@ -5,7 +5,7 @@ class MudisClient
|
|
|
5
5
|
|
|
6
6
|
def open_connection: () -> (TCPSocket | UNIXSocket)
|
|
7
7
|
|
|
8
|
-
def request: (payload:
|
|
8
|
+
def request: (payload: Hash[Symbol, untyped]) -> untyped
|
|
9
9
|
|
|
10
10
|
def read: (key: String, namespace?: String?) -> untyped
|
|
11
11
|
|
|
@@ -17,9 +17,19 @@ class MudisClient
|
|
|
17
17
|
|
|
18
18
|
def fetch: (key: String, expires_in?: Integer?, namespace?: String?, &block: { () -> untyped }) -> untyped
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def inspect: (key: String, namespace?: String?) -> Hash[Symbol, untyped]?
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def keys: (namespace: String) -> Array[String]
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def clear_namespace: (namespace: String) -> void
|
|
25
|
+
|
|
26
|
+
def least_touched: (?Integer) -> Array[[String, Integer]]
|
|
27
|
+
|
|
28
|
+
def all_keys: () -> Array[String]
|
|
29
|
+
|
|
30
|
+
def current_memory_bytes: () -> Integer
|
|
31
|
+
|
|
32
|
+
def max_memory_bytes: () -> Integer
|
|
33
|
+
|
|
34
|
+
def metrics: () -> Hash[Symbol, untyped]
|
|
35
|
+
end
|
data/sig/mudis_config.rbs
CHANGED
|
@@ -4,7 +4,12 @@ class MudisConfig
|
|
|
4
4
|
attr_accessor max_value_bytes: Integer?
|
|
5
5
|
attr_accessor hard_memory_limit: bool
|
|
6
6
|
attr_accessor max_bytes: Integer
|
|
7
|
+
attr_accessor eviction_threshold: Float?
|
|
7
8
|
attr_accessor max_ttl: Integer?
|
|
8
9
|
attr_accessor default_ttl: Integer?
|
|
9
10
|
attr_accessor buckets: Integer?
|
|
11
|
+
attr_accessor persistence_enabled: bool
|
|
12
|
+
attr_accessor persistence_path: String
|
|
13
|
+
attr_accessor persistence_format: Symbol
|
|
14
|
+
attr_accessor persistence_safe_write: bool
|
|
10
15
|
end
|
data/sig/mudis_expiry.rbs
CHANGED
data/sig/mudis_ipc_config.rbs
CHANGED
data/sig/mudis_lru.rbs
CHANGED
data/sig/mudis_metrics.rbs
CHANGED
data/sig/mudis_persistence.rbs
CHANGED
|
@@ -8,12 +8,12 @@ class Mudis
|
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
def snapshot_dump: () ->
|
|
11
|
+
def snapshot_dump: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
|
|
12
12
|
|
|
13
|
-
def snapshot_restore: (
|
|
13
|
+
def snapshot_restore: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
|
|
14
14
|
|
|
15
|
-
def safe_write_snapshot: (
|
|
15
|
+
def safe_write_snapshot: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
|
|
16
16
|
|
|
17
|
-
def read_snapshot: () ->
|
|
17
|
+
def read_snapshot: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
|
|
18
18
|
end
|
|
19
19
|
end
|