Class: Mudis
- Inherits:
-
Object
- Object
- Mudis
- Defined in:
- lib/mudis.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
Classes: LRUNode
Class Attribute Summary collapse
-
.compress ⇒ Object
Returns the value of attribute compress.
-
.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.
-
.bucket_index(key) ⇒ Object
Computes which bucket a key belongs to.
-
.buckets ⇒ Object
Number of cache buckets (shards).
-
.cleanup_expired! ⇒ Object
Removes expired keys across all buckets.
-
.current_memory_bytes ⇒ Object
Returns total memory used across all buckets.
-
.delete(key) ⇒ Object
Deletes a key from the cache.
-
.exists?(key) ⇒ Boolean
Checks if a key exists and is not expired.
-
.max_memory_bytes ⇒ Object
Returns configured maximum memory allowed.
-
.metrics ⇒ Object
Returns a snapshot of metrics (thread-safe).
-
.read(key) ⇒ Object
Reads and returns the value for a key, updating LRU and metrics.
-
.start_expiry_thread(interval: 60) ⇒ Object
Starts a thread that periodically removes expired entries.
-
.stop_expiry_thread ⇒ Object
Signals and joins the expiry thread.
-
.update(key) ⇒ Object
Atomically updates the value for a key using a block.
-
.write(key, value, expires_in: nil) ⇒ Object
Writes a value to the cache with optional expiry and LRU tracking.
Class Attribute Details
.compress ⇒ Object
Returns the value of attribute compress.
19 20 21 |
# File 'lib/mudis.rb', line 19 def compress @compress end |
.max_value_bytes ⇒ Object
Returns the value of attribute max_value_bytes.
19 20 21 |
# File 'lib/mudis.rb', line 19 def max_value_bytes @max_value_bytes end |
.serializer ⇒ Object
Returns the value of attribute serializer.
19 20 21 |
# File 'lib/mudis.rb', line 19 def serializer @serializer end |
Class Method Details
.all_keys ⇒ Object
Returns an array of all cache keys
193 194 195 196 197 198 199 200 201 |
# File 'lib/mudis.rb', line 193 def all_keys keys = [] buckets.times do |idx| mutex = @mutexes[idx] store = @stores[idx] mutex.synchronize { keys.concat(store.keys) } end keys end |
.bucket_index(key) ⇒ Object
Computes which bucket a key belongs to
77 78 79 |
# File 'lib/mudis.rb', line 77 def bucket_index(key) key.hash % buckets end |
.buckets ⇒ Object
Number of cache buckets (shards). Default: 32
38 39 40 |
# File 'lib/mudis.rb', line 38 def self.buckets @buckets ||= (ENV['MUDIS_BUCKETS']&.to_i || 32) end |
.cleanup_expired! ⇒ Object
Removes expired keys across all buckets
177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/mudis.rb', line 177 def cleanup_expired! now = Time.now buckets.times do |idx| mutex = @mutexes[idx] store = @stores[idx] mutex.synchronize do store.keys.each do |key| if store[key][:expires_at] && now > store[key][:expires_at] evict_key(idx, key) end end end end end |
.current_memory_bytes ⇒ Object
Returns total memory used across all buckets
204 205 206 |
# File 'lib/mudis.rb', line 204 def current_memory_bytes @current_bytes.sum end |
.delete(key) ⇒ Object
Deletes a key from the cache
167 168 169 170 171 172 173 174 |
# File 'lib/mudis.rb', line 167 def delete(key) idx = bucket_index(key) mutex = @mutexes[idx] mutex.synchronize do evict_key(idx, key) end end |
.exists?(key) ⇒ Boolean
Checks if a key exists and is not expired
82 83 84 |
# File 'lib/mudis.rb', line 82 def exists?(key) !!read(key) end |
.max_memory_bytes ⇒ Object
Returns configured maximum memory allowed
209 210 211 |
# File 'lib/mudis.rb', line 209 def max_memory_bytes @max_bytes end |
.metrics ⇒ Object
Returns a snapshot of metrics (thread-safe)
22 23 24 |
# File 'lib/mudis.rb', line 22 def metrics @metrics_mutex.synchronize { @metrics.dup } end |
.read(key) ⇒ Object
Reads and returns the value for a key, updating LRU and metrics
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/mudis.rb', line 87 def read(key) raw_entry = nil idx = bucket_index(key) mutex = @mutexes[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 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 |
.start_expiry_thread(interval: 60) ⇒ Object
Starts a thread that periodically removes expired entries
56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/mudis.rb', line 56 def start_expiry_thread(interval: 60) return if @expiry_thread&.alive? @stop_expiry = false @expiry_thread = Thread.new do loop do break if @stop_expiry sleep interval cleanup_expired! end end end |
.stop_expiry_thread ⇒ Object
Signals and joins the expiry thread
70 71 72 73 74 |
# File 'lib/mudis.rb', line 70 def stop_expiry_thread @stop_expiry = true @expiry_thread&.join @expiry_thread = nil end |
.update(key) ⇒ Object
Atomically updates the value for a key using a block
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/mudis.rb', line 141 def update(key) 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 |
.write(key, value, expires_in: nil) ⇒ Object
Writes a value to the cache with optional expiry and LRU tracking
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/mudis.rb', line 111 def write(key, value, expires_in: nil) 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 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 } insert_lru(idx, key) @current_bytes[idx] += size end end |