Class: Factorix::Cache::Redis
Overview
Redis-based cache storage implementation.
Stores cache entries in Redis with automatic namespace prefixing. Metadata (size, created_at) stored in separate hash keys. Supports distributed locking with Lua script for atomic release.
Constant Summary collapse
- DEFAULT_LOCK_TIMEOUT =
Default timeout for distributed lock acquisition in seconds.
30
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
-
#age(key) ⇒ Integer?
Get the age of a cache entry in seconds.
-
#backend_info ⇒ Hash
Return backend-specific information.
-
#clear ⇒ void
Clear all cache entries in this namespace.
-
#delete(key) ⇒ Boolean
Delete a cache entry.
-
#each {|key, entry| ... } ⇒ Enumerator
Enumerate cache entries.
-
#exist?(key) ⇒ Boolean
Check if a cache entry exists.
-
#expired?(key) ⇒ Boolean
Check if a cache entry has expired.
-
#initialize(cache_type:, url: nil, lock_timeout: DEFAULT_LOCK_TIMEOUT) ⇒ Redis
constructor
Initialize a new Redis cache storage.
-
#read(key) ⇒ String?
Read a cached entry.
-
#size(key) ⇒ Integer?
Get the size of a cached entry in bytes.
-
#store(key, src) ⇒ Boolean
Store data in the cache.
-
#with_lock(key) { ... } ⇒ Object
Execute a block with a distributed lock.
-
#write_to(key, output) ⇒ Boolean
Write cached content to a file.
Constructor Details
#initialize(cache_type:, url: nil, lock_timeout: DEFAULT_LOCK_TIMEOUT) ⇒ Redis
Initialize a new Redis cache storage.
55 56 57 58 59 60 61 62 |
# File 'lib/factorix/cache/redis.rb', line 55 def initialize(cache_type:, url: nil, lock_timeout: DEFAULT_LOCK_TIMEOUT, **) super(**) @url = url || ENV.fetch("REDIS_URL", nil) @redis = ::Redis.new(url: @url) @namespace = "factorix-cache:#{cache_type}" @lock_timeout = lock_timeout logger.info("Initializing Redis cache", namespace: @namespace, ttl: @ttl, lock_timeout: @lock_timeout) end |
Instance Method Details
#age(key) ⇒ Integer?
Get the age of a cache entry in seconds.
151 152 153 154 155 156 157 158 159 |
# File 'lib/factorix/cache/redis.rb', line 151 def age(key) value = @redis.hget((key), "created_at") return nil if value.nil? created_at = Integer(value, 10) return nil if created_at.zero? Time.now.to_i - created_at end |
#backend_info ⇒ Hash
Return backend-specific information.
242 243 244 245 246 247 248 249 |
# File 'lib/factorix/cache/redis.rb', line 242 def backend_info { type: "redis", url: mask_url(@url), namespace: @namespace, lock_timeout: @lock_timeout } end |
#clear ⇒ void
This method returns an undefined value.
Clear all cache entries in this namespace.
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/factorix/cache/redis.rb', line 129 def clear logger.info("Clearing Redis cache namespace", namespace: @namespace) count = 0 cursor = "0" pattern = "#{@namespace}:*" loop do cursor, keys = @redis.scan(cursor, match: pattern, count: 100) unless keys.empty? @redis.del(*keys) count += keys.size end break if cursor == "0" end logger.info("Cache cleared", keys_removed: count) end |
#delete(key) ⇒ Boolean
Delete a cache entry.
120 121 122 123 124 |
# File 'lib/factorix/cache/redis.rb', line 120 def delete(key) deleted = @redis.del(data_key(key), (key)) logger.debug("Deleted from cache", key:) if deleted.positive? deleted.positive? end |
#each {|key, entry| ... } ⇒ Enumerator
Enumerate cache entries.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/factorix/cache/redis.rb', line 211 def each return enum_for(__method__) unless block_given? cursor = "0" pattern = "#{@namespace}:*" loop do cursor, keys = @redis.scan(cursor, match: pattern, count: 100) keys.each do |data_k| next if data_k.include?(":meta:") || data_k.include?(":lock:") logical_key = logical_key_from_data_key(data_k) = @redis.hgetall((logical_key)) entry = Entry[ size: ["size"] ? Integer(["size"], 10) : 0, age: ["created_at"] ? Time.now.to_i - Integer(["created_at"], 10) : 0, expired: false # Redis handles expiry natively ] yield logical_key, entry end break if cursor == "0" end end |
#exist?(key) ⇒ Boolean
Check if a cache entry exists.
68 |
# File 'lib/factorix/cache/redis.rb', line 68 def exist?(key) = @redis.exists?(data_key(key)) |
#expired?(key) ⇒ Boolean
Check if a cache entry has expired. With Redis native EXPIRE, non-existent keys are considered expired.
166 |
# File 'lib/factorix/cache/redis.rb', line 166 def expired?(key) = !exist?(key) |
#read(key) ⇒ String?
Read a cached entry.
74 75 76 |
# File 'lib/factorix/cache/redis.rb', line 74 def read(key) @redis.get(data_key(key)) end |
#size(key) ⇒ Integer?
Get the size of a cached entry in bytes.
172 173 174 175 176 177 |
# File 'lib/factorix/cache/redis.rb', line 172 def size(key) return nil unless exist?(key) value = @redis.hget((key), "size") value.nil? ? nil : Integer(value, 10) end |
#store(key, src) ⇒ Boolean
Store data in the cache.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/factorix/cache/redis.rb', line 97 def store(key, src) data = src.binread data_k = data_key(key) = (key) @redis.multi do |tx| tx.set(data_k, data) tx.hset(, "size", data.bytesize, "created_at", Time.now.to_i) if @ttl tx.expire(data_k, @ttl) tx.expire(, @ttl) end end logger.debug("Stored in cache", key:, size_bytes: data.bytesize) true end |
#with_lock(key) { ... } ⇒ Object
Execute a block with a distributed lock. Uses Redis SET NX EX for lock acquisition and Lua script for atomic release.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/factorix/cache/redis.rb', line 185 def with_lock(key) lkey = lock_key(key) lock_value = SecureRandom.uuid deadline = Time.now + @lock_timeout until @redis.set(lkey, lock_value, nx: true, ex: LOCK_TTL) raise LockTimeoutError, "Failed to acquire lock for key: #{key}" if Time.now > deadline sleep 0.1 end logger.debug("Acquired lock", key:) begin yield ensure @redis.eval(RELEASE_LOCK_SCRIPT, keys: [lkey], argv: [lock_value]) logger.debug("Released lock", key:) end end |
#write_to(key, output) ⇒ Boolean
Write cached content to a file.
83 84 85 86 87 88 89 90 |
# File 'lib/factorix/cache/redis.rb', line 83 def write_to(key, output) data = @redis.get(data_key(key)) return false if data.nil? output.binwrite(data) logger.debug("Cache hit", key:) true end |