parse-stack-next 5.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
- data/.github/dependabot.yml +13 -0
- data/.github/workflows/codeql.yml +1 -1
- data/.github/workflows/docs.yml +3 -3
- data/.github/workflows/release.yml +14 -3
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/.yardopts +19 -0
- data/CHANGELOG.md +792 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +8 -5
- data/README.md +15 -0
- data/Rakefile +5 -1
- data/docs/acl_clp_guide.md +553 -0
- data/docs/atlas_vector_search_guide.md +123 -22
- data/docs/client_sdk_guide.md +201 -5
- data/docs/usage_guide.md +21 -0
- data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
- data/lib/parse/agent/tools.rb +153 -1
- data/lib/parse/cache/redis.rb +53 -0
- data/lib/parse/client/caching.rb +18 -1
- data/lib/parse/client.rb +79 -12
- data/lib/parse/embeddings/cohere.rb +143 -6
- data/lib/parse/embeddings/provider.rb +20 -2
- data/lib/parse/embeddings/voyage.rb +102 -0
- data/lib/parse/embeddings.rb +332 -1
- data/lib/parse/live_query/client.rb +167 -4
- data/lib/parse/live_query/configuration.rb +12 -0
- data/lib/parse/live_query/subscription.rb +55 -2
- data/lib/parse/live_query.rb +123 -1
- data/lib/parse/lock.rb +342 -0
- data/lib/parse/lock_backend.rb +308 -0
- data/lib/parse/model/classes/audience.rb +5 -0
- data/lib/parse/model/classes/installation.rb +122 -0
- data/lib/parse/model/classes/job_schedule.rb +3 -1
- data/lib/parse/model/classes/job_status.rb +4 -1
- data/lib/parse/model/classes/push_status.rb +4 -1
- data/lib/parse/model/classes/session.rb +7 -0
- data/lib/parse/model/classes/user.rb +204 -0
- data/lib/parse/model/core/create_lock.rb +28 -146
- data/lib/parse/model/core/embed_managed.rb +162 -13
- data/lib/parse/model/core/parse_reference.rb +17 -1
- data/lib/parse/model/core/querying.rb +26 -2
- data/lib/parse/model/file.rb +523 -18
- data/lib/parse/query.rb +31 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +98 -1
- data/parse-stack-next.gemspec +2 -2
- metadata +17 -7
|
@@ -225,6 +225,24 @@ module Parse
|
|
|
225
225
|
# @return [Array<Parse::Session>] A list of active Parse::Session objects.
|
|
226
226
|
has_many :active_sessions, as: :session
|
|
227
227
|
|
|
228
|
+
# @!attribute installations
|
|
229
|
+
# A `has_many` query-form association resolving to all
|
|
230
|
+
# {Parse::Installation} records whose `user` pointer is this user.
|
|
231
|
+
# Useful for targeted push — e.g. sending a notification to every
|
|
232
|
+
# device the user is signed into. This is a query (no column is
|
|
233
|
+
# stored on `_User`); each access issues a `find` against
|
|
234
|
+
# `_Installation` for `where(user: self)`.
|
|
235
|
+
#
|
|
236
|
+
# **Requires a master-key client.** Parse Server hardcodes
|
|
237
|
+
# `_Installation` `find` to master-key-only at the REST layer, so
|
|
238
|
+
# this association will return an empty array (or fail-closed
|
|
239
|
+
# depending on agent scope) under a session-token-only / sessionless
|
|
240
|
+
# client. The `user` pointer is also not a reliable owner identity
|
|
241
|
+
# — devices outlive sessions and can change users — see
|
|
242
|
+
# {Parse::Installation} for the full caveat list.
|
|
243
|
+
# @return [Array<Parse::Installation>]
|
|
244
|
+
has_many :installations, as: :installation
|
|
245
|
+
|
|
228
246
|
# CHANGE -- ACLs can be managed
|
|
229
247
|
# before_save do
|
|
230
248
|
# # You cannot specify user ACLs.
|
|
@@ -278,6 +296,192 @@ module Parse
|
|
|
278
296
|
def authdata_trusted?
|
|
279
297
|
Thread.current[AUTHDATA_TRUST_KEY] == true
|
|
280
298
|
end
|
|
299
|
+
|
|
300
|
+
# =========================================================================
|
|
301
|
+
# Field-visibility DSL for _User
|
|
302
|
+
# =========================================================================
|
|
303
|
+
#
|
|
304
|
+
# `_User` has two field-visibility flavors that vanilla
|
|
305
|
+
# {Parse::Object.protect_fields} can't express on its own because
|
|
306
|
+
# Parse Server's `protectedFieldsOwnerExempt` option special-cases
|
|
307
|
+
# the owning user (the user sees their own row in full unless the
|
|
308
|
+
# option is disabled). These helpers wrap that pattern.
|
|
309
|
+
#
|
|
310
|
+
# ## Prerequisites
|
|
311
|
+
#
|
|
312
|
+
# 1. Set `protectedFieldsOwnerExempt: false` in your Parse Server
|
|
313
|
+
# startup options. With the historical default `true`, the owning
|
|
314
|
+
# user is silently exempted from every `protectedFields` rule on
|
|
315
|
+
# `_User`, so {.master_only_fields} would still be visible to the
|
|
316
|
+
# user themselves. Parse Server's default for this option is
|
|
317
|
+
# changing to `false` in a future version; until your server
|
|
318
|
+
# adopts that default you must set it explicitly.
|
|
319
|
+
# 2. For {.self_visible_fields}: add a self-pointer field on `_User`
|
|
320
|
+
# that points to the same user, and maintain it from a
|
|
321
|
+
# `beforeSave('_User')` Cloud Code trigger:
|
|
322
|
+
#
|
|
323
|
+
# ```js
|
|
324
|
+
# Parse.Cloud.beforeSave(Parse.User, (req) => {
|
|
325
|
+
# const u = req.object;
|
|
326
|
+
# if (!u.get('self')) u.set('self', u); // self-pointer
|
|
327
|
+
# });
|
|
328
|
+
# ```
|
|
329
|
+
#
|
|
330
|
+
# The SDK cannot install either of those — they're server-side
|
|
331
|
+
# configuration — but the helpers below will warn if they detect
|
|
332
|
+
# they're being mis-applied.
|
|
333
|
+
|
|
334
|
+
# Hide one or more fields from query/get responses for **all**
|
|
335
|
+
# non-master callers, including the owning user themselves.
|
|
336
|
+
# Useful for admin-only metadata living on `_User`
|
|
337
|
+
# (e.g. internal scoring, moderation notes).
|
|
338
|
+
#
|
|
339
|
+
# Requires Parse Server option `protectedFieldsOwnerExempt: false`.
|
|
340
|
+
# With the default `true`, the owning user still sees these fields
|
|
341
|
+
# on their own row.
|
|
342
|
+
#
|
|
343
|
+
# @param fields [Array<Symbol,String>] field names. Use snake_case
|
|
344
|
+
# Ruby property names; they're auto-converted to camelCase.
|
|
345
|
+
# @return [Array<Symbol>] full master-only field list after this call.
|
|
346
|
+
# @example
|
|
347
|
+
# class Parse::User
|
|
348
|
+
# property :my_opinion_of_them, :string
|
|
349
|
+
# master_only_fields :my_opinion_of_them
|
|
350
|
+
# end
|
|
351
|
+
def master_only_fields(*fields)
|
|
352
|
+
@master_only_fields ||= []
|
|
353
|
+
@master_only_fields = (@master_only_fields + fields.flatten.map(&:to_sym)).uniq
|
|
354
|
+
_warn_about_owner_exempt_prereq!
|
|
355
|
+
_rebuild_user_protected_fields!
|
|
356
|
+
@master_only_fields.dup
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Hide one or more fields from public/role/other-user callers, but
|
|
360
|
+
# allow the **owning user** to see them. Useful for private profile
|
|
361
|
+
# data that belongs to the user (e.g. preferences, private notes).
|
|
362
|
+
#
|
|
363
|
+
# Requires:
|
|
364
|
+
# * Parse Server option `protectedFieldsOwnerExempt: false`.
|
|
365
|
+
# * A self-pointer field on `_User` (named via `via:`, default
|
|
366
|
+
# `:self`) that is set to the row's own pointer by a
|
|
367
|
+
# `beforeSave('_User')` Cloud Code trigger.
|
|
368
|
+
#
|
|
369
|
+
# @param fields [Array<Symbol,String>] field names. snake_case OK.
|
|
370
|
+
# @param via [Symbol,String] name of the self-pointer field on
|
|
371
|
+
# `_User` (default `:self`).
|
|
372
|
+
# @return [Array<Symbol>]
|
|
373
|
+
# @example
|
|
374
|
+
# class Parse::User
|
|
375
|
+
# property :favorite_color, :string
|
|
376
|
+
# self_visible_fields :favorite_color, via: :self
|
|
377
|
+
# end
|
|
378
|
+
def self_visible_fields(*fields, via: :self)
|
|
379
|
+
@self_visible_fields ||= []
|
|
380
|
+
@self_visible_fields = (@self_visible_fields + fields.flatten.map(&:to_sym)).uniq
|
|
381
|
+
@self_pointer_field = via.to_sym
|
|
382
|
+
_warn_about_owner_exempt_prereq!
|
|
383
|
+
_warn_about_self_pointer_prereq!(via)
|
|
384
|
+
_rebuild_user_protected_fields!
|
|
385
|
+
@self_visible_fields.dup
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Override {Parse::Object.protect_fields} on `_User` so that ad-hoc
|
|
389
|
+
# uses (i.e. not through {.master_only_fields} /
|
|
390
|
+
# {.self_visible_fields}) emit a one-time advisory pointing at the
|
|
391
|
+
# higher-level helpers and the `protectedFieldsOwnerExempt` flag.
|
|
392
|
+
# The behavior is otherwise unchanged.
|
|
393
|
+
def protect_fields(pattern, fields)
|
|
394
|
+
_warn_about_user_protect_fields! unless @_user_field_dsl_active
|
|
395
|
+
super
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# @!visibility private
|
|
399
|
+
def _rebuild_user_protected_fields!
|
|
400
|
+
@master_only_fields ||= []
|
|
401
|
+
@self_visible_fields ||= []
|
|
402
|
+
pointer = @self_pointer_field || :self
|
|
403
|
+
all_hidden = (@master_only_fields + @self_visible_fields).uniq
|
|
404
|
+
|
|
405
|
+
@_user_field_dsl_active = true
|
|
406
|
+
begin
|
|
407
|
+
protect_fields("*", all_hidden) unless all_hidden.empty?
|
|
408
|
+
unless @self_visible_fields.empty?
|
|
409
|
+
protect_fields("userField:#{pointer}", @master_only_fields)
|
|
410
|
+
end
|
|
411
|
+
ensure
|
|
412
|
+
@_user_field_dsl_active = false
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# @!visibility private
|
|
417
|
+
def _warn_about_user_protect_fields!
|
|
418
|
+
return if @_user_protect_fields_warned
|
|
419
|
+
@_user_protect_fields_warned = true
|
|
420
|
+
_emit_user_field_advisory(
|
|
421
|
+
"[Parse::User] protect_fields was called directly on _User. " \
|
|
422
|
+
"For master-only and owner-visible field patterns prefer " \
|
|
423
|
+
"`Parse::User.master_only_fields` and `Parse::User.self_visible_fields`. " \
|
|
424
|
+
"Either way, ensure Parse Server is started with " \
|
|
425
|
+
"`protectedFieldsOwnerExempt: false` (the historical default `true` — " \
|
|
426
|
+
"changing to `false` in a future Parse Server version — exempts the " \
|
|
427
|
+
"owning user from every protectedFields rule on _User, which silently " \
|
|
428
|
+
"negates these protections for the user's own row).",
|
|
429
|
+
)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# @!visibility private
|
|
433
|
+
# Fires once when `master_only_fields` / `self_visible_fields` is first
|
|
434
|
+
# used. Without `protectedFieldsOwnerExempt: false` in Parse Server's
|
|
435
|
+
# startup options, neither helper does what its name promises -- the
|
|
436
|
+
# default `true` silently exempts the owning user from every
|
|
437
|
+
# protectedFields rule on _User. The SDK can't introspect Parse
|
|
438
|
+
# Server's startup options, so we surface this as a one-time advisory
|
|
439
|
+
# at declaration time so it's loud enough to catch before deploy.
|
|
440
|
+
def _warn_about_owner_exempt_prereq!
|
|
441
|
+
return if @_owner_exempt_warned
|
|
442
|
+
@_owner_exempt_warned = true
|
|
443
|
+
_emit_user_field_advisory(
|
|
444
|
+
"[Parse::User] master_only_fields / self_visible_fields require " \
|
|
445
|
+
"Parse Server option `protectedFieldsOwnerExempt: false`. With the " \
|
|
446
|
+
"historical default `true`, the owning user is silently exempted " \
|
|
447
|
+
"from every protectedFields rule on _User, so a field declared " \
|
|
448
|
+
"master-only would still be visible to the user themselves on their " \
|
|
449
|
+
"own row. Parse Server's default for this option is changing to " \
|
|
450
|
+
"`false` in a future version (which makes these helpers work " \
|
|
451
|
+
"without extra config), but until your server adopts that default " \
|
|
452
|
+
"you must set `protectedFieldsOwnerExempt: false` in your " \
|
|
453
|
+
"ParseServer options BEFORE deploying. See docs/acl_clp_guide.md §4.2.",
|
|
454
|
+
)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# @!visibility private
|
|
458
|
+
# Fires once when `self_visible_fields` is first used. The Parse
|
|
459
|
+
# Server side requires (a) a self-pointer field on _User populated
|
|
460
|
+
# by a beforeSave('_User') trigger, AND (b) a one-shot backfill on
|
|
461
|
+
# any pre-existing user rows so the pointer is set before the
|
|
462
|
+
# `userField:<via>` group matches them.
|
|
463
|
+
def _warn_about_self_pointer_prereq!(via)
|
|
464
|
+
return if @_self_pointer_warned
|
|
465
|
+
@_self_pointer_warned = true
|
|
466
|
+
_emit_user_field_advisory(
|
|
467
|
+
"[Parse::User] self_visible_fields(via: :#{via}) requires a " \
|
|
468
|
+
"self-pointer field named `#{via}` on _User pointing at the same " \
|
|
469
|
+
"row, populated by a beforeSave('_User') Cloud Code trigger. " \
|
|
470
|
+
"Existing user rows ALSO need a one-shot backfill (the trigger " \
|
|
471
|
+
"only fires on save) -- without it, those rows never match the " \
|
|
472
|
+
"`userField:#{via}` group and the field stays hidden from the " \
|
|
473
|
+
"user themselves. See docs/acl_clp_guide.md §4.2.",
|
|
474
|
+
)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# @!visibility private
|
|
478
|
+
def _emit_user_field_advisory(msg)
|
|
479
|
+
if Parse.respond_to?(:logger) && Parse.logger
|
|
480
|
+
Parse.logger.warn(msg)
|
|
481
|
+
else
|
|
482
|
+
Kernel.warn(msg)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
281
485
|
end
|
|
282
486
|
|
|
283
487
|
# @!visibility private
|
|
@@ -6,6 +6,7 @@ require "openssl"
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "securerandom"
|
|
8
8
|
require "monitor"
|
|
9
|
+
require_relative "../../lock_backend"
|
|
9
10
|
|
|
10
11
|
module Parse
|
|
11
12
|
# Mutual-exclusion primitive for `first_or_create!` / `create_or_update!` to
|
|
@@ -70,25 +71,29 @@ module Parse
|
|
|
70
71
|
master_key: master_key,
|
|
71
72
|
)
|
|
72
73
|
|
|
73
|
-
store = lock_store
|
|
74
|
-
if degraded_store?(store)
|
|
75
|
-
handle_degraded(
|
|
76
|
-
|
|
74
|
+
store = LockBackend.lock_store
|
|
75
|
+
if LockBackend.degraded_store?(store)
|
|
76
|
+
LockBackend.handle_degraded(
|
|
77
|
+
on_degraded, key,
|
|
78
|
+
source: "Parse::CreateLock",
|
|
79
|
+
unavailable_error: Parse::CreateLockUnavailableError,
|
|
80
|
+
)
|
|
81
|
+
return LockBackend.process_mutex(key).synchronize(&block)
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
owner = SecureRandom.uuid
|
|
80
85
|
acquired_at = nil
|
|
81
|
-
start = monotonic_now
|
|
86
|
+
start = LockBackend.monotonic_now
|
|
82
87
|
|
|
83
88
|
loop do
|
|
84
|
-
if try_acquire(store, key, owner, ttl)
|
|
85
|
-
acquired_at = monotonic_now
|
|
89
|
+
if LockBackend.try_acquire(store, key, owner, ttl)
|
|
90
|
+
acquired_at = LockBackend.monotonic_now
|
|
86
91
|
wait_ms = ((acquired_at - start) * 1000).round
|
|
87
92
|
instrument("acquired", key, wait_ms: wait_ms)
|
|
88
93
|
break
|
|
89
94
|
end
|
|
90
95
|
|
|
91
|
-
elapsed = monotonic_now - start
|
|
96
|
+
elapsed = LockBackend.monotonic_now - start
|
|
92
97
|
if elapsed >= wait
|
|
93
98
|
waited_ms = (elapsed * 1000).round
|
|
94
99
|
instrument("timeout", key, waited_ms: waited_ms)
|
|
@@ -96,15 +101,15 @@ module Parse
|
|
|
96
101
|
"Could not acquire create-lock for #{parse_class} within #{wait}s"
|
|
97
102
|
end
|
|
98
103
|
instrument("contended", key, elapsed_ms: (elapsed * 1000).round) if elapsed > 0
|
|
99
|
-
sleep(poll_interval)
|
|
104
|
+
sleep(LockBackend.poll_interval)
|
|
100
105
|
end
|
|
101
106
|
|
|
102
107
|
begin
|
|
103
108
|
yield
|
|
104
109
|
ensure
|
|
105
110
|
if acquired_at
|
|
106
|
-
release(store, key, owner)
|
|
107
|
-
held_ms = ((monotonic_now - acquired_at) * 1000).round
|
|
111
|
+
LockBackend.release(store, key, owner)
|
|
112
|
+
held_ms = ((LockBackend.monotonic_now - acquired_at) * 1000).round
|
|
108
113
|
instrument("released", key, held_ms: held_ms)
|
|
109
114
|
end
|
|
110
115
|
end
|
|
@@ -123,7 +128,7 @@ module Parse
|
|
|
123
128
|
"synchronize key payload exceeds #{MAX_PAYLOAD_BYTES} bytes (got #{payload.bytesize})"
|
|
124
129
|
end
|
|
125
130
|
|
|
126
|
-
secret = lock_secret_for(store: lock_store)
|
|
131
|
+
secret = lock_secret_for(store: LockBackend.lock_store)
|
|
127
132
|
digest = if secret
|
|
128
133
|
OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
|
|
129
134
|
else
|
|
@@ -134,23 +139,14 @@ module Parse
|
|
|
134
139
|
|
|
135
140
|
# @!visibility private
|
|
136
141
|
def reset!
|
|
137
|
-
|
|
138
|
-
@
|
|
139
|
-
@
|
|
140
|
-
|
|
142
|
+
# All resettable state lives on Parse::LockBackend now —
|
|
143
|
+
# @degraded_warned_at, @process_mutex_registry, @auto_secret,
|
|
144
|
+
# @plain_sha_warned. v5.1.0 extraction.
|
|
145
|
+
LockBackend.reset!
|
|
141
146
|
end
|
|
142
147
|
|
|
143
148
|
private
|
|
144
149
|
|
|
145
|
-
def lock_store
|
|
146
|
-
if Parse.respond_to?(:synchronize_create_store) && Parse.synchronize_create_store
|
|
147
|
-
return Parse.synchronize_create_store
|
|
148
|
-
end
|
|
149
|
-
Parse.cache
|
|
150
|
-
rescue Parse::Error::ConnectionError
|
|
151
|
-
nil
|
|
152
|
-
end
|
|
153
|
-
|
|
154
150
|
def parse_application_id
|
|
155
151
|
Parse.client.application_id
|
|
156
152
|
rescue Parse::Error::ConnectionError
|
|
@@ -253,133 +249,19 @@ module Parse
|
|
|
253
249
|
end
|
|
254
250
|
end
|
|
255
251
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# path rather than spinning until timeout on NoMethodError.
|
|
263
|
-
return false if defined?(Parse::Cache::Redis) && store.is_a?(Parse::Cache::Redis)
|
|
264
|
-
return true unless store.respond_to?(:create)
|
|
265
|
-
bottom = walk_to_adapter(store)
|
|
266
|
-
return true if bottom.nil?
|
|
267
|
-
klass_name = bottom.class.name.to_s
|
|
268
|
-
klass_name.include?("Memory") || klass_name.include?("Null")
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def walk_to_adapter(store)
|
|
272
|
-
current = store
|
|
273
|
-
# Walk transformer chain to find bottom Moneta adapter
|
|
274
|
-
while current.respond_to?(:adapter) && current.adapter && current.adapter != current
|
|
275
|
-
current = current.adapter
|
|
276
|
-
end
|
|
277
|
-
current
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def handle_degraded(mode, key)
|
|
281
|
-
case mode
|
|
282
|
-
when :raise
|
|
283
|
-
raise Parse::CreateLockUnavailableError,
|
|
284
|
-
"synchronize requires a cross-process cache store (Redis); current store is process-local"
|
|
285
|
-
when :proceed
|
|
286
|
-
# silent
|
|
287
|
-
when :warn_throttled
|
|
288
|
-
now = monotonic_now
|
|
289
|
-
if @degraded_warned_at.nil? || (now - @degraded_warned_at) >= DEGRADED_WARNING_THROTTLE_SECONDS
|
|
290
|
-
@degraded_warned_at = now
|
|
291
|
-
warn "[Parse::CreateLock] Lock store is process-local (Moneta Memory/Null). " \
|
|
292
|
-
"Cross-process locking is NOT in effect. Configure a Redis-backed cache to enable distributed locking."
|
|
293
|
-
end
|
|
294
|
-
else # :warn (default)
|
|
295
|
-
warn "[Parse::CreateLock] Lock store is process-local; cross-process locking disabled. " \
|
|
296
|
-
"key_digest=#{key[KEY_PREFIX.size, 12]}…"
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def try_acquire(store, key, owner, ttl)
|
|
301
|
-
# Trigger lazy TTL sweep on Moneta::Memory before #create (no-op on Redis).
|
|
302
|
-
# Without this, Memory adapter returns false on #create even after TTL expiry
|
|
303
|
-
# until a #key? or #[] access flushes the stale entry.
|
|
304
|
-
store.key?(key)
|
|
305
|
-
store.create(key, owner, expires: ttl)
|
|
306
|
-
rescue StandardError => e
|
|
307
|
-
warn "[Parse::CreateLock] acquire error (#{e.class}): #{e.message}"
|
|
308
|
-
false
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def release(store, key, owner)
|
|
312
|
-
# Best-effort compare-and-delete. Moneta does not expose atomic CAD;
|
|
313
|
-
# the worst-case race here is bounded by the short TTL (default #{DEFAULT_TTL}s).
|
|
314
|
-
current = store[key]
|
|
315
|
-
store.delete(key) if current == owner
|
|
316
|
-
rescue StandardError => e
|
|
317
|
-
warn "[Parse::CreateLock] release error (#{e.class}): #{e.message}"
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def poll_interval
|
|
321
|
-
DEFAULT_POLL_BASE + (rand * 2 - 1) * DEFAULT_POLL_JITTER
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
def monotonic_now
|
|
325
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
326
|
-
end
|
|
252
|
+
# All the lock infrastructure — store discovery, degraded
|
|
253
|
+
# detection, atomic-SETNX, release semantics, poll-interval
|
|
254
|
+
# jitter, process-mutex registry, AND HMAC secret resolution —
|
|
255
|
+
# lives on {Parse::LockBackend} now (v5.1.0 extraction). The
|
|
256
|
+
# only CreateLock-specific helper still here is `clamp` for
|
|
257
|
+
# the public API's input range.
|
|
327
258
|
|
|
328
259
|
def clamp(value, lo, hi)
|
|
329
260
|
[lo, value, hi].sort[1]
|
|
330
261
|
end
|
|
331
262
|
|
|
332
|
-
def process_mutex(key)
|
|
333
|
-
@process_mutex_registry_lock ||= Mutex.new
|
|
334
|
-
@process_mutex_registry_lock.synchronize do
|
|
335
|
-
@process_mutex_registry ||= {}
|
|
336
|
-
@process_mutex_registry[key] ||= Mutex.new
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
263
|
def lock_secret_for(store:)
|
|
341
|
-
|
|
342
|
-
return configured if configured && !configured.empty?
|
|
343
|
-
|
|
344
|
-
# No operator-configured secret. Behavior depends on store type:
|
|
345
|
-
# - Memory/Null adapter: locking is already process-local, so a
|
|
346
|
-
# per-process auto-derived HMAC secret is fine and preserves
|
|
347
|
-
# privacy in tests / single-process deployments.
|
|
348
|
-
# - Redis (or any cross-process store): a per-process secret would
|
|
349
|
-
# break cross-process key equality, defeating the lock. Fall back
|
|
350
|
-
# to plain SHA256 with a one-time warning so operators know to
|
|
351
|
-
# harden key material.
|
|
352
|
-
if degraded_store?(store)
|
|
353
|
-
auto_secret
|
|
354
|
-
else
|
|
355
|
-
warn_plain_sha_once
|
|
356
|
-
nil
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
def configured_secret
|
|
361
|
-
if Parse.respond_to?(:synchronize_create_secret) && Parse.synchronize_create_secret
|
|
362
|
-
return Parse.synchronize_create_secret.to_s
|
|
363
|
-
end
|
|
364
|
-
ENV["PARSE_STACK_LOCK_SECRET"]
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def auto_secret
|
|
368
|
-
@auto_secret ||= SecureRandom.hex(32)
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
def warn_plain_sha_once
|
|
372
|
-
return if @plain_sha_warned
|
|
373
|
-
@plain_sha_warned = true
|
|
374
|
-
warn "[Parse::CreateLock:SECURITY] No PARSE_STACK_LOCK_SECRET configured and Redis-backed store detected. " \
|
|
375
|
-
"Falling back to plain SHA256 for lock-key derivation so cross-process locking actually works. " \
|
|
376
|
-
"Risks of running without an HMAC secret: (1) lock keys are deterministic and may expose query_attrs " \
|
|
377
|
-
"content via Redis MONITOR/snapshots; (2) when the response cache and the lock store share a Redis DB, " \
|
|
378
|
-
"any caller with write access to Parse.cache can plant a `parse-stack:foc:v1:<sha>` key under a guessable " \
|
|
379
|
-
"digest of (app_id, class, principal, query_attrs) and suppress first_or_create!/create_or_update! for " \
|
|
380
|
-
"that tuple until TTL expiry — a targeted DoS / create-pinning primitive. " \
|
|
381
|
-
"Set PARSE_STACK_LOCK_SECRET (or Parse.synchronize_create_secret = '…') to enable HMAC keying, or " \
|
|
382
|
-
"point Parse.synchronize_create_store at a separate Redis DB from the response cache."
|
|
264
|
+
LockBackend.lock_secret_for(store: store, source: "Parse::CreateLock")
|
|
383
265
|
end
|
|
384
266
|
|
|
385
267
|
def instrument(event, key, payload = {})
|