Class: Parse::Cache::Redis

Inherits:
Object
  • Object
show all
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 key only if its current value equals expected. 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

Instance Method Summary collapse

Constructor Details

#initialize(url:, namespace: nil, pool_size: 5, pool_timeout: 5, **moneta_options) ⇒ Redis

Returns a new instance of Redis.

Parameters:

  • url (String)

    Redis URL (e.g. "redis://localhost:6379/0").

  • namespace (String, nil) (defaults to: nil)

    optional key prefix so multiple Parse apps can share one Redis without colliding. When non-nil, the namespace is automatically forwarded to the caching middleware as cache_namespace:.

  • pool_size (Integer) (defaults to: 5)

    number of pooled Moneta-Redis stores. Defaults to 5 (the Puma default thread count).

    Sizing math (per Faraday request):

    • cache hit: key? + [] = 2 checkouts
    • GET miss + successful store: key? + 3 variant deletes (anonymous + master-key sibling + final key) + 1 store in on_complete = up to 5 checkouts
    • non-GET write (POST/PUT/DELETE): 3 variant deletes = 3 checkouts

    The worst case (5) is on the write-through-after-miss path, not the hit path. Rule of thumb: start at pool_size = RAILS_MAX_THREADS, then bump it up if you observe ConnectionPool::TimeoutError in parse.cache.error notifications (the middleware swallows that error into a passthrough request rather than raising to the caller).

  • pool_timeout (Numeric) (defaults to: 5)

    seconds to wait for a backend checkout before raising ConnectionPool::TimeoutError. Defaults to 5s. The caching middleware catches that error and falls back to a passthrough request rather than raising to the caller.

  • moneta_options (Hash)

    extra options passed through to Moneta.new(:Redis, ...) (e.g. :db, :connect_timeout). expires: true is set automatically so per-key TTLs supplied by the caching middleware (the :expires Faraday option) are honored by Redis. Pass expires: false here to opt out — but note that doing so causes cached responses to live forever, which is rarely what you want for a session-token-scoped response cache.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/parse/cache/redis.rb', line 73

def initialize(url:, namespace: nil, pool_size: 5, pool_timeout: 5, **moneta_options)
  @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.
  merged_options = { expires: true }.merge(moneta_options)
  @moneta_options = merged_options
  @closed = false
  @pool = Pool.new(size: pool_size, timeout: pool_timeout) do
    Moneta.new(:Redis, { url: url }.merge(merged_options))
  end
end

Instance Attribute Details

#namespaceString? (readonly)

Returns cache key namespace prefix (or nil if not set).

Returns:

  • (String, nil)

    cache key namespace prefix (or nil if not set).



32
33
34
# File 'lib/parse/cache/redis.rb', line 32

def namespace
  @namespace
end

#pool_sizeInteger (readonly)

Returns pool size.

Returns:

  • (Integer)

    pool size.



35
36
37
# File 'lib/parse/cache/redis.rb', line 35

def pool_size
  @pool_size
end

#urlString (readonly)

Returns Redis connection URL.

Returns:

  • (String)

    Redis connection URL.



38
39
40
# File 'lib/parse/cache/redis.rb', line 38

def url
  @url
end

Instance Method Details

#[](key) ⇒ Object



92
93
94
# File 'lib/parse/cache/redis.rb', line 92

def [](key)
  @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!.

Parameters:

  • scope (String, nil) (defaults to: nil)

    explicit namespace prefix to scan-delete. When provided, overrides the wrapper's configured @namespace and SCAN-deletes <scope>:* regardless of how the wrapper was built. This is the safe escape hatch for tenants that share a non- namespaced wrapper but still want to evict only their own keys without FLUSHDB-ing siblings (and without wiping parse-stack:foc:v1:* create-lock keys that live on the same DB). The scope must be a non-empty String; the trailing : is added automatically and any trailing : in the input is stripped so "tenant_x" and "tenant_x:" are equivalent.



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/parse/cache/redis.rb', line 195

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

#closeObject

Close all pooled connections. Safe to call multiple times.



217
218
219
220
221
# File 'lib/parse/cache/redis.rb', line 217

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.



112
113
114
# File 'lib/parse/cache/redis.rb', line 112

def create(key, value, options = {})
  @pool.create(key, value, options)
end

#delete(key) ⇒ Object



100
101
102
# File 'lib/parse/cache/redis.rb', line 100

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.



211
212
213
214
# File 'lib/parse/cache/redis.rb', line 211

def flush_db!
  @pool.clear
  self
end

#increment(key, amount = 1, options = {}) ⇒ Object

Atomic counter increment. Forwarded for Moneta surface parity.



117
118
119
# File 'lib/parse/cache/redis.rb', line 117

def increment(key, amount = 1, options = {})
  @pool.increment(key, amount, options)
end

#key?(key) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/parse/cache/redis.rb', line 96

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 createMoneta.new(:Redis) marshals BOTH keys and values, so a raw-Redis compare-and-delete on the marshaled 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 between the Moneta-encoded and raw-encoded paths.

Parameters:

  • key (String)

    plain-string lock key.

  • owner (String)

    unique-per-acquisition owner token.

  • ttl (Integer)

    seconds until the key self-clears.

Returns:

  • (Boolean)

    true when the key was set (lock acquired).



151
152
153
154
155
156
157
# File 'lib/parse/cache/redis.rb', line 151

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.

Parameters:

Returns:

  • (Boolean)

    true when this owner's key was deleted.



167
168
169
170
171
172
# File 'lib/parse/cache/redis.rb', line 167

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



104
105
106
# File 'lib/parse/cache/redis.rb', line 104

def store(key, value, options = {})
  @pool.store(key, value, options)
end