Class: Mudis
- Inherits:
-
Object
- Object
- Mudis
- Extended by:
- Expiry, LRU, Metrics, Namespace, Persistence
- Defined in:
- lib/mudis.rb,
lib/mudis/lru.rb,
lib/mudis/bound.rb,
lib/mudis/expiry.rb,
lib/mudis/metrics.rb,
lib/mudis/namespace.rb,
lib/mudis/persistence.rb
Overview
Mudis is a thread-safe, in-memory, sharded, LRU cache with optional compression and expiry. It is designed for high concurrency and performance within a Ruby application.
Defined Under Namespace
Modules: Expiry, LRU, Metrics, Namespace, Persistence Classes: Bound, LRUNode
Class Attribute Summary collapse
-
.compress ⇒ Object
Returns the value of attribute compress.
-
.default_ttl ⇒ Object
Returns the value of attribute default_ttl.
-
.eviction_threshold ⇒ Object
Returns the value of attribute eviction_threshold.
-
.hard_memory_limit ⇒ Object
Returns the value of attribute hard_memory_limit.
-
.max_bytes ⇒ Object
Returns the value of attribute max_bytes.
-
.max_ttl ⇒ Object
Returns the value of attribute max_ttl.
-
.max_value_bytes ⇒ Object
Returns the value of attribute max_value_bytes.
-
.serializer ⇒ Object
Returns the value of attribute serializer.
Class Method Summary collapse
-
.all_keys ⇒ Object
Returns an array of all cache keys.
-
.apply_config! ⇒ Object
Applies the current configuration to Mudis.
- .bind(namespace:, default_ttl: nil, max_ttl: nil, max_value_bytes: nil) ⇒ Object
-
.bucket_index(key) ⇒ Object
Computes which bucket a key belongs to.
-
.buckets ⇒ Object
Number of cache buckets (shards).
-
.clear(key, namespace: nil) ⇒ Object
Clears a specific key from the cache, a semantic synonym for delete This method is provided for clarity in usage It behaves the same as delete.
-
.config ⇒ Object
Returns the current configuration object.
-
.configure ⇒ Object
Configures Mudis with a block, allowing customization of settings.
-
.current_memory_bytes ⇒ Object
Returns total memory used across all buckets.
-
.delete(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars.
-
.exists?(key, namespace: nil) ⇒ Boolean
Checks if a key exists and is not expired.
-
.fetch(*a, **k, &b) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars.
-
.inspect(key, namespace: nil) ⇒ Object
Inspects a key and returns all meta data for it.
-
.least_touched(n = 10) ⇒ Object
Returns the least-touched keys across all buckets.
-
.max_memory_bytes ⇒ Object
Returns configured maximum memory allowed.
-
.metrics ⇒ Object
rubocop:disable Style/GlobalVars.
-
.read(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars.
-
.replace(key, value, expires_in: nil, namespace: nil) ⇒ Object
Replaces the value for a key if it exists, otherwise does nothing This is useful for updating values without needing to check existence first It will write the new value and update the expiration if provided If the key does not exist, it will not create a new entry.
-
.reset! ⇒ Object
rubocop:disable Style/GlobalVars.
-
.reset_metrics! ⇒ Object
rubocop:disable Style/GlobalVars.
-
.update(key, namespace: nil) ⇒ Object
Atomically updates the value for a key using a block.
-
.validate_config! ⇒ Object
Validates the current configuration, raising errors for invalid settings.
-
.write(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars.
Methods included from Persistence
install_persistence_hook!, load_snapshot!, save_snapshot!
Methods included from Metrics
Methods included from Namespace
clear_namespace, keys, with_namespace
Methods included from Expiry
cleanup_expired!, start_expiry_thread, stop_expiry_thread
Class Attribute Details
.compress ⇒ Object
Returns the value of attribute compress.
40 41 42 |
# File 'lib/mudis.rb', line 40 def compress @compress end |
.default_ttl ⇒ Object
Returns the value of attribute default_ttl.
40 41 42 |
# File 'lib/mudis.rb', line 40 def default_ttl @default_ttl end |
.eviction_threshold ⇒ Object
Returns the value of attribute eviction_threshold.
40 41 42 |
# File 'lib/mudis.rb', line 40 def eviction_threshold @eviction_threshold end |
.hard_memory_limit ⇒ Object
Returns the value of attribute hard_memory_limit.
40 41 42 |
# File 'lib/mudis.rb', line 40 def hard_memory_limit @hard_memory_limit end |
.max_bytes ⇒ Object
Returns the value of attribute max_bytes.
41 42 43 |
# File 'lib/mudis.rb', line 41 def max_bytes @max_bytes end |
.max_ttl ⇒ Object
Returns the value of attribute max_ttl.
40 41 42 |
# File 'lib/mudis.rb', line 40 def max_ttl @max_ttl end |
.max_value_bytes ⇒ Object
Returns the value of attribute max_value_bytes.
41 42 43 |
# File 'lib/mudis.rb', line 41 def max_value_bytes @max_value_bytes end |
.serializer ⇒ Object
Returns the value of attribute serializer.
40 41 42 |
# File 'lib/mudis.rb', line 40 def serializer @serializer end |
Class Method Details
.all_keys ⇒ Object
Returns an array of all cache keys
389 390 391 392 393 394 395 396 397 |
# File 'lib/mudis.rb', line 389 def all_keys keys = [] buckets.times do |idx| mutex = @mutexes[idx] store = @stores[idx] mutex.synchronize { keys.concat(store.keys) } end keys end |
.apply_config! ⇒ Object
Applies the current configuration to Mudis
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/mudis.rb', line 71 def apply_config! # rubocop:disable Metrics/AbcSize,Metrics/MethodLength validate_config! self.serializer = config.serializer self.compress = config.compress self.max_value_bytes = config.max_value_bytes self.hard_memory_limit = config.hard_memory_limit self.max_bytes = config.max_bytes self.eviction_threshold = config.eviction_threshold self.max_ttl = config.max_ttl self.default_ttl = config.default_ttl @persistence_enabled = config.persistence_enabled @persistence_path = config.persistence_path @persistence_format = config.persistence_format @persistence_safe_write = config.persistence_safe_write if config.buckets @buckets = config.buckets reset! end return unless @persistence_enabled install_persistence_hook! end |
.bind(namespace:, default_ttl: nil, max_ttl: nil, max_value_bytes: nil) ⇒ Object
61 62 63 64 65 66 67 68 |
# File 'lib/mudis.rb', line 61 def bind(namespace:, default_ttl: nil, max_ttl: nil, max_value_bytes: nil) Bound.new( namespace: namespace, default_ttl: default_ttl, max_ttl: max_ttl, max_value_bytes: max_value_bytes ) end |
.bucket_index(key) ⇒ Object
Computes which bucket a key belongs to
188 189 190 |
# File 'lib/mudis.rb', line 188 def bucket_index(key) key.hash % buckets end |
.buckets ⇒ Object
Number of cache buckets (shards). Default: 32
159 160 161 162 163 164 165 166 |
# File 'lib/mudis.rb', line 159 def self.buckets return @buckets if @buckets val = config.buckets || ENV["MUDIS_BUCKETS"]&.to_i || 32 raise ArgumentError, "bucket count must be > 0" if val <= 0 @buckets = val end |
.clear(key, namespace: nil) ⇒ Object
Clears a specific key from the cache, a semantic synonym for delete This method is provided for clarity in usage It behaves the same as delete
352 353 354 |
# File 'lib/mudis.rb', line 352 def clear(key, namespace: nil) delete(key, namespace: namespace) end |
.config ⇒ Object
Returns the current configuration object
57 58 59 |
# File 'lib/mudis.rb', line 57 def config @config ||= MudisConfig.new end |
.configure ⇒ Object
Configures Mudis with a block, allowing customization of settings
44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/mudis.rb', line 44 def configure new_config = config.dup yield(new_config) old_config = @config @config = new_config apply_config! rescue StandardError @config = old_config raise end |
.current_memory_bytes ⇒ Object
Returns total memory used across all buckets
418 419 420 |
# File 'lib/mudis.rb', line 418 def current_memory_bytes @current_bytes.sum end |
.delete(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars
326 327 328 329 330 331 332 333 334 |
# File 'lib/mudis.rb', line 326 def delete(key, namespace: nil) key = namespaced_key(key, namespace) idx = bucket_index(key) mutex = @mutexes[idx] mutex.synchronize do evict_key(idx, key) end end |
.exists?(key, namespace: nil) ⇒ Boolean
Checks if a key exists and is not expired
193 194 195 |
# File 'lib/mudis.rb', line 193 def exists?(key, namespace: nil) !!read(key, namespace: namespace) end |
.fetch(*a, **k, &b) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars
340 341 342 343 344 345 346 347 |
# File 'lib/mudis.rb', line 340 def fetch(key, expires_in: nil, force: false, namespace: nil, singleflight: false) # rubocop:disable Metrics/MethodLength return fetch_without_lock(key, expires_in:, force:, namespace:) { yield } unless singleflight lock_key = namespaced_key(key, namespace) with_inflight_lock(lock_key) do fetch_without_lock(key, expires_in:, force:, namespace:) { yield } end end |
.inspect(key, namespace: nil) ⇒ Object
Inspects a key and returns all meta data for it
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/mudis.rb', line 367 def inspect(key, namespace: nil) # rubocop:disable Metrics/MethodLength key = namespaced_key(key, namespace) idx = bucket_index(key) store = @stores[idx] mutex = @mutexes[idx] mutex.synchronize do entry = store[key] return nil unless entry { key: key, bucket: idx, expires_at: entry[:expires_at], created_at: entry[:created_at], size_bytes: key.bytesize + entry[:value].bytesize, compressed: compress } end end |
.least_touched(n = 10) ⇒ Object
Returns the least-touched keys across all buckets
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/mudis.rb', line 400 def least_touched(n = 10) # rubocop:disable Metrics/MethodLength,Naming/MethodParameterName keys_with_touches = [] buckets.times do |idx| mutex = @mutexes[idx] store = @stores[idx] mutex.synchronize do store.each do |key, entry| keys_with_touches << [key, entry[:touches] || 0] end end end keys_with_touches.sort_by { |_, count| count }.first(n) end |
.max_memory_bytes ⇒ Object
Returns configured maximum memory allowed
423 424 425 |
# File 'lib/mudis.rb', line 423 def max_memory_bytes @max_bytes end |
.metrics ⇒ Object
rubocop:disable Style/GlobalVars
34 |
# File 'lib/mudis_proxy.rb', line 34 def metrics = $mudis.metrics # rubocop:disable Style/GlobalVars |
.read(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/mudis.rb', line 198 def read(key, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity key = namespaced_key(key, namespace) ns = namespace || Thread.current[:mudis_namespace] raw_entry = nil idx = bucket_index(key) mutex = @mutexes[idx] store = @stores[idx] mutex.synchronize do raw_entry = @stores[idx][key] if raw_entry && raw_entry[:expires_at] && Time.now > raw_entry[:expires_at] evict_key(idx, key) raw_entry = nil end if store[key] store[key][:touches] = (store[key][:touches] || 0) + 1 promote_lru(idx, key) end metric(:hits, namespace: ns) if raw_entry metric(:misses, namespace: ns) unless raw_entry end return nil unless raw_entry decompress_and_deserialize(raw_entry[:value]) end |
.replace(key, value, expires_in: nil, namespace: nil) ⇒ Object
Replaces the value for a key if it exists, otherwise does nothing This is useful for updating values without needing to check existence first It will write the new value and update the expiration if provided If the key does not exist, it will not create a new entry
360 361 362 363 364 |
# File 'lib/mudis.rb', line 360 def replace(key, value, expires_in: nil, namespace: nil) return unless exists?(key, namespace: namespace) write(key, value, expires_in: expires_in, namespace: namespace) end |
.reset! ⇒ Object
rubocop:disable Style/GlobalVars
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/mudis.rb', line 116 def reset! stop_expiry_thread @buckets = nil b = buckets @stores = Array.new(b) { {} } @mutexes = Array.new(b) { Mutex.new } @lru_heads = Array.new(b) { nil } @lru_tails = Array.new(b) { nil } @lru_nodes = Array.new(b) { {} } @current_bytes = Array.new(b, 0) @inflight_mutexes = {} reset_metrics! end |
.reset_metrics! ⇒ Object
rubocop:disable Style/GlobalVars
35 |
# File 'lib/mudis_proxy.rb', line 35 def reset_metrics! = $mudis.reset_metrics! # rubocop:disable Style/GlobalVars |
.update(key, namespace: nil) ⇒ Object
Atomically updates the value for a key using a block
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/mudis.rb', line 272 def update(key, namespace: nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength key = namespaced_key(key, namespace) idx = bucket_index(key) mutex = @mutexes[idx] store = @stores[idx] raw_entry = nil mutex.synchronize do raw_entry = store[key] return nil unless raw_entry end value = decompress_and_deserialize(raw_entry[:value]) new_value = yield(value) new_raw = serializer.dump(new_value) new_raw = Zlib::Deflate.deflate(new_raw) if compress return if max_value_bytes && new_raw.bytesize > max_value_bytes mutex.synchronize do current_entry = store[key] return nil unless current_entry old_size = key.bytesize + current_entry[:value].bytesize new_size = key.bytesize + new_raw.bytesize ns = current_entry[:namespace] if hard_memory_limit && (current_memory_bytes - old_size + new_size) > max_memory_bytes metric(:rejected, namespace: ns) return end while (@current_bytes[idx] - old_size + new_size) > (@threshold_bytes / buckets) && @lru_tails[idx] break if @lru_tails[idx].key == key evict_key_name = @lru_tails[idx].key evict_ns = store[evict_key_name] && store[evict_key_name][:namespace] evict_key(idx, evict_key_name) metric(:evictions, namespace: evict_ns) end store[key][:value] = new_raw # Refresh TTL on update. If no TTL applies, keep existing expiry (possibly nil). now = Time.now ttl = current_entry[:expires_at] ? (current_entry[:expires_at] - current_entry[:created_at]) : nil store[key][:created_at] = now store[key][:expires_at] = ttl ? now + ttl : nil @current_bytes[idx] += (new_size - old_size) promote_lru(idx, key) end end |
.validate_config! ⇒ Object
Validates the current configuration, raising errors for invalid settings
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/mudis.rb', line 99 def validate_config! # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity if config.max_value_bytes && config.max_value_bytes > config.max_bytes raise ArgumentError, "max_value_bytes cannot exceed max_bytes" end raise ArgumentError, "max_value_bytes must be > 0" if config.max_value_bytes && config.max_value_bytes <= 0 raise ArgumentError, "buckets must be > 0" if config.buckets && config.buckets <= 0 if config.eviction_threshold && (config.eviction_threshold <= 0 || config.eviction_threshold > 1) raise ArgumentError, "eviction_threshold must be > 0 and <= 1" end raise ArgumentError, "max_ttl must be > 0" if config.max_ttl && config.max_ttl <= 0 raise ArgumentError, "default_ttl must be > 0" if config.default_ttl && config.default_ttl <= 0 end |
.write(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/mudis.rb', line 228 def write(key, value, expires_in: nil, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity key = namespaced_key(key, namespace) ns = namespace || Thread.current[:mudis_namespace] raw = serializer.dump(value) raw = Zlib::Deflate.deflate(raw) if compress size = key.bytesize + raw.bytesize return if max_value_bytes && raw.bytesize > max_value_bytes if hard_memory_limit && current_memory_bytes + size > max_memory_bytes metric(:rejected, namespace: ns) return end # Ensure expires_in respects max_ttl and default_ttl expires_in = effective_ttl(expires_in) idx = bucket_index(key) mutex = @mutexes[idx] store = @stores[idx] mutex.synchronize do evict_key(idx, key) if store[key] while @current_bytes[idx] + size > (@threshold_bytes / buckets) && @lru_tails[idx] evict_key_name = @lru_tails[idx].key evict_ns = store[evict_key_name] && store[evict_key_name][:namespace] evict_key(idx, evict_key_name) metric(:evictions, namespace: evict_ns) end store[key] = { value: raw, expires_at: expires_in ? Time.now + expires_in : nil, created_at: Time.now, touches: 0, namespace: ns } insert_lru(idx, key) @current_bytes[idx] += size end end |