Class: Parse::Cache::Keyspace
- Inherits:
-
Object
- Object
- Parse::Cache::Keyspace
- Defined in:
- lib/parse/cache/keyspace.rb
Overview
Owns the physical layout of every key this SDK writes to a shared cache backend, and the patterns used to delete them again.
A single object generates keys and the patterns that clear them, so the
two can never disagree. Previously the caching middleware composed keys
from a cache_namespace: it held privately while Parse::Cache::Redis
held a separate namespace, and Parse::Client#clear_cache! cleared
using only the latter. A client namespaced at the middleware but not at
the wrapper would therefore delete every SDK key on the database rather
than its own.
Layout:
parse-stack:<version>:<app_scope>[:<namespace>]:<family>[:T:<tenant>]:<rest>
Every clear is a strict prefix of this, so narrowing the scope can only ever delete a subset:
all of this client parse-stack:v1:<app_scope>[:<ns>]:*
one family parse-stack:v1:<app_scope>[:<ns>]:<family>:*
one tenant parse-stack:v1:<app_scope>[:<ns>]:<family>:T:<tenant>:*
app_scope is a digest of the Parse application id and server URL rather
than the raw values. Two apps sharing one Redis with no namespace
configured would otherwise collide, and a raw application id could carry
glob metacharacters (*, [) that would silently widen a SCAN pattern.
The digest is fixed-length and glob-safe by construction.
Create-locks are deliberately NOT part of this layout. They keep the
historical parse-stack:foc:v1: prefix from Model::CreateLock.
Moving them would mean that during a rolling deploy two workers compute
different lock keys, stop contending on the same key, and silently lose
mutual exclusion for the length of the deploy.
Constant Summary collapse
- ROOT =
Root segment for every key this SDK owns.
"parse-stack"- VERSION =
Layout version. Bump only for a breaking key-shape change, which orphans every existing entry and therefore needs a migration note.
"v1"- NO_NAMESPACE =
Occupies the namespace position when no namespace is configured. Chosen because #normalize_segment rejects it as caller input, so a caller can never collide with the unnamespaced keyspace by naming their namespace after the sentinel.
"_"- FAMILIES =
Key families.
cacheis the Faraday response cache,idnis session-token identity,roleis role closures. %i[cache idn role].freeze
- SCOPE_DIGEST_LENGTH =
Length of the truncated app/server digest. 12 hex characters is 48 bits, which is ample for distinguishing apps on one database while keeping keys readable.
12- GLOB_METACHARS =
Characters that carry meaning in a Redis glob pattern. A namespace or tenant containing one of these could widen a SCAN pattern beyond its intended scope, so they are rejected at construction rather than escaped, since a caller passing one is a configuration bug.
:is included deliberately alongside the glob metacharacters. It is the segment separator, so a namespace offoo:barwould forge an extra segment and make the pattern forfooalso matchfoo:bar, which is the same subset violation the sentinel above prevents. /[\*\?\[\]\\\x00:]/.freeze
- AUTH_ANON =
Auth discriminator for an anonymous (unauthenticated) request.
"anon"- AUTH_MASTER =
Auth discriminator for a master-key request.
"mk"- TOKEN_DIGEST_RE =
A session-token discriminator must look like the truncated SHA-256 the caching middleware produces. Anything else is a caller bug, and a raw token must never reach a key.
/\A[0-9a-f]{16,64}\z/.freeze
Instance Attribute Summary collapse
-
#app_id ⇒ String?
readonly
The raw Parse application id.
-
#app_scope ⇒ String
readonly
Digest identifying the Parse app and server.
-
#namespace ⇒ String?
readonly
Validated namespace, or nil when unset.
-
#version ⇒ String
readonly
Layout version segment.
Class Method Summary collapse
-
.digest_scope(app_id, server_url) ⇒ String
Digest of the app id and server URL.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Whether two keyspaces address the same key space.
-
#cache_key(url, auth:, tenant: nil) ⇒ String
Build a response-cache key.
-
#family_prefix(family) ⇒ String
Prefix for one family.
- #hash ⇒ Object
-
#initialize(app_id: nil, server_url: nil, namespace: nil, version: VERSION) ⇒ Keyspace
constructor
A new instance of Keyspace.
- #inspect ⇒ Object
-
#key(family, *segments, tenant: nil) ⇒ String
Build a key for the
idnorrolefamilies. -
#pattern(family: nil, tenant: nil) ⇒ String
Glob pattern selecting keys to clear.
-
#resource_pattern(url, tenant: nil) ⇒ String
Pattern matching every auth variant of one resource.
-
#root_prefix ⇒ String
Prefix shared by every key this keyspace owns, across all families.
- #to_s ⇒ Object
Constructor Details
#initialize(app_id: nil, server_url: nil, namespace: nil, version: VERSION) ⇒ Keyspace
Returns a new instance of Keyspace.
94 95 96 97 98 99 |
# File 'lib/parse/cache/keyspace.rb', line 94 def initialize(app_id: nil, server_url: nil, namespace: nil, version: VERSION) @app_id = app_id&.to_s @app_scope = self.class.digest_scope(app_id, server_url) @namespace = normalize_segment(namespace, "namespace") @version = version.to_s end |
Instance Attribute Details
#app_id ⇒ String? (readonly)
Returns the raw Parse application id. Retained alongside
the digest because Parse Server's own cache keys use it verbatim
(<appId>:role:<userId>), so an attached read needs the original
even though our own layout uses the digest.
81 82 83 |
# File 'lib/parse/cache/keyspace.rb', line 81 def app_id @app_id end |
#app_scope ⇒ String (readonly)
Returns digest identifying the Parse app and server.
75 76 77 |
# File 'lib/parse/cache/keyspace.rb', line 75 def app_scope @app_scope end |
#namespace ⇒ String? (readonly)
Returns validated namespace, or nil when unset.
84 85 86 |
# File 'lib/parse/cache/keyspace.rb', line 84 def namespace @namespace end |
#version ⇒ String (readonly)
Returns layout version segment.
87 88 89 |
# File 'lib/parse/cache/keyspace.rb', line 87 def version @version end |
Class Method Details
.digest_scope(app_id, server_url) ⇒ String
Digest of the app id and server URL. Public so callers can compare two keyspaces for equivalence without reaching into internals.
105 106 107 108 |
# File 'lib/parse/cache/keyspace.rb', line 105 def self.digest_scope(app_id, server_url) material = "#{app_id}\x00#{server_url}" Digest::SHA256.hexdigest(material)[0, SCOPE_DIGEST_LENGTH] end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Whether two keyspaces address the same key space. Used to detect a client reconfigured with a different namespace or app.
212 213 214 |
# File 'lib/parse/cache/keyspace.rb', line 212 def ==(other) other.is_a?(Keyspace) && other.root_prefix == root_prefix end |
#cache_key(url, auth:, tenant: nil) ⇒ String
Build a response-cache key.
auth: is mandatory and has no default. A master-key request bypasses
ACL, CLP and protectedFields, so the same URL returns a strictly
fuller body than a session-token request, and two different sessions can
differ from each other through protectedFields entity rules and row
ACLs. Collapsing those into one key would serve privileged fields to an
unprivileged caller out of the cache, so the key cannot be built without
stating which auth produced the body.
The URL is digested and placed before the discriminator so that every
auth variant of one resource shares a prefix, which is what makes
#resource_pattern able to invalidate a write for all callers rather
than only the three variants the old delete_cache_variants could name.
174 175 176 |
# File 'lib/parse/cache/keyspace.rb', line 174 def cache_key(url, auth:, tenant: nil) "#{resource_prefix(url, tenant: tenant)}:#{normalize_auth(auth)}" end |
#family_prefix(family) ⇒ String
Prefix for one family.
125 126 127 |
# File 'lib/parse/cache/keyspace.rb', line 125 def family_prefix(family) "#{root_prefix}:#{assert_family!(family)}" end |
#hash ⇒ Object
218 219 220 |
# File 'lib/parse/cache/keyspace.rb', line 218 def hash root_prefix.hash end |
#inspect ⇒ Object
226 227 228 |
# File 'lib/parse/cache/keyspace.rb', line 226 def inspect "#<Parse::Cache::Keyspace #{root_prefix}>" end |
#key(family, *segments, tenant: nil) ⇒ String
Build a key for the idn or role families.
Neither takes an auth discriminator: a role key is keyed by user id and an identity key by session token, so the key already is the auth identity. Only the response cache has one URL yielding different bodies to different callers, and it uses #cache_key.
143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/parse/cache/keyspace.rb', line 143 def key(family, *segments, tenant: nil) if family.to_sym == :cache raise ArgumentError, "Parse::Cache::Keyspace: use #cache_key for the cache family, " \ "so the auth discriminator cannot be omitted" end parts = [family_prefix(family)] parts << "T:#{normalize_segment(tenant, "tenant")}" unless tenant.nil? parts.concat(segments.map(&:to_s)) parts.join(":") end |
#pattern(family: nil, tenant: nil) ⇒ String
Glob pattern selecting keys to clear. With no arguments it selects every key this keyspace owns and nothing else.
197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/parse/cache/keyspace.rb', line 197 def pattern(family: nil, tenant: nil) if tenant && family.nil? raise ArgumentError, "Parse::Cache::Keyspace#pattern requires a family: when a tenant: is given" end return "#{root_prefix}:*" if family.nil? prefix = family_prefix(family) return "#{prefix}:*" if tenant.nil? "#{prefix}:T:#{normalize_segment(tenant, "tenant")}:*" end |
#resource_pattern(url, tenant: nil) ⇒ String
Pattern matching every auth variant of one resource. Used to invalidate a resource on write for all callers, including sessions this process has never seen.
185 186 187 |
# File 'lib/parse/cache/keyspace.rb', line 185 def resource_pattern(url, tenant: nil) "#{resource_prefix(url, tenant: tenant)}:*" end |
#root_prefix ⇒ String
Prefix shared by every key this keyspace owns, across all families.
112 113 114 115 116 117 118 119 |
# File 'lib/parse/cache/keyspace.rb', line 112 def root_prefix # The namespace segment is ALWAYS emitted, using NO_NAMESPACE when unset. # Without it an unnamespaced root is a strict prefix of every namespaced # root for the same app, so `<root>:*` would also delete every named # namespace's keys. Narrowing must only ever delete a subset, and a # sentinel is the only way to keep the segment count fixed. [ROOT, @version, @app_scope, @namespace || NO_NAMESPACE].join(":") end |
#to_s ⇒ Object
222 223 224 |
# File 'lib/parse/cache/keyspace.rb', line 222 def to_s root_prefix end |