Class: Parse::Cache::ScopedView
- Inherits:
-
Object
- Object
- Parse::Cache::ScopedView
- Includes:
- MonetaSurface
- Defined in:
- lib/parse/cache/scoped_view.rb
Overview
An immutable, per-client view over one shared Redis backend.
Redis is a connection pool: sharing ONE backend across
several Parse::Client instances (one Redis, several Parse apps) is a
normal and supported deployment. What is not supported is sharing
ownership of a single keyspace binding, which is what the old
Parse::Cache::Redis#keyspace= setter allowed: client B calling
keyspace = ks_b on a backend client A already configured rebinds the
one @keyspace ivar out from under A. A's caching middleware had
already captured A's keyspace object and keeps writing/reading under
it, while clear, identity, roles, and the memoized
upstream_roles on the now-shared backend answer with B's keyspace
instead. A stops invalidating its own entries, or worse, a scoped
clear issued through A now deletes B's keys.
A ScopedView closes that hole by never mutating the backend at all.
Parse::Cache::Redis#scoped(keyspace) hands back one of these per
caller, each carrying its own keyspace and its own memoized
identity / roles / upstream_roles planes, all backed by the same
underlying connection pool:
backend = Parse::Cache::Redis.new(url: redis_url)
view_a = backend.scoped(keyspace_a)
view_b = backend.scoped(keyspace_b)
view_a and view_b share backend's pooled Redis connections but
can never see or clear each other's keys.
Moneta surface. Implements [], key?, delete, store (and
create / increment when the backend supports them) by delegating
straight to the backend, so a view is a drop-in replacement anywhere a
bare Parse::Cache::Redis was accepted: most importantly, the Faraday
caching middleware.
Locks are NOT scoped. lock_acquire / lock_release delegate to
the backend unchanged, still using the historical
parse-stack:foc:v1: prefix from Parse::CreateLock. During a
rolling deploy two workers must compute the SAME lock key regardless of
which client/keyspace they were configured with, or first_or_create!
silently loses cross-process mutual exclusion for the length of the
deploy.
No flush_db!. A whole-database flush is a connection-level
operation, not something a scoped view over one client's slice of the
keyspace should be able to trigger.
Constant Summary collapse
- DEFAULT_IDENTITY_TTL =
Safe defaults for callers that create a plane implicitly, most notably the webhook invalidation handlers. Keep these aligned with Parse::Authorization::Context's defaults: the first accessor call memoizes the plane and therefore fixes its generation lifetime.
3600- DEFAULT_ROLE_TTL =
30
Instance Attribute Summary collapse
-
#backend ⇒ Parse::Cache::Redis
readonly
The shared backend this view reads and writes through.
-
#keyspace ⇒ Parse::Cache::Keyspace
readonly
This view's key layout.
Instance Method Summary collapse
- #[](key) ⇒ Object
-
#clear(scope: nil, family: nil, tenant: nil) ⇒ self
Clear cached entries belonging to THIS view's keyspace, and nothing else: never anything from another view over the same backend.
- #delete(key, options = {}) ⇒ Object
-
#delete_matching(pattern) ⇒ Integer
Delete every key matching a glob pattern, refusing anything outside this view's own keyspace.
- #identity(ttl: DEFAULT_IDENTITY_TTL) ⇒ Parse::Cache::SubCache
-
#initialize(backend:, keyspace:) ⇒ ScopedView
constructor
A new instance of ScopedView.
- #inspect ⇒ Object
- #key?(key, options = {}) ⇒ Boolean
-
#load(key, options = {}) ⇒ Object
loadis the read primitive, delegated to the backend so the options argument reaches something that can honor it. - #lock_acquire(key, owner, ttl) ⇒ Object
- #lock_release(key, owner) ⇒ Object
- #roles(ttl: DEFAULT_ROLE_TTL) ⇒ Parse::Cache::SubCache
- #store(key, value, options = {}) ⇒ Object
-
#upstream_roles ⇒ Parse::Cache::UpstreamRoles?
Read-only consumer of Parse Server's own role cache, scoped to this view's app id and role plane.
Methods included from MonetaSurface
#[]=, #fetch, #fetch_values, #merge!, #slice, #values_at
Constructor Details
#initialize(backend:, keyspace:) ⇒ ScopedView
Returns a new instance of ScopedView.
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 106 107 108 109 110 111 112 113 |
# File 'lib/parse/cache/scoped_view.rb', line 81 def initialize(backend:, keyspace:) unless keyspace.is_a?(Parse::Cache::Keyspace) raise ArgumentError, "Parse::Cache::ScopedView keyspace must be a Parse::Cache::Keyspace; got #{keyspace.class}" end @backend = backend # The keyspace itself has no setters to begin with, but freezing it # here is a cheap extra guarantee that nothing downstream (this view # included) can mutate the layout this view was constructed with. @keyspace = keyspace.freeze # `create` and `increment` are only defined when the backend itself # supports them, so `respond_to?(:create)` on a view accurately # reflects what the backend can actually do rather than always # claiming support and raising NoMethodError on first use. if @backend.respond_to?(:create) define_singleton_method(:create) do |key, value, = {}| @backend.create(key, value, ) end end if @backend.respond_to?(:increment) define_singleton_method(:increment) do |key, amount = 1, = {}| @backend.increment(key, amount, ) end end # Value-preserving TTL. SubCache feature-detects this to avoid the # re-store that would lose a concurrent generation increment. if @backend.respond_to?(:expire) define_singleton_method(:expire) do |key, ttl| @backend.expire(key, ttl) end end end |
Instance Attribute Details
#backend ⇒ Parse::Cache::Redis (readonly)
Returns the shared backend this view reads and writes through. Exposed for introspection (e.g. tests asserting two views share one connection pool). The backend itself has no keyspace concept at all anymore (#scoped is the only way to associate a keyspace with it), so there is nothing on the backend left to rebind out from under this or any other view.
76 77 78 |
# File 'lib/parse/cache/scoped_view.rb', line 76 def backend @backend end |
#keyspace ⇒ Parse::Cache::Keyspace (readonly)
Returns this view's key layout. Fixed at construction; there is no setter, so a view can never be rebound to a different keyspace after the fact.
68 69 70 |
# File 'lib/parse/cache/scoped_view.rb', line 68 def keyspace @keyspace end |
Instance Method Details
#[](key) ⇒ Object
127 128 129 |
# File 'lib/parse/cache/scoped_view.rb', line 127 def [](key) load(key, {}) end |
#clear(scope: nil, family: nil, tenant: nil) ⇒ self
Clear cached entries belonging to THIS view's keyspace, and nothing else: never anything from another view over the same backend.
156 157 158 159 160 161 162 163 164 |
# File 'lib/parse/cache/scoped_view.rb', line 156 def clear(scope: nil, family: nil, tenant: nil) if scope prefix = @backend.send(:validate_scope!, scope) raw_delete_matching!("#{@keyspace.root_prefix}:#{prefix}:*") else raw_delete_matching!(@keyspace.pattern(family: family, tenant: tenant)) end self end |
#delete(key, options = {}) ⇒ Object
135 136 137 |
# File 'lib/parse/cache/scoped_view.rb', line 135 def delete(key, = {}) @backend.delete(key, || {}) end |
#delete_matching(pattern) ⇒ Integer
Delete every key matching a glob pattern, refusing anything outside this view's own keyspace.
The check requires the segment boundary (root_prefix followed by
:), not a bare string prefix: every pattern this keyspace actually
generates (Keyspace#pattern,
Keyspace#resource_pattern) is "<root_prefix>:...",
so this rejects nothing legitimate. A bare start_with?(root_prefix)
would accept a pattern belonging to a DIFFERENT namespace that merely
shares a prefix: a view whose namespace is "foo" would happily
delete_matching a pattern for namespace "foobar", since the string
"...:foobar:..." starts with "...:foo".
181 182 183 184 185 |
# File 'lib/parse/cache/scoped_view.rb', line 181 def delete_matching(pattern) return 0 if pattern.nil? || pattern.to_s.empty? return 0 unless pattern.to_s.start_with?("#{@keyspace.root_prefix}:") raw_delete_matching!(pattern) end |
#identity(ttl: DEFAULT_IDENTITY_TTL) ⇒ Parse::Cache::SubCache
195 196 197 |
# File 'lib/parse/cache/scoped_view.rb', line 195 def identity(ttl: DEFAULT_IDENTITY_TTL) @identity ||= Parse::Cache::SubCache.new(store: self, keyspace: @keyspace, family: :idn, ttl: ttl) end |
#inspect ⇒ Object
233 234 235 |
# File 'lib/parse/cache/scoped_view.rb', line 233 def inspect "#<Parse::Cache::ScopedView #{@keyspace.root_prefix}>" end |
#key?(key, options = {}) ⇒ Boolean
131 132 133 |
# File 'lib/parse/cache/scoped_view.rb', line 131 def key?(key, = {}) @backend.key?(key, || {}) end |
#load(key, options = {}) ⇒ Object
load is the read primitive, delegated to the backend so the options
argument reaches something that can honor it. Deriving it from []
here is what produced infinite recursion in an earlier version.
123 124 125 |
# File 'lib/parse/cache/scoped_view.rb', line 123 def load(key, = {}) @backend.load(key, || {}) end |
#lock_acquire(key, owner, ttl) ⇒ Object
224 225 226 |
# File 'lib/parse/cache/scoped_view.rb', line 224 def lock_acquire(key, owner, ttl) @backend.lock_acquire(key, owner, ttl) end |
#lock_release(key, owner) ⇒ Object
229 230 231 |
# File 'lib/parse/cache/scoped_view.rb', line 229 def lock_release(key, owner) @backend.lock_release(key, owner) end |
#roles(ttl: DEFAULT_ROLE_TTL) ⇒ Parse::Cache::SubCache
202 203 204 |
# File 'lib/parse/cache/scoped_view.rb', line 202 def roles(ttl: DEFAULT_ROLE_TTL) @roles ||= Parse::Cache::SubCache.new(store: self, keyspace: @keyspace, family: :role, ttl: ttl) end |
#store(key, value, options = {}) ⇒ Object
139 140 141 |
# File 'lib/parse/cache/scoped_view.rb', line 139 def store(key, value, = {}) @backend.store(key, value, || {}) end |
#upstream_roles ⇒ Parse::Cache::UpstreamRoles?
Read-only consumer of Parse Server's own role cache, scoped to this view's app id and role plane. See Redis#upstream_roles for the full rationale; the only difference here is that the app id and roles plane come from THIS view's keyspace, never a sibling view's.
212 213 214 215 216 217 218 219 |
# File 'lib/parse/cache/scoped_view.rb', line 212 def upstream_roles return nil if @backend.parse_cache_url.nil? @upstream_roles ||= Parse::Cache::UpstreamRoles.new( client: @backend.send(:upstream_client), app_id: @keyspace.app_id, roles_plane: roles, ) end |