Class: Parse::Cache::SubCache
- Inherits:
-
Object
- Object
- Parse::Cache::SubCache
- Defined in:
- lib/parse/cache/sub_cache.rb
Overview
A named plane within a Keyspace, exposing the
get / set / invalidate contract that Parse::AtlasSearch's
session_cache= and role_cache= slots accept.
The response cache speaks Moneta ([], key?, delete, store) because
that is what the Faraday middleware requires. Identity and role resolution
speak a different, smaller contract. Rather than flattening both onto one
object, where get and [] would sit side by side and clear would be
ambiguous about which plane it cleared, each plane is its own object over
the same connection. This mirrors Parse Server's own
CacheController / SubCache split.
Constant Summary collapse
- GENERATION_TTL_FACTOR =
Generation keys live this many times longer than the longest entry they guard. Any factor above 1 is sufficient; 2 leaves margin for clock skew and for an entry written moments before a bump.
2
Instance Attribute Summary collapse
-
#family ⇒ Symbol
readonly
The keyspace family this plane writes.
Instance Method Summary collapse
-
#bump_generation(subject) ⇒ Integer
Monotonic per-subject generation, used to invalidate entries that cannot be named.
-
#clear ⇒ Integer
Evict every entry in this plane, and nothing outside it.
-
#epoch ⇒ Float
The last invalidation epoch, 0.0 when never set.
-
#fresh_since_epoch?(written_at) ⇒ Boolean
Whether an entry written at
written_atpredates the last invalidation and must therefore be treated as a miss. -
#generation(subject) ⇒ Integer
Current generation, 0 when never bumped.
-
#generation_current?(subject, gen) ⇒ Boolean
Whether a value carrying
genis still current forsubject. -
#generation_ttl ⇒ Integer?
How long a generation key lives, or nil when this plane has no default TTL and generations must therefore be permanent.
-
#get(key) ⇒ Object?
The stored value, or nil on a miss.
-
#initialize(store:, keyspace:, family:, ttl: nil, digest_keys: nil) ⇒ SubCache
constructor
A new instance of SubCache.
- #invalidate(key) ⇒ Object
- #set(key, value, ttl: nil) ⇒ Object
-
#touch_epoch(at = Time.now.to_f) ⇒ Float
Record that this plane was invalidated at
at.
Constructor Details
#initialize(store:, keyspace:, family:, ttl: nil, digest_keys: nil) ⇒ SubCache
Returns a new instance of SubCache.
41 42 43 44 45 46 47 |
# File 'lib/parse/cache/sub_cache.rb', line 41 def initialize(store:, keyspace:, family:, ttl: nil, digest_keys: nil) @store = store @keyspace = keyspace @family = family.to_sym @ttl = ttl @digest_keys = digest_keys.nil? ? @family == :idn : digest_keys end |
Instance Attribute Details
#family ⇒ Symbol (readonly)
Returns the keyspace family this plane writes.
22 23 24 |
# File 'lib/parse/cache/sub_cache.rb', line 22 def family @family end |
Instance Method Details
#bump_generation(subject) ⇒ Integer
Monotonic per-subject generation, used to invalidate entries that cannot be named.
A _User write tells us a user id, but identity entries are keyed by
session token and there is no reverse map, so the entries belonging to
that user cannot be enumerated. Bumping a generation the reader checks
invalidates all of them in O(1), including tokens this process has never
seen, without a master-key _Session query.
Generation keys expire, and their TTL must exceed the longest-lived
entry they guard. One key per user id, written on every _User
webhook, is unbounded growth on a public signup flow: every account
that ever saves leaves a permanent key behind.
The TTL cannot be chosen freely, because expiry resets the counter to 0 and 0 is also the value for a user who has never been bumped. If a generation expired while an entry written at generation 0 were still alive, that entry would compare current again and come back from the dead after having been invalidated. #generation_ttl is therefore GENERATION_TTL_FACTOR times the plane's entry TTL, so every entry predating a bump has expired on its own before the counter can reset. #set clamps per-call TTLs to keep that invariant true.
A plane with no default TTL keeps permanent generations: entries there never expire, so no counter lifetime is safe.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/parse/cache/sub_cache.rb', line 116 def bump_generation(subject) return 0 if subject.nil? key = generation_key(subject) ttl = generation_ttl # Prefer an atomic INCR. A read-then-write loses a concurrent bump, and # a lost bump means an entry that should have been invalidated stays # readable, which is the permissive direction. if @store.respond_to?(:increment) value = @store.increment(key).to_i # INCR does not set an expiry, and re-applying it on each bump is # what keeps an actively-bumped user's counter alive. refresh_generation_expiry(key, value, ttl) value else current = @store[key].to_i @store.store(key, current + 1, ttl ? { expires: ttl } : {}) current + 1 end end |
#clear ⇒ Integer
Evict every entry in this plane, and nothing outside it.
83 84 85 86 |
# File 'lib/parse/cache/sub_cache.rb', line 83 def clear return 0 unless @store.respond_to?(:delete_matching) @store.delete_matching(@keyspace.pattern(family: @family)) end |
#epoch ⇒ Float
Returns the last invalidation epoch, 0.0 when never set.
167 168 169 |
# File 'lib/parse/cache/sub_cache.rb', line 167 def epoch @store[epoch_key].to_f end |
#fresh_since_epoch?(written_at) ⇒ Boolean
Whether an entry written at written_at predates the last invalidation
and must therefore be treated as a miss.
176 177 178 179 |
# File 'lib/parse/cache/sub_cache.rb', line 176 def fresh_since_epoch?(written_at) return false if written_at.nil? written_at.to_f >= epoch end |
#generation(subject) ⇒ Integer
Returns current generation, 0 when never bumped.
138 139 140 141 |
# File 'lib/parse/cache/sub_cache.rb', line 138 def generation(subject) return 0 if subject.nil? @store[generation_key(subject)].to_i end |
#generation_current?(subject, gen) ⇒ Boolean
Whether a value carrying gen is still current for subject.
145 146 147 |
# File 'lib/parse/cache/sub_cache.rb', line 145 def generation_current?(subject, gen) generation(subject).to_i == gen.to_i end |
#generation_ttl ⇒ Integer?
Returns how long a generation key lives, or nil when this plane has no default TTL and generations must therefore be permanent. See #bump_generation for why the two are tied.
52 53 54 55 |
# File 'lib/parse/cache/sub_cache.rb', line 52 def generation_ttl return nil if @ttl.nil? (@ttl * GENERATION_TTL_FACTOR).ceil end |
#get(key) ⇒ Object?
Returns the stored value, or nil on a miss.
59 60 61 62 |
# File 'lib/parse/cache/sub_cache.rb', line 59 def get(key) return nil if key.nil? decode(@store[@keyspace.key(@family, logical_key(key))]) end |
#invalidate(key) ⇒ Object
76 77 78 79 |
# File 'lib/parse/cache/sub_cache.rb', line 76 def invalidate(key) return if key.nil? @store.delete(@keyspace.key(@family, logical_key(key))) end |
#set(key, value, ttl: nil) ⇒ Object
67 68 69 70 71 72 73 |
# File 'lib/parse/cache/sub_cache.rb', line 67 def set(key, value, ttl: nil) return value if key.nil? effective = clamp_ttl(ttl || @ttl) = effective ? { expires: effective } : {} @store.store(@keyspace.key(@family, logical_key(key)), encode(value), ) value end |
#touch_epoch(at = Time.now.to_f) ⇒ Float
Record that this plane was invalidated at at. Unlike a generation,
which answers "is this exact subject stale", the epoch answers "is an
entry written before now stale", which is what is needed to reject a
foreign cache entry whose write time can only be derived from its
remaining TTL.
157 158 159 160 161 162 163 164 |
# File 'lib/parse/cache/sub_cache.rb', line 157 def touch_epoch(at = Time.now.to_f) current = epoch # Never move the epoch backwards: clock skew between workers would # otherwise re-admit entries a previous invalidation had rejected. value = at > current ? at : current @store.store(epoch_key, value, {}) value end |