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.
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
- yield(config)
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
- @threshold_bytes = (@max_bytes * 0.9).to_i
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
- @threshold_bytes = (@max_bytes * 0.9).to_i # Eviction threshold at 90%
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
- key = namespaced_key(key, namespace)
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
- store[key][:touches] = (store[key][:touches] || 0) + 1 if store[key]
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
- value = decompress_and_deserialize(raw_entry[:value])
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
- evict_key(idx, @lru_tails[idx].key)
215
- metric(:evictions)
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
- old_size = key.bytesize + raw_entry[:value].bytesize
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
- key = namespaced_key(key, namespace)
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
- value = yield
280
- write(key, value, expires_in: expires_in)
281
- value
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, Metrics/AbcSize
29
+ def request(payload) # rubocop:disable Metrics/MethodLength
29
30
  @mutex.synchronize do
30
- sock = open_connection
31
- sock.puts(JSON.dump(payload))
32
- response = sock.gets
33
- sock.close
34
-
35
- return nil unless response
36
-
37
- res = JSON.parse(response, symbolize_names: true)
38
- raise res[:error] unless res[:ok]
39
-
40
- res[:value]
41
- rescue Errno::ENOENT, Errno::ECONNREFUSED
42
- warn "[MudisClient] Cannot connect to MudisServer. Is it running?"
43
- nil
44
- rescue JSON::ParserError
45
- warn "[MudisClient] Invalid JSON response from server"
46
- nil
47
- rescue IOError, SystemCallError => e
48
- warn "[MudisClient] Connection error: #{e.message}"
49
- nil
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
- # Retrieve metrics from the Mudis server
86
- def metrics
87
- command("metrics")
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
- # Reset metrics on the Mudis server
91
- def reset_metrics!
92
- command("reset_metrics")
119
+ # Return all keys
120
+ def all_keys
121
+ command("all_keys")
93
122
  end
94
123
 
95
- # Reset the Mudis server cache state
96
- def reset!
97
- command("reset")
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)
@@ -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" => ->(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
- "metrics" => ->(_) { Mudis.metrics },
22
- "reset_metrics" => ->(_) { Mudis.reset_metrics! },
23
- "reset" => ->(_) { Mudis.reset! }
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! # rubocop:disable Metrics/MethodLength
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: (?namespace: String) -> Array[String]
49
- def self.clear_namespace: (?namespace: String) -> void
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
@@ -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: { cmd: String, key?: String, value?: untyped, ttl?: Integer?, namespace?: String? }) -> untyped
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 metrics: () -> { reads: Integer, writes: Integer, deletes: Integer, exists: Integer }
20
+ def inspect: (key: String, namespace?: String?) -> Hash[Symbol, untyped]?
21
21
 
22
- def reset_metrics!: () -> void
22
+ def keys: (namespace: String) -> Array[String]
23
23
 
24
- def reset!: () -> void
25
- end
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
@@ -8,6 +8,6 @@ class Mudis
8
8
 
9
9
  private
10
10
 
11
- def effective_ttl: (?expires_in: Integer?) -> Integer?
11
+ def effective_ttl: (Integer?) -> Integer?
12
12
  end
13
13
  end
@@ -6,5 +6,13 @@ module MudisIPCConfig
6
6
 
7
7
  TCP_PORT: Integer
8
8
 
9
+ DEFAULT_TIMEOUT: Integer
10
+
11
+ DEFAULT_RETRIES: Integer
12
+
9
13
  def self.use_tcp?: () -> bool
14
+
15
+ def self.timeout: () -> Float
16
+
17
+ def self.retries: () -> Integer
10
18
  end
data/sig/mudis_lru.rbs CHANGED
@@ -12,7 +12,7 @@ class Mudis
12
12
 
13
13
  def evict_key: (Integer, String) -> void
14
14
 
15
- def insert_lru: (Integer, LRUNode) -> void
15
+ def insert_lru: (Integer, String) -> void
16
16
 
17
17
  def promote_lru: (Integer, String) -> void
18
18
 
@@ -1,6 +1,6 @@
1
1
  class Mudis
2
2
  module Metrics
3
- def metrics: () -> Hash[Symbol, untyped]
3
+ def metrics: (?namespace: String) -> Hash[Symbol, untyped]
4
4
 
5
5
  def reset_metrics!: () -> void
6
6
 
@@ -8,12 +8,12 @@ class Mudis
8
8
 
9
9
  private
10
10
 
11
- def snapshot_dump: () -> Hash[String, untyped]
11
+ def snapshot_dump: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
12
12
 
13
- def snapshot_restore: (Hash[String, untyped]) -> void
13
+ def snapshot_restore: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
14
14
 
15
- def safe_write_snapshot: (Hash[String, untyped]) -> void
15
+ def safe_write_snapshot: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
16
16
 
17
- def read_snapshot: () -> Hash[String, untyped]
17
+ def read_snapshot: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
18
18
  end
19
19
  end