Module: Parse::LockBackend Private
- Defined in:
- lib/parse/lock_backend.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
Shared low-level primitives for both CreateLock (the
internal lock used by first_or_create! / create_or_update!)
and Lock (the public mutual-exclusion primitive). The
extraction exists so Lock does not reach into
CreateLock's private methods via .send — a brittle
coupling pattern called out in the v5.1.0 round-2 review. Both
callers now depend on a small documented surface and any future
refactor of the lock store discovery / degraded-detection
heuristic / atomic-SETNX semantics happens in exactly one place.
Not a public API for application code. @!visibility private
is intentional. End users compose with locking through
Parse::Lock.acquire (block-form) or first_or_create! /
create_or_update! (find-or-create). This module is documented
only because SDK extension authors and security auditors need to
know where the SETNX semantics actually live.
Constant Summary collapse
- DEFAULT_POLL_BASE =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Base poll interval for the wait-loop spin in the caller's acquire loop. Caller adds jitter via poll_interval; this constant is the midpoint.
0.05- DEFAULT_POLL_JITTER =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Half-width of the symmetric jitter applied around DEFAULT_POLL_BASE. Spreads contended-acquire spin starts so N waiters don't all hit
try_acquireon the same monotonic tick after a release. 0.015- DEGRADED_WARNING_THROTTLE_SECONDS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Throttle floor for handle_degraded(
:warn_throttled). One warning per process per this many seconds; subsequent degraded acquisitions are silent. 60
Class Method Summary collapse
-
.auto_secret ⇒ String
private
Per-process random secret.
-
.configured_secret ⇒ String?
private
Operator-configured HMAC secret from
Parse.synchronize_create_secretorPARSE_STACK_LOCK_SECRET. -
.degraded_store?(store) ⇒ Boolean
private
Decide whether
storeis process-local (Memory / Null / missing-:create/ nil) — i.e. -
.handle_degraded(mode, key, source: "Parse::LockBackend", unavailable_error: nil) ⇒ Object
private
Emit the configured degraded-store warning.
-
.lock_secret_for(store:, source: "Parse::LockBackend") ⇒ String?
private
Resolve the HMAC secret for lock-key derivation.
-
.lock_store ⇒ Object?
private
Find the Moneta store the lock should write through.
-
.monotonic_now ⇒ Float
private
CLOCK_MONOTONIC seconds.
-
.poll_interval ⇒ Float
private
Jittered poll interval for the wait-loop.
-
.process_mutex(key) ⇒ Mutex
private
Per-key in-process Mutex registry for the degraded fallback path.
-
.release(store, key, owner) ⇒ Object
private
Compare-and-delete release.
-
.reset!
private
Reset backend-owned state.
-
.try_acquire(store, key, owner, ttl) ⇒ Boolean
private
Atomic SETNX-style acquisition.
-
.warn_plain_sha_once(source:) ⇒ Object
private
One-time process-scoped warn when a cross-process lock store is in use without an operator-configured HMAC secret.
Class Method Details
.auto_secret ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns per-process random secret. Memoized.
271 272 273 |
# File 'lib/parse/lock_backend.rb', line 271 def auto_secret @auto_secret ||= SecureRandom.hex(32) end |
.configured_secret ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns operator-configured HMAC secret from
Parse.synchronize_create_secret or
PARSE_STACK_LOCK_SECRET. The env-var and accessor names
carry "synchronize_create" / "LOCK" historical naming;
both Parse::CreateLock and Parse::Lock consume the same
value.
263 264 265 266 267 268 |
# File 'lib/parse/lock_backend.rb', line 263 def configured_secret if Parse.respond_to?(:synchronize_create_secret) && Parse.synchronize_create_secret return Parse.synchronize_create_secret.to_s end ENV["PARSE_STACK_LOCK_SECRET"] end |
.degraded_store?(store) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Decide whether store is process-local (Memory / Null /
missing-:create / nil) — i.e. cannot serve as a cross-
process lock store, so the caller should fall back to a
per-key in-process Mutex. The Cache::Redis wrapper
is explicitly accepted because it doesn't expose a Moneta
.adapter chain to walk.
69 70 71 72 73 74 75 76 77 |
# File 'lib/parse/lock_backend.rb', line 69 def degraded_store?(store) return true if store.nil? return false if defined?(Parse::Cache::Redis) && store.is_a?(Parse::Cache::Redis) return true unless store.respond_to?(:create) bottom = walk_to_adapter(store) return true if bottom.nil? klass_name = bottom.class.name.to_s klass_name.include?("Memory") || klass_name.include?("Null") end |
.handle_degraded(mode, key, source: "Parse::LockBackend", unavailable_error: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Emit the configured degraded-store warning. source: lets
the caller tag the prefix so an operator reading
[Parse::Lock] Lock store is process-local knows which
caller surfaced the warning (vs [Parse::CreateLock] for
the find-or-create path).
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/parse/lock_backend.rb', line 96 def handle_degraded(mode, key, source: "Parse::LockBackend", unavailable_error: nil) case mode when :raise err = unavailable_error || Parse::Error raise err, "#{source}: cross-process lock store unavailable; " \ "current store is process-local" when :proceed # silent when :warn_throttled now = monotonic_now if @degraded_warned_at.nil? || (now - @degraded_warned_at) >= DEGRADED_WARNING_THROTTLE_SECONDS @degraded_warned_at = now warn "[#{source}] Lock store is process-local (Moneta Memory/Null). " \ "Cross-process locking is NOT in effect. Configure a Redis-backed " \ "cache to enable distributed locking." end else warn "[#{source}] Lock store is process-local; cross-process locking disabled. " \ "key_digest=#{key.is_a?(String) ? key[-12..] : key.inspect}" end end |
.lock_secret_for(store:, source: "Parse::LockBackend") ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolve the HMAC secret for lock-key derivation. Behavior depends on store type:
- Configured secret present — returned verbatim (operator
set
Parse.synchronize_create_secretorPARSE_STACK_LOCK_SECRET). - Degraded (process-local) store, no configured secret —
returns the per-process auto-derived random secret. Locking
is already process-local in this branch, so a per-process
secret is fine and improves test/single-process privacy by
preventing
KEYS *enumeration. - Cross-process store, no configured secret — returns
nilwith a one-time warn. Per-process auto-derived secrets would break cross-process key equality (and therefore the lock itself), so the caller falls back to plain SHA-256 and gets a loud nudge to configure a real secret.
246 247 248 249 250 251 252 253 254 255 |
# File 'lib/parse/lock_backend.rb', line 246 def lock_secret_for(store:, source: "Parse::LockBackend") configured = configured_secret return configured if configured && !configured.empty? if degraded_store?(store) auto_secret else warn_plain_sha_once(source: source) nil end end |
.lock_store ⇒ Object?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Find the Moneta store the lock should write through. Resolved
at call time (not memoized) so a test or an operator can swap
Parse.synchronize_create_store after boot and see the change
take effect on the next acquisition.
51 52 53 54 55 56 57 58 |
# File 'lib/parse/lock_backend.rb', line 51 def lock_store if Parse.respond_to?(:synchronize_create_store) && Parse.synchronize_create_store return Parse.synchronize_create_store end Parse.cache rescue Parse::Error::ConnectionError nil end |
.monotonic_now ⇒ Float
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns CLOCK_MONOTONIC seconds.
202 203 204 |
# File 'lib/parse/lock_backend.rb', line 202 def monotonic_now Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
.poll_interval ⇒ Float
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Jittered poll interval for the wait-loop. Symmetric jitter around DEFAULT_POLL_BASE of half-width DEFAULT_POLL_JITTER; the result is bounded but non-deterministic so contended waiters don't sync up.
181 182 183 |
# File 'lib/parse/lock_backend.rb', line 181 def poll_interval DEFAULT_POLL_BASE + (rand * 2 - 1) * DEFAULT_POLL_JITTER end |
.process_mutex(key) ⇒ Mutex
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Per-key in-process Mutex registry for the degraded fallback path. The first acquisition for a given key creates the Mutex; subsequent acquisitions reuse it. Registry itself is guarded by a tiny outer Mutex so two threads racing the first acquisition of the same key get the same Mutex.
193 194 195 196 197 198 199 |
# File 'lib/parse/lock_backend.rb', line 193 def process_mutex(key) @process_mutex_registry_lock ||= Mutex.new @process_mutex_registry_lock.synchronize do @process_mutex_registry ||= {} @process_mutex_registry[key] ||= Mutex.new end end |
.release(store, key, owner) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compare-and-delete release. When the store exposes an atomic
primitive (Parse::Cache::Redis → server-side Lua CAD), use it so
a holder whose lease expired and was re-acquired by someone else
can never delete the new holder's key. Falls back to a
best-effort GET-then-DEL for raw-Moneta stores, where the
worst-case cross-holder-delete race is bounded by the short TTL
(callers clamp ttl: to ≤ 30s) — documented residual risk for
the non-Redis path.
166 167 168 169 170 171 172 173 |
# File 'lib/parse/lock_backend.rb', line 166 def release(store, key, owner) return store.lock_release(key, owner) if store.respond_to?(:lock_release) current = store[key] store.delete(key) if current == owner rescue StandardError => e warn "[Parse::LockBackend] release error (#{e.class}): #{e.}" end |
.reset!
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Reset backend-owned state. Intended for test teardown — production code should never call this.
210 211 212 213 214 215 216 |
# File 'lib/parse/lock_backend.rb', line 210 def reset! @degraded_warned_at = nil @process_mutex_registry = nil @process_mutex_registry_lock = nil @auto_secret = nil @plain_sha_warned = nil end |
.try_acquire(store, key, owner, ttl) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Atomic SETNX-style acquisition. Returns true on success, false on contention OR error (logged). Never raises — the caller's wait loop is the source of truth for "did we get the lock," and a transient store error should look the same to the loop as "someone else has it."
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/parse/lock_backend.rb', line 135 def try_acquire(store, key, owner, ttl) # Prefer the store's native atomic lock primitive when it exposes # one (Parse::Cache::Redis). That path uses raw-Redis # `SET key owner NX EX ttl` with plain-string encoding so it pairs # with the atomic compare-and-delete in {.release}. Falls back to # Moneta `:create` (also an atomic SETNX) for raw-Moneta stores. return store.lock_acquire(key, owner, ttl) if store.respond_to?(:lock_acquire) # Trigger lazy TTL sweep on Moneta::Memory before `:create` # (no-op on Redis). Without this, the Memory adapter returns # false on `:create` even after TTL expiry until a `:key?` # or `:[]` access flushes the stale entry. store.key?(key) store.create(key, owner, expires: ttl) rescue StandardError => e warn "[Parse::LockBackend] acquire error (#{e.class}): #{e.}" false end |
.warn_plain_sha_once(source:) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
One-time process-scoped warn when a cross-process lock store is in use without an operator-configured HMAC secret. The warning text explains both the enumeration risk (key material is deterministic) and the lock-pinning risk (when the cache and lock store share a Redis DB) and points at the remediation knobs.
281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/parse/lock_backend.rb', line 281 def warn_plain_sha_once(source:) return if @plain_sha_warned @plain_sha_warned = true warn "[#{source}:SECURITY] No PARSE_STACK_LOCK_SECRET configured and Redis-backed store detected. " \ "Falling back to plain SHA256 for lock-key derivation so cross-process locking actually works. " \ "Risks of running without an HMAC secret: (1) lock keys are deterministic and may expose key " \ "material content via Redis MONITOR/snapshots; (2) when the response cache and the lock store " \ "share a Redis DB, any caller with write access to Parse.cache can plant a lock key under a " \ "guessable digest and pin the lock for that resource until TTL expiry — a targeted DoS / " \ "lock-pinning primitive. Set PARSE_STACK_LOCK_SECRET (or Parse.synchronize_create_secret = '…') " \ "to enable HMAC keying, or point Parse.synchronize_create_store at a separate Redis DB from " \ "the response cache." end |