Class: Mudis
- Inherits:
-
Object
- Object
- Mudis
- Extended by:
- Expiry, LRU, Metrics, Namespace, Persistence
- Defined in:
- lib/mudis.rb,
lib/mudis/lru.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: LRUNode
Class Attribute Summary collapse
-
.compress ⇒ Object
Returns the value of attribute compress.
-
.default_ttl ⇒ Object
Returns the value of attribute default_ttl.
-
.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.
-
.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 {|config| ... } ⇒ 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.
37 38 39 |
# File 'lib/mudis.rb', line 37 def compress @compress end |
.default_ttl ⇒ Object
Returns the value of attribute default_ttl.
37 38 39 |
# File 'lib/mudis.rb', line 37 def default_ttl @default_ttl end |
.hard_memory_limit ⇒ Object
Returns the value of attribute hard_memory_limit.
37 38 39 |
# File 'lib/mudis.rb', line 37 def hard_memory_limit @hard_memory_limit end |
.max_bytes ⇒ Object
Returns the value of attribute max_bytes.
38 39 40 |
# File 'lib/mudis.rb', line 38 def max_bytes @max_bytes end |
.max_ttl ⇒ Object
Returns the value of attribute max_ttl.
37 38 39 |
# File 'lib/mudis.rb', line 37 def max_ttl @max_ttl end |
.max_value_bytes ⇒ Object
Returns the value of attribute max_value_bytes.
38 39 40 |
# File 'lib/mudis.rb', line 38 def max_value_bytes @max_value_bytes end |
.serializer ⇒ Object
Returns the value of attribute serializer.
37 38 39 |
# File 'lib/mudis.rb', line 37 def serializer @serializer end |
Class Method Details
.all_keys ⇒ Object
Returns an array of all cache keys
324 325 326 327 328 329 330 331 332 |
# File 'lib/mudis.rb', line 324 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
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/mudis.rb', line 52 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.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 |
.bucket_index(key) ⇒ Object
Computes which bucket a key belongs to
152 153 154 |
# File 'lib/mudis.rb', line 152 def bucket_index(key) key.hash % buckets end |
.buckets ⇒ Object
Number of cache buckets (shards). Default: 32
126 127 128 129 130 131 132 133 |
# File 'lib/mudis.rb', line 126 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
287 288 289 |
# File 'lib/mudis.rb', line 287 def clear(key, namespace: nil) delete(key, namespace: namespace) end |
.config ⇒ Object
Returns the current configuration object
47 48 49 |
# File 'lib/mudis.rb', line 47 def config @config ||= MudisConfig.new end |
.configure {|config| ... } ⇒ Object
Configures Mudis with a block, allowing customization of settings
41 42 43 44 |
# File 'lib/mudis.rb', line 41 def configure yield(config) apply_config! end |
.current_memory_bytes ⇒ Object
Returns total memory used across all buckets
353 354 355 |
# File 'lib/mudis.rb', line 353 def current_memory_bytes @current_bytes.sum end |
.delete(*a, **k) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars
258 259 260 261 262 263 264 265 266 |
# File 'lib/mudis.rb', line 258 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
157 158 159 160 |
# File 'lib/mudis.rb', line 157 def exists?(key, namespace: nil) key = namespaced_key(key, namespace) !!read(key) end |
.fetch(*a, **k, &b) ⇒ Object
rubocop:disable Naming/MethodParameterName,Style/GlobalVars
272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/mudis.rb', line 272 def fetch(key, expires_in: nil, force: false, namespace: nil) key = namespaced_key(key, namespace) unless force cached = read(key) return cached if cached end value = yield write(key, value, expires_in: expires_in) value end |
.inspect(key, namespace: nil) ⇒ Object
Inspects a key and returns all meta data for it
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/mudis.rb', line 302 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
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/mudis.rb', line 335 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
358 359 360 |
# File 'lib/mudis.rb', line 358 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
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/mudis.rb', line 163 def read(key, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity key = namespaced_key(key, 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 store[key][:touches] = (store[key][:touches] || 0) + 1 if store[key] metric(:hits) if raw_entry metric(:misses) unless raw_entry end return nil unless raw_entry value = decompress_and_deserialize(raw_entry[:value]) promote_lru(idx, key) 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
295 296 297 298 299 |
# File 'lib/mudis.rb', line 295 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
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/mudis.rb', line 93 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) 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
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 |
# File 'lib/mudis.rb', line 231 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 mutex.synchronize do old_size = key.bytesize + raw_entry[:value].bytesize new_size = key.bytesize + new_raw.bytesize store[key][:value] = new_raw @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
79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/mudis.rb', line 79 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 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
191 192 193 194 195 196 197 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 226 227 228 |
# File 'lib/mudis.rb', line 191 def write(key, value, expires_in: nil, namespace: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity key = namespaced_key(key, 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) 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(idx, @lru_tails[idx].key) metric(:evictions) end store[key] = { value: raw, expires_at: expires_in ? Time.now + expires_in : nil, created_at: Time.now, touches: 0 } insert_lru(idx, key) @current_bytes[idx] += size end end |