Class: Parse::Cache::Redis
- Inherits:
-
Object
- Object
- Parse::Cache::Redis
- Defined in:
- lib/parse/cache/redis.rb
Overview
Ergonomic Redis cache builder for Parse Stack. Composes a
ConnectionPool of Moneta-Redis stores and carries an optional
namespace that Parse::Client will pick up automatically — there
is no need to also pass cache_namespace: to Parse.setup when
using this wrapper.
Usage: Parse.setup( cache: Parse::Cache::Redis.new( url: "redis://localhost:6379/0", namespace: "app_x", pool_size: 10, ), expires: 60, ... )
The instance is a Moneta-compatible store (it delegates the four
methods the Faraday caching middleware uses — [], key?,
delete, store — to a pooled backend), so it can be passed
directly to Parse.setup(cache:) / Parse::Client.new(cache:).
Constant Summary collapse
- LOCK_RELEASE_SCRIPT =
Lua compare-and-delete: delete
keyonly if its current value equalsexpected. Atomic on the Redis server (the GET, the compare, and the DEL are one script invocation), which closes the check-then-delete race in a naive GET-then-DEL release where the lease can expire and be re-acquired by another holder between the two commands. <<~LUA if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end LUA
Instance Attribute Summary collapse
-
#namespace ⇒ String?
readonly
Cache key namespace prefix (or nil if not set).
-
#pool_size ⇒ Integer
readonly
Pool size.
-
#url ⇒ String
readonly
Redis connection URL.
Instance Method Summary collapse
- #[](key) ⇒ Object
-
#clear(scope: nil) ⇒ Object
Clear cached entries belonging to this wrapper.
-
#close ⇒ Object
Close all pooled connections.
-
#create(key, value, options = {}) ⇒ Object
Atomic SETNX.
- #delete(key) ⇒ Object
-
#flush_db! ⇒ Object
Issue
FLUSHDBon the backing Redis DB, regardless of whether a namespace is configured. -
#increment(key, amount = 1, options = {}) ⇒ Object
Atomic counter increment.
-
#initialize(url:, namespace: nil, pool_size: 5, pool_timeout: 5, **moneta_options) ⇒ Redis
constructor
A new instance of Redis.
- #key?(key) ⇒ Boolean
-
#lock_acquire(key, owner, ttl) ⇒ Boolean
Atomically acquire a lock: SET key=owner only if absent, with a native expiry.
-
#lock_release(key, owner) ⇒ Boolean
Atomically release a lock via compare-and-delete.
- #store(key, value, options = {}) ⇒ Object
Constructor Details
#initialize(url:, namespace: nil, pool_size: 5, pool_timeout: 5, **moneta_options) ⇒ Redis
Returns a new instance of Redis.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/parse/cache/redis.rb', line 74 def initialize(url:, namespace: nil, pool_size: 5, pool_timeout: 5, **) @url = url @namespace = normalize_namespace(namespace) @pool_size = pool_size @pool_timeout = pool_timeout # Default expires: true so per-call `expires:` (the TTL the # Faraday caching middleware passes on store) is honored. The # Moneta-Redis adapter ignores per-call expires unless the # store was constructed with this flag. Without it, cached # session-scoped REST responses outlive their token's # validity. Callers can still pass `expires: false` to opt out. = { expires: true }.merge() # SECURITY: disable Moneta's value serializer so cached values are NOT # Marshal-encoded. We JSON-(de)serialize values ourselves in #store / # #[] (see #encode_value / #decode_value). The default Moneta-Redis # value serializer is Marshal, which would `Marshal.load` whatever # bytes come back from Redis on every cache hit — an arbitrary-code- # execution primitive if the Redis cache is shared, unauthenticated, # or reachable through a plaintext `redis://` MITM. Forcing nil here # (overriding any caller-supplied `value_serializer:`/`serializer:`) # keeps that gadget-deserialization vector closed regardless of how # the wrapper is configured. Keys keep the default (:marshal) encoding: # they are only ever written and SCAN/DEL-compared as opaque strings, # never `Marshal.load`ed from Redis content, so they are not a # deserialization vector. = .merge(value_serializer: nil) @moneta_options = @closed = false @pool = Pool.new(size: pool_size, timeout: pool_timeout) do Moneta.new(:Redis, { url: url }.merge()) end end |
Instance Attribute Details
#namespace ⇒ String? (readonly)
Returns cache key namespace prefix (or nil if not set).
33 34 35 |
# File 'lib/parse/cache/redis.rb', line 33 def namespace @namespace end |
#pool_size ⇒ Integer (readonly)
Returns pool size.
36 37 38 |
# File 'lib/parse/cache/redis.rb', line 36 def pool_size @pool_size end |
#url ⇒ String (readonly)
Returns Redis connection URL.
39 40 41 |
# File 'lib/parse/cache/redis.rb', line 39 def url @url end |
Instance Method Details
#[](key) ⇒ Object
107 108 109 |
# File 'lib/parse/cache/redis.rb', line 107 def [](key) decode_value(@pool[key]) end |
#clear(scope: nil) ⇒ Object
Clear cached entries belonging to this wrapper. Required for
Parse::Client#clear_cache! compatibility.
Namespace-scoped when a namespace is set: the wrapper walks
<namespace>:* via Redis SCAN and DELs the matching keys,
leaving other tenants on the same DB untouched. When no
namespace is configured the wrapper falls back to FLUSHDB on
the backing DB — same blast radius as previous versions, but
only for unnamespaced deployments. To opt into the wide
FLUSHDB explicitly (e.g. ops tooling), call #flush_db!.
213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/parse/cache/redis.rb', line 213 def clear(scope: nil) if scope prefix = validate_scope!(scope) delete_keys_matching!("#{prefix}:*") elsif @namespace delete_keys_matching!("#{@namespace}:*") else @pool.clear end self end |
#close ⇒ Object
Close all pooled connections. Safe to call multiple times.
235 236 237 238 239 |
# File 'lib/parse/cache/redis.rb', line 235 def close return if @closed @closed = true @pool.close end |
#create(key, value, options = {}) ⇒ Object
Atomic SETNX. Required so Parse::CreateLock can acquire
cross-process locks when this wrapper is the configured cache /
synchronize_create_store. Returns true only when the key did
not already exist. The value goes through the same JSON encoding
as #store so a later #[] read round-trips instead of decoding
to nil. (Parse::LockBackend never hits this path on this wrapper —
it prefers the raw-Redis #lock_acquire/#lock_release pair.)
130 131 132 |
# File 'lib/parse/cache/redis.rb', line 130 def create(key, value, = {}) @pool.create(key, encode_value(value), ) end |
#delete(key) ⇒ Object
115 116 117 |
# File 'lib/parse/cache/redis.rb', line 115 def delete(key) @pool.delete(key) end |
#flush_db! ⇒ Object
Issue FLUSHDB on the backing Redis DB, regardless of whether a
namespace is configured. Evicts every key on the selected DB,
including unrelated tenants — use only for ops tooling that
owns the whole DB.
229 230 231 232 |
# File 'lib/parse/cache/redis.rb', line 229 def flush_db! @pool.clear self end |
#increment(key, amount = 1, options = {}) ⇒ Object
Atomic counter increment. Forwarded for Moneta surface parity.
135 136 137 |
# File 'lib/parse/cache/redis.rb', line 135 def increment(key, amount = 1, = {}) @pool.increment(key, amount, ) end |
#key?(key) ⇒ Boolean
111 112 113 |
# File 'lib/parse/cache/redis.rb', line 111 def key?(key) @pool.key?(key) end |
#lock_acquire(key, owner, ttl) ⇒ Boolean
Atomically acquire a lock: SET key=owner only if absent, with a
native expiry. Used by LockBackend for Lock and
Parse::CreateLock. Deliberately bypasses Moneta's create —
Moneta.new(:Redis) marshals keys (and, by default, values), so a
raw-Redis compare-and-delete on a Moneta-encoded blob would be
fragile and coupled to Moneta's serializer config. Routing acquire
AND release through plain-string raw Redis here keeps one consistent
encoding across both ends of the lock and makes the keys human-
inspectable in Redis (parse-stack:lock:v1:<digest>). Lock keys are
short-lived (TTL ≤ 30s) so there is no migration concern when a
deploy flips encodings.
169 170 171 172 173 174 175 |
# File 'lib/parse/cache/redis.rb', line 169 def lock_acquire(key, owner, ttl) @pool.pool.with do |store| redis = backend_client(store) # redis-rb returns "OK" on success, nil when NX fails. !!redis.set(key, owner, nx: true, ex: ttl) end end |
#lock_release(key, owner) ⇒ Boolean
Atomically release a lock via compare-and-delete. Only the holder
whose owner token still matches the stored value deletes the
key — a holder whose lease already expired and was re-acquired by
someone else is a no-op, never a cross-holder delete.
185 186 187 188 189 190 |
# File 'lib/parse/cache/redis.rb', line 185 def lock_release(key, owner) @pool.pool.with do |store| redis = backend_client(store) redis.eval(LOCK_RELEASE_SCRIPT, keys: [key], argv: [owner]).to_i == 1 end end |
#store(key, value, options = {}) ⇒ Object
119 120 121 |
# File 'lib/parse/cache/redis.rb', line 119 def store(key, value, = {}) @pool.store(key, encode_value(value), ) end |