parse-stack-next 5.1.1 → 5.2.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/.env.sample +12 -0
- data/.env.test +4 -4
- data/CHANGELOG.md +545 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +6 -1
- data/README.md +167 -38
- data/Rakefile +56 -10
- data/docs/atlas_vector_search_guide.md +110 -9
- data/docs/mcp_guide.md +433 -0
- data/docs/mongodb_direct_guide.md +66 -1
- data/docs/mongodb_index_optimization_guide.md +22 -1
- data/docs/usage_guide.md +15 -0
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +90 -19
- data/lib/parse/agent/describe.rb +1 -0
- data/lib/parse/agent/errors.rb +16 -0
- data/lib/parse/agent/mcp_client.rb +9 -0
- data/lib/parse/agent/mcp_dispatcher.rb +139 -7
- data/lib/parse/agent/mcp_rack_app.rb +621 -17
- data/lib/parse/agent/mcp_subscriptions.rb +607 -0
- data/lib/parse/agent/metadata_dsl.rb +58 -0
- data/lib/parse/agent/metadata_registry.rb +141 -1
- data/lib/parse/agent/prompt_hardening.rb +213 -0
- data/lib/parse/agent/result_formatter.rb +18 -3
- data/lib/parse/agent/tools.rb +167 -24
- data/lib/parse/agent.rb +692 -21
- data/lib/parse/client/request.rb +55 -4
- data/lib/parse/client/response.rb +4 -0
- data/lib/parse/client.rb +205 -7
- data/lib/parse/model/classes/installation.rb +27 -10
- data/lib/parse/model/classes/user.rb +8 -0
- data/lib/parse/model/core/actions.rb +58 -4
- data/lib/parse/model/core/embed_managed.rb +19 -14
- data/lib/parse/model/core/indexing.rb +108 -16
- data/lib/parse/model/core/querying.rb +29 -0
- data/lib/parse/model/model.rb +34 -3
- data/lib/parse/model/object.rb +1 -0
- data/lib/parse/query.rb +90 -24
- data/lib/parse/retrieval/agent_tool.rb +369 -0
- data/lib/parse/retrieval/chunk.rb +74 -0
- data/lib/parse/retrieval/chunker.rb +208 -0
- data/lib/parse/retrieval/retriever.rb +274 -0
- data/lib/parse/retrieval.rb +10 -0
- data/lib/parse/schema.rb +69 -20
- data/lib/parse/stack/version.rb +2 -2
- data/parse-stack-next.gemspec +1 -1
- data/scripts/docker/docker-compose.atlas.yml +14 -10
- data/scripts/docker/docker-compose.test.yml +24 -20
- data/scripts/docker/mongo-init.js +3 -3
- data/scripts/start-parse.sh +10 -0
- data/scripts/start_mcp_server.rb +1 -1
- data/scripts/test_server_connection.rb +1 -1
- data/scripts/vector_prototype/create_vector_index.js +1 -1
- data/scripts/vector_prototype/fetch_embeddings.py +2 -2
- data/scripts/vector_prototype/query_prototype.rb +1 -1
- data/scripts/vector_prototype/run.sh +4 -4
- metadata +10 -2
data/lib/parse/client/request.rb
CHANGED
|
@@ -46,12 +46,38 @@ module Parse
|
|
|
46
46
|
# @!attribute [rw] idempotent_methods
|
|
47
47
|
# @return [Array<Symbol>] HTTP methods that should include request IDs
|
|
48
48
|
attr_accessor :idempotent_methods
|
|
49
|
+
|
|
50
|
+
# @!attribute [rw] assume_server_idempotency
|
|
51
|
+
# @return [Boolean] operator assertion that the Parse Server is
|
|
52
|
+
# configured with `idempotencyOptions` covering the write paths the
|
|
53
|
+
# SDK targets. When true, a request that carries a stable
|
|
54
|
+
# `X-Parse-Request-Id` header becomes safe for {Parse::Client} to
|
|
55
|
+
# transparently RETRY on an ambiguous failure (500/503/dropped
|
|
56
|
+
# connection) even when it is a POST or an atomic-op write — Parse
|
|
57
|
+
# Server deduplicates the replay server-side, so the write applies AT
|
|
58
|
+
# MOST ONCE.
|
|
59
|
+
#
|
|
60
|
+
# The replay does NOT transparently return the original response,
|
|
61
|
+
# though: Parse Server rejects the duplicate with error 159, which the
|
|
62
|
+
# SDK raises as {Parse::Error::DuplicateRequestError}. A caller relying
|
|
63
|
+
# on this retry must rescue that error (the original write already
|
|
64
|
+
# landed) and re-fetch by its own key if it needs the result.
|
|
65
|
+
#
|
|
66
|
+
# Default false. Sending the `X-Parse-Request-Id` header is harmless
|
|
67
|
+
# on its own, but ASSUMING the server deduplicates when it does not
|
|
68
|
+
# would double-apply the write on retry. Only set this true when
|
|
69
|
+
# Parse Server's `idempotencyOptions` is actually configured to cover
|
|
70
|
+
# those paths (it is OFF by default on Parse Server).
|
|
71
|
+
attr_accessor :assume_server_idempotency
|
|
49
72
|
end
|
|
50
73
|
|
|
51
74
|
# Default configuration
|
|
52
75
|
self.enable_request_id = true # Enabled by default for production safety
|
|
53
76
|
self.request_id_header = "X-Parse-Request-Id" # Standard Parse header
|
|
54
77
|
self.idempotent_methods = [:post, :put, :patch] # Methods that can benefit from idempotency
|
|
78
|
+
# OFF by default: the client cannot know whether the server deduplicates,
|
|
79
|
+
# so it never assumes retry-safety for writes unless the operator opts in.
|
|
80
|
+
self.assume_server_idempotency = false
|
|
55
81
|
|
|
56
82
|
# Creates a new request
|
|
57
83
|
# @param method [String] the HTTP method
|
|
@@ -121,11 +147,23 @@ module Parse
|
|
|
121
147
|
|
|
122
148
|
return unless should_use_request_id
|
|
123
149
|
|
|
150
|
+
header_name = self.class.request_id_header
|
|
151
|
+
|
|
152
|
+
# If a request id is already on the headers — e.g. a retry re-builds the
|
|
153
|
+
# Request with the same headers hash carried over from the first attempt
|
|
154
|
+
# — adopt it so the `request_id` ivar matches the value actually on the
|
|
155
|
+
# wire. Generating a fresh UUID here while the `||=` below leaves the old
|
|
156
|
+
# header in place would silently diverge the ivar from the sent header.
|
|
157
|
+
existing = @headers[header_name]
|
|
158
|
+
if existing && !existing.to_s.empty?
|
|
159
|
+
@request_id = existing
|
|
160
|
+
return
|
|
161
|
+
end
|
|
162
|
+
|
|
124
163
|
# Use custom request ID if provided, otherwise generate one
|
|
125
164
|
@request_id = @opts[:request_id] || generate_request_id
|
|
126
165
|
|
|
127
166
|
# Add request ID to headers if not already present
|
|
128
|
-
header_name = self.class.request_id_header
|
|
129
167
|
@headers[header_name] ||= @request_id
|
|
130
168
|
end
|
|
131
169
|
|
|
@@ -209,25 +247,38 @@ module Parse
|
|
|
209
247
|
# Enables request ID generation globally
|
|
210
248
|
# @param methods [Array<Symbol>] HTTP methods to apply idempotency to
|
|
211
249
|
# @param header [String] header name to use for request IDs
|
|
212
|
-
|
|
250
|
+
# @param assume_server_dedup [Boolean, nil] when non-nil, also sets
|
|
251
|
+
# {assume_server_idempotency} — pass `true` ONLY when Parse Server's
|
|
252
|
+
# `idempotencyOptions` is configured, to additionally make writes
|
|
253
|
+
# retry-safe. Leave nil (default) to send the header without changing
|
|
254
|
+
# the retry posture.
|
|
255
|
+
def self.enable_idempotency!(methods: [:post, :put, :patch], header: "X-Parse-Request-Id", assume_server_dedup: nil)
|
|
213
256
|
self.enable_request_id = true
|
|
214
257
|
self.idempotent_methods = methods
|
|
215
258
|
self.request_id_header = header
|
|
259
|
+
self.assume_server_idempotency = assume_server_dedup unless assume_server_dedup.nil?
|
|
216
260
|
end
|
|
217
261
|
|
|
218
|
-
# Disables request ID generation globally
|
|
262
|
+
# Disables request ID generation globally. Also clears
|
|
263
|
+
# {assume_server_idempotency} so writes are never treated as retry-safe
|
|
264
|
+
# once the header is no longer sent.
|
|
219
265
|
def self.disable_idempotency!
|
|
220
266
|
self.enable_request_id = false
|
|
267
|
+
self.assume_server_idempotency = false
|
|
221
268
|
end
|
|
222
269
|
|
|
223
270
|
# Configures idempotency settings
|
|
224
271
|
# @param enabled [Boolean] whether to enable idempotency
|
|
225
272
|
# @param methods [Array<Symbol>] HTTP methods to apply idempotency to
|
|
226
273
|
# @param header [String] header name to use for request IDs
|
|
227
|
-
|
|
274
|
+
# @param assume_server_dedup [Boolean] sets {assume_server_idempotency}
|
|
275
|
+
# (default false). Pass true ONLY when Parse Server `idempotencyOptions`
|
|
276
|
+
# is configured for the targeted paths.
|
|
277
|
+
def self.configure_idempotency(enabled: true, methods: [:post, :put, :patch], header: "X-Parse-Request-Id", assume_server_dedup: false)
|
|
228
278
|
self.enable_request_id = enabled
|
|
229
279
|
self.idempotent_methods = methods
|
|
230
280
|
self.request_id_header = header
|
|
281
|
+
self.assume_server_idempotency = assume_server_dedup
|
|
231
282
|
end
|
|
232
283
|
end
|
|
233
284
|
end
|
|
@@ -19,6 +19,10 @@ module Parse
|
|
|
19
19
|
ERROR_TIMEOUT = 124
|
|
20
20
|
# Code when the requests per second limit as been exceeded.
|
|
21
21
|
ERROR_EXCEEDED_BURST_LIMIT = 155
|
|
22
|
+
# Code when a request carrying a previously-seen `X-Parse-Request-Id` is
|
|
23
|
+
# rejected by Parse Server's idempotency layer (the request was already
|
|
24
|
+
# applied). Surfaced as {Parse::Error::DuplicateRequestError}.
|
|
25
|
+
ERROR_DUPLICATE_REQUEST = 159
|
|
22
26
|
# Code when a requested record is not found.
|
|
23
27
|
ERROR_OBJECT_NOT_FOUND = 101
|
|
24
28
|
# Code when the username is missing in request.
|
data/lib/parse/client.rb
CHANGED
|
@@ -62,6 +62,17 @@ module Parse
|
|
|
62
62
|
# An error when the session token provided in the request is invalid.
|
|
63
63
|
class InvalidSessionTokenError < Error; end
|
|
64
64
|
|
|
65
|
+
# Raised when Parse Server's request-id idempotency layer rejects a request
|
|
66
|
+
# carrying a previously-seen `X-Parse-Request-Id` (Parse error code
|
|
67
|
+
# {Parse::Response::ERROR_DUPLICATE_REQUEST}, 159). The duplicate was NOT
|
|
68
|
+
# applied a second time — the original request already succeeded. The SDK
|
|
69
|
+
# reuses the same request id across transparent retries, so a retried write
|
|
70
|
+
# that lands but loses its response will surface THIS error on the replay:
|
|
71
|
+
# treat it as "the write already applied" (re-fetch by your own key if you
|
|
72
|
+
# need the resulting object — Parse Server does not echo the original
|
|
73
|
+
# response on a duplicate).
|
|
74
|
+
class DuplicateRequestError < Error; end
|
|
75
|
+
|
|
65
76
|
# An error raised when a cloud function or job returns an error response
|
|
66
77
|
# (e.g. when the cloud code calls error!()). Carries the function name,
|
|
67
78
|
# Parse error code, HTTP status, and the underlying Response for debugging.
|
|
@@ -167,6 +178,40 @@ module Parse
|
|
|
167
178
|
Parse::Client.client(conn)
|
|
168
179
|
end
|
|
169
180
|
|
|
181
|
+
# Check that the Parse Server is reachable via the health endpoint.
|
|
182
|
+
# This is a no-credentials liveness probe — it does NOT validate the
|
|
183
|
+
# application_id or REST key. A server with a mistyped app_id still returns
|
|
184
|
+
# `true` here; use {.connected?} when you also need to validate credentials.
|
|
185
|
+
# @param conn [Symbol] the named client connection to probe. Defaults to :default.
|
|
186
|
+
# @return [Boolean] +true+ if the server responded with status "ok", +false+
|
|
187
|
+
# if the server is unreachable or returned an unexpected response.
|
|
188
|
+
def self.reachable?(conn = :default)
|
|
189
|
+
client(conn).reachable?
|
|
190
|
+
rescue StandardError
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check that the Parse Server is reachable and responding.
|
|
195
|
+
# By default this probes the credential-less health endpoint, so it returns
|
|
196
|
+
# +true+ whenever the server is up (it does NOT, by itself, validate the
|
|
197
|
+
# application_id or REST key — see {Parse::Client#connected?} for why a
|
|
198
|
+
# data-class probe is unreliable on a hardened server). Pass an `endpoint`
|
|
199
|
+
# (e.g. `"classes/_User"`) to additionally exercise the auth stack against a
|
|
200
|
+
# class you know the configured key can read; a bad key then surfaces as an
|
|
201
|
+
# {Parse::Error::AuthenticationError} at the instance level, which the
|
|
202
|
+
# instance method converts to +false+. Any other failure is caught by this
|
|
203
|
+
# module-boundary rescue and converted to +false+.
|
|
204
|
+
# @param conn [Symbol] the named client connection to probe. Defaults to :default.
|
|
205
|
+
# @param endpoint [String, nil] optional path to probe instead of the health
|
|
206
|
+
# endpoint, to validate credentials against a readable class.
|
|
207
|
+
# @return [Boolean] +true+ if the server is reachable (and, when an endpoint
|
|
208
|
+
# is given, the credentials are accepted).
|
|
209
|
+
def self.connected?(conn = :default, endpoint = nil)
|
|
210
|
+
client(conn).connected?(endpoint)
|
|
211
|
+
rescue StandardError
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
|
|
170
215
|
# The shared cache for the default client connection. This is useful if you want to
|
|
171
216
|
# also utilize the same cache store for other purposes in your application.
|
|
172
217
|
# This should normally be a {https://github.com/minad/moneta Moneta} unified
|
|
@@ -784,6 +829,50 @@ module Parse
|
|
|
784
829
|
self.cache.clear if self.cache.present?
|
|
785
830
|
end
|
|
786
831
|
|
|
832
|
+
# No-credentials liveness probe. Hits the Parse Server health endpoint and
|
|
833
|
+
# returns +true+ when the server responds with status "ok". No application
|
|
834
|
+
# credentials are required, so this passes even when the configured
|
|
835
|
+
# application_id or REST key is wrong. Use {#connected?} to also validate
|
|
836
|
+
# credentials.
|
|
837
|
+
# @return [Boolean] +true+ if the server is up and returned a healthy status.
|
|
838
|
+
def reachable?
|
|
839
|
+
response = request(:get, Parse::API::Server::SERVER_HEALTH_PATH, opts: { cache: false })
|
|
840
|
+
response.success?
|
|
841
|
+
rescue Parse::Error, Faraday::Error
|
|
842
|
+
false
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# Connectivity probe. By default hits the Parse Server health endpoint —
|
|
846
|
+
# the same target as {#reachable?} — so it returns +true+ whenever the
|
|
847
|
+
# server is up, regardless of CLP configuration.
|
|
848
|
+
#
|
|
849
|
+
# Why not a `_User` find? A limit-0 find against +_User+ exercises the auth
|
|
850
|
+
# stack, but locking +_User+ finds to the master key via a Class-Level
|
|
851
|
+
# Permission is standard production hardening — on such a server the probe
|
|
852
|
+
# gets a permission error and (wrongly) reports "not connected" for a
|
|
853
|
+
# perfectly healthy, correctly-configured deployment. The default therefore
|
|
854
|
+
# avoids any data class.
|
|
855
|
+
#
|
|
856
|
+
# To ALSO validate credentials, pass `endpoint:` a path to a class the
|
|
857
|
+
# configured key is allowed to read (e.g. `"classes/_User"` on a default
|
|
858
|
+
# server, or one of your own readable classes). The probe runs `limit: 0`
|
|
859
|
+
# so it never pulls rows, and routes through the auth middleware, so a wrong
|
|
860
|
+
# application_id / REST key surfaces as an {Parse::Error::AuthenticationError}
|
|
861
|
+
# and is converted to +false+. Any connection, timeout, or API error returns
|
|
862
|
+
# +false+ rather than raising; genuine programming errors (e.g.
|
|
863
|
+
# +NoMethodError+) still propagate.
|
|
864
|
+
# @param endpoint [String, nil] optional path to probe instead of the
|
|
865
|
+
# health endpoint, to validate credentials against a readable class.
|
|
866
|
+
# @return [Boolean] +true+ if the server is reachable (and, when an endpoint
|
|
867
|
+
# is given, the credentials are accepted).
|
|
868
|
+
def connected?(endpoint = nil)
|
|
869
|
+
path = endpoint || Parse::API::Server::SERVER_HEALTH_PATH
|
|
870
|
+
response = request(:get, path, query: { limit: 0 }, opts: { cache: false })
|
|
871
|
+
response.success?
|
|
872
|
+
rescue Parse::Error, Faraday::Error
|
|
873
|
+
false
|
|
874
|
+
end
|
|
875
|
+
|
|
787
876
|
# Send a REST API request to the server. This is the low-level API used for all requests
|
|
788
877
|
# to the Parse server with the provided options. Every request sent to Parse through
|
|
789
878
|
# the client goes through the configured set of middleware that can be modified by applying
|
|
@@ -824,6 +913,9 @@ module Parse
|
|
|
824
913
|
# - This usually means you have exceeded the burst limit on requests, which will mean you will be throttled for the
|
|
825
914
|
# next 60 seconds.
|
|
826
915
|
# @raise Parse::Error::InvalidSessionTokenError when the Parse response code is 209.
|
|
916
|
+
# @raise Parse::Error::DuplicateRequestError when the Parse response code is
|
|
917
|
+
# {Parse::Response::ERROR_DUPLICATE_REQUEST} (159) — request-id idempotency
|
|
918
|
+
# rejected a duplicate; the original write already applied.
|
|
827
919
|
# - This means the session token that was sent in the request seems to be invalid.
|
|
828
920
|
# @return [Parse::Response] the response for this request.
|
|
829
921
|
# @see Parse::Middleware::BodyBuilder
|
|
@@ -855,6 +947,11 @@ module Parse
|
|
|
855
947
|
"Good: Parse.client.create_object('X', body, session_token: t, use_master_key: false)"
|
|
856
948
|
end
|
|
857
949
|
|
|
950
|
+
# Retry budget. Initialized ONCE here, ABOVE the `begin` below, so the
|
|
951
|
+
# `retry` keyword in the rescue clauses (which re-runs only the begin
|
|
952
|
+
# block, not the whole method) preserves the countdown across attempts.
|
|
953
|
+
# If this initialization ran inside the begin it would reset on every
|
|
954
|
+
# attempt, turning a transient 500/503/429 into an infinite retry loop.
|
|
858
955
|
_retry_count ||= self.retry_limit
|
|
859
956
|
|
|
860
957
|
if opts[:retry] == false
|
|
@@ -863,6 +960,16 @@ module Parse
|
|
|
863
960
|
_retry_count = opts[:retry]
|
|
864
961
|
end
|
|
865
962
|
|
|
963
|
+
# The effective starting budget, captured ONCE after the opts override
|
|
964
|
+
# (and, like `_retry_count`, above the `begin` so `retry` doesn't reset
|
|
965
|
+
# it). The backoff multiplier is `(_retry_max - _retry_count)`, which
|
|
966
|
+
# grows 1, 2, 3, … as attempts are consumed. Deriving it from
|
|
967
|
+
# `self.retry_limit` instead would go to zero or negative whenever a
|
|
968
|
+
# caller passes `opts: { retry: N }` with N above the instance default,
|
|
969
|
+
# silently disabling the backoff (every retry firing at zero delay).
|
|
970
|
+
_retry_max ||= _retry_count
|
|
971
|
+
|
|
972
|
+
begin
|
|
866
973
|
headers ||= {}
|
|
867
974
|
# if the first argument is a Parse::Request object, then construct it
|
|
868
975
|
_request = nil
|
|
@@ -970,39 +1077,130 @@ module Parse
|
|
|
970
1077
|
elsif response.code == 209 # Error 209: invalid session token
|
|
971
1078
|
Parse::Client._safe_warn("InvalidSessionTokenError", response)
|
|
972
1079
|
raise Parse::Error::InvalidSessionTokenError, response
|
|
1080
|
+
elsif response.code == Parse::Response::ERROR_DUPLICATE_REQUEST # 159
|
|
1081
|
+
# Request-id idempotency rejected a duplicate — the original write
|
|
1082
|
+
# already applied (NOT a second time). Surface a typed, catchable
|
|
1083
|
+
# signal rather than a generic error; this is what a transparently-
|
|
1084
|
+
# retried write that landed-but-lost-its-response sees on the replay.
|
|
1085
|
+
Parse::Client._safe_warn("DuplicateRequestError", response)
|
|
1086
|
+
raise Parse::Error::DuplicateRequestError, response
|
|
973
1087
|
end
|
|
974
1088
|
end
|
|
975
1089
|
|
|
976
1090
|
response
|
|
977
1091
|
rescue Parse::Error::RequestLimitExceededError, Parse::Error::ServiceUnavailableError => e
|
|
978
|
-
|
|
1092
|
+
# 429 (RequestLimitExceeded): the server threw the request away, so
|
|
1093
|
+
# re-sending is safe for any method. 500/503 (ServiceUnavailable) is
|
|
1094
|
+
# ambiguous — a write may have applied before the error — so only
|
|
1095
|
+
# re-send when the request is idempotent (see #idempotent_retry?).
|
|
1096
|
+
retryable = e.is_a?(Parse::Error::RequestLimitExceededError) || idempotent_retry?(method, body, headers)
|
|
1097
|
+
if _retry_count > 0 && retryable
|
|
979
1098
|
warn "[Parse:Retry] Retries remaining #{_retry_count} : #{response.request}"
|
|
980
1099
|
_retry_count -= 1
|
|
981
|
-
# Use Retry-After header if available, otherwise use
|
|
1100
|
+
# Use Retry-After header if available, otherwise use linear backoff
|
|
982
1101
|
retry_after = response.retry_after if response.respond_to?(:retry_after)
|
|
983
1102
|
if retry_after && retry_after > 0
|
|
984
1103
|
_retry_delay = retry_after
|
|
985
1104
|
warn "[Parse:Retry] Using Retry-After header: #{_retry_delay}s"
|
|
986
1105
|
else
|
|
987
|
-
#
|
|
1106
|
+
# Linear backoff (RETRY_DELAY × attempt number) with +/-25% jitter.
|
|
1107
|
+
# Never zero —
|
|
988
1108
|
# zero-wait retries amplify DoS against upstream and stampede on 429.
|
|
989
|
-
backoff_delay = RETRY_DELAY * (
|
|
1109
|
+
backoff_delay = RETRY_DELAY * (_retry_max - _retry_count)
|
|
990
1110
|
_retry_delay = backoff_delay * (0.75 + rand * 0.5)
|
|
991
1111
|
end
|
|
992
1112
|
sleep _retry_delay if _retry_delay > 0
|
|
993
1113
|
retry
|
|
994
1114
|
end
|
|
995
1115
|
raise
|
|
996
|
-
rescue Faraday::ClientError, Net::OpenTimeout => e
|
|
997
|
-
|
|
1116
|
+
rescue Faraday::ClientError, Faraday::TimeoutError, Net::OpenTimeout => e
|
|
1117
|
+
# Request timed out mid-flight: the outcome is unknown (the server may
|
|
1118
|
+
# have received and applied the write but never answered), so only
|
|
1119
|
+
# re-send idempotent requests to avoid double-applying.
|
|
1120
|
+
#
|
|
1121
|
+
# Faraday 2.x raises `Faraday::TimeoutError` for a read timeout
|
|
1122
|
+
# (`Timeout::Error` / `Errno::ETIMEDOUT`); it subclasses `Faraday::Error`,
|
|
1123
|
+
# not `ClientError`, so it must be listed explicitly to be caught. We
|
|
1124
|
+
# deliberately do NOT catch `Faraday::ConnectionFailed` (connection
|
|
1125
|
+
# refused/reset, plus the wrapped connect-timeout): refused is a
|
|
1126
|
+
# non-transient "server down / misconfigured" failure, and auto-retrying
|
|
1127
|
+
# it only adds backoff latency before the inevitable error. Broadening to
|
|
1128
|
+
# reset connections safely (retry reset, fail fast on refused) is tracked
|
|
1129
|
+
# as a follow-up.
|
|
1130
|
+
if _retry_count > 0 && idempotent_retry?(method, body, headers)
|
|
998
1131
|
warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}"
|
|
999
1132
|
_retry_count -= 1
|
|
1000
|
-
backoff_delay = RETRY_DELAY * (
|
|
1133
|
+
backoff_delay = RETRY_DELAY * (_retry_max - _retry_count)
|
|
1001
1134
|
_retry_delay = backoff_delay * (0.75 + rand * 0.5)
|
|
1002
1135
|
sleep _retry_delay if _retry_delay > 0
|
|
1003
1136
|
retry
|
|
1004
1137
|
end
|
|
1005
1138
|
raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.message}"
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
# Whether a request whose outcome is UNKNOWN (a 500/503 or a dropped
|
|
1143
|
+
# connection) is safe to transparently re-send.
|
|
1144
|
+
#
|
|
1145
|
+
# Server-dedup fast path: when the operator has asserted Parse Server
|
|
1146
|
+
# idempotency is configured ({Parse::Request.assume_server_idempotency})
|
|
1147
|
+
# AND this request carries a stable `X-Parse-Request-Id` header, the
|
|
1148
|
+
# server deduplicates a replay, so even a POST or an atomic-op write is
|
|
1149
|
+
# safe to retry — the write applies at most once. The replay is NOT a
|
|
1150
|
+
# transparent success, though: Parse Server rejects the duplicate with
|
|
1151
|
+
# error 159, surfaced as a raised {Parse::Error::DuplicateRequestError}
|
|
1152
|
+
# the caller must rescue (the original write already landed). The SDK sends
|
|
1153
|
+
# the same request id on every retry (the header is set once and preserved
|
|
1154
|
+
# across the `retry`), which is what makes the server-side dedup match.
|
|
1155
|
+
#
|
|
1156
|
+
# Otherwise the conservative method/body heuristic applies: GET and DELETE
|
|
1157
|
+
# are idempotent; a full-object PUT update is idempotent ONLY when it
|
|
1158
|
+
# carries no atomic `__op` mutation (Increment/Add/AddUnique/Remove/Relation
|
|
1159
|
+
# would double-apply on replay); POST (object create / batch) is never
|
|
1160
|
+
# auto-retried. 429 throttles are handled at the call site, since the
|
|
1161
|
+
# server provably discarded the request and those re-send regardless.
|
|
1162
|
+
# @param method [Symbol] the (downcased) HTTP method of the request.
|
|
1163
|
+
# @param body [Hash, Object, nil] the request body.
|
|
1164
|
+
# @param headers [Hash, nil] the outgoing request headers (consulted for
|
|
1165
|
+
# the request-id header on the server-dedup fast path).
|
|
1166
|
+
# @return [Boolean]
|
|
1167
|
+
def idempotent_retry?(method, body, headers = nil)
|
|
1168
|
+
return true if server_deduped_request?(headers)
|
|
1169
|
+
case method
|
|
1170
|
+
when :get, :delete then true
|
|
1171
|
+
when :put then !body_carries_atomic_op?(body)
|
|
1172
|
+
else false
|
|
1173
|
+
end
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
# Whether this request is covered by Parse Server's server-side request-id
|
|
1177
|
+
# deduplication, making a replay a no-op. True only when the operator has
|
|
1178
|
+
# opted in via {Parse::Request.assume_server_idempotency} AND the request
|
|
1179
|
+
# actually carries a non-blank request-id header (writes to inherently
|
|
1180
|
+
# non-idempotent paths — sessions, logout, functions, push, jobs — never
|
|
1181
|
+
# get a request id, so they correctly fail this check).
|
|
1182
|
+
# @param headers [Hash, nil] the outgoing request headers.
|
|
1183
|
+
# @return [Boolean]
|
|
1184
|
+
def server_deduped_request?(headers)
|
|
1185
|
+
return false unless Parse::Request.assume_server_idempotency
|
|
1186
|
+
return false unless headers.is_a?(Hash)
|
|
1187
|
+
rid = headers[Parse::Request.request_id_header]
|
|
1188
|
+
rid.is_a?(String) && !rid.strip.empty?
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
# Whether a request body carries a Parse atomic operation, i.e. any field
|
|
1192
|
+
# whose value is a Hash with an `__op` key (Increment, Add, AddUnique,
|
|
1193
|
+
# Remove, AddRelation, RemoveRelation, Delete). Such ops are not idempotent
|
|
1194
|
+
# and must not be replayed on an ambiguous failure. Assumes the body is a
|
|
1195
|
+
# Ruby Hash, which the SDK's normal save/update path always provides; a
|
|
1196
|
+
# pre-serialized String body is treated as op-free (and therefore
|
|
1197
|
+
# retryable), so callers handing `request` a raw JSON string for a
|
|
1198
|
+
# PUT-with-op would bypass this guard.
|
|
1199
|
+
# @param body [Object] the request body.
|
|
1200
|
+
# @return [Boolean]
|
|
1201
|
+
def body_carries_atomic_op?(body)
|
|
1202
|
+
return false unless body.is_a?(Hash)
|
|
1203
|
+
body.any? { |_k, v| v.is_a?(Hash) && (v.key?("__op") || v.key?(:__op)) }
|
|
1006
1204
|
end
|
|
1007
1205
|
|
|
1008
1206
|
# Send a GET request.
|
|
@@ -85,22 +85,32 @@ module Parse
|
|
|
85
85
|
class Installation < Parse::Object
|
|
86
86
|
parse_class Parse::Model::CLASS_INSTALLATION
|
|
87
87
|
|
|
88
|
+
# The CLP operations Parse Server does NOT honor on `_Installation`:
|
|
89
|
+
# `find` and `delete` are hardcoded master-key-only at the REST layer,
|
|
90
|
+
# and `create`/`update` are gated on the `X-Parse-Installation-Id`
|
|
91
|
+
# header rather than CLP. Setting CLP for any of these either does
|
|
92
|
+
# nothing or breaks the SDK's device-registration flow, so the advisory
|
|
93
|
+
# fires only for them. `get`, `count`, `addField`, and `protectedFields`
|
|
94
|
+
# respond to CLP normally and are configured without a warning.
|
|
95
|
+
INEFFECTIVE_CLP_OPERATIONS = %i[find create update delete].freeze
|
|
96
|
+
|
|
88
97
|
class << self
|
|
89
|
-
# Override {Parse::Object.set_clp} on `_Installation` so that
|
|
90
|
-
# attempt to change CLP
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
# most CLP changes here either do nothing or break the SDK's
|
|
95
|
-
# device-registration flow. Behavior is otherwise unchanged.
|
|
98
|
+
# Override {Parse::Object.set_clp} on `_Installation` so that an
|
|
99
|
+
# attempt to change CLP for an operation the server ignores emits a
|
|
100
|
+
# one-time advisory. CLP changes for `get` / `count` / `addField`
|
|
101
|
+
# take effect normally and are applied without a warning. Behavior is
|
|
102
|
+
# otherwise unchanged.
|
|
96
103
|
def set_clp(operation, **opts)
|
|
97
|
-
_warn_about_installation_clp!(:set_clp, operation)
|
|
104
|
+
_warn_about_installation_clp!(:set_clp, operation) if _installation_clp_ineffective?(operation)
|
|
98
105
|
super
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
# Same advisory for the bulk-config DSL
|
|
108
|
+
# Same advisory for the bulk-config DSL — warn only about the keys that
|
|
109
|
+
# name an ineffective operation, and stay silent when the caller only
|
|
110
|
+
# touches the operations CLP actually controls.
|
|
102
111
|
def set_class_access(**ops_to_access)
|
|
103
|
-
|
|
112
|
+
offending = ops_to_access.keys.select { |op| _installation_clp_ineffective?(op) }
|
|
113
|
+
_warn_about_installation_clp!(:set_class_access, offending) unless offending.empty?
|
|
104
114
|
super
|
|
105
115
|
end
|
|
106
116
|
|
|
@@ -127,6 +137,13 @@ module Parse
|
|
|
127
137
|
super
|
|
128
138
|
end
|
|
129
139
|
|
|
140
|
+
# @!visibility private
|
|
141
|
+
# Whether a CLP operation is one Parse Server ignores on `_Installation`
|
|
142
|
+
# (and therefore worth warning about). Normalizes Strings/Symbols.
|
|
143
|
+
def _installation_clp_ineffective?(operation)
|
|
144
|
+
INEFFECTIVE_CLP_OPERATIONS.include?(operation.to_s.to_sym)
|
|
145
|
+
end
|
|
146
|
+
|
|
130
147
|
# @!visibility private
|
|
131
148
|
def _warn_about_installation_clp!(method, detail)
|
|
132
149
|
return if @_installation_clp_warned
|
|
@@ -654,6 +654,10 @@ module Parse
|
|
|
654
654
|
|
|
655
655
|
# Request a password reset for this user
|
|
656
656
|
# @return [Boolean] true if it was successful requested. false otherwise.
|
|
657
|
+
# @raise [Parse::Error::ServiceUnavailableError] if Parse Server returns a
|
|
658
|
+
# 500/503 (e.g. no emailAdapter / publicServerURL configured). Callers that
|
|
659
|
+
# branch on the Boolean should rescue this; `if request_password_reset(email)`
|
|
660
|
+
# is not exception-safe on a misconfigured server.
|
|
657
661
|
# @see Parse::User.request_password_reset
|
|
658
662
|
def request_password_reset
|
|
659
663
|
return false if email.nil?
|
|
@@ -1061,6 +1065,10 @@ module Parse
|
|
|
1061
1065
|
# Parse::User.request_password_reset("user@example.com")
|
|
1062
1066
|
# @param email [String] The user's email address.
|
|
1063
1067
|
# @return [Boolean] True/false if successful.
|
|
1068
|
+
# @raise [Parse::Error::ServiceUnavailableError] if Parse Server returns a
|
|
1069
|
+
# 500/503 (e.g. no emailAdapter / publicServerURL configured). Callers that
|
|
1070
|
+
# branch on the Boolean should rescue this; `if request_password_reset(email)`
|
|
1071
|
+
# is not exception-safe on a misconfigured server.
|
|
1064
1072
|
def self.request_password_reset(email)
|
|
1065
1073
|
email = email.email if email.is_a?(Parse::User)
|
|
1066
1074
|
return false if email.blank?
|
|
@@ -409,6 +409,13 @@ module Parse
|
|
|
409
409
|
winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
|
|
410
410
|
raise unless winner
|
|
411
411
|
winner
|
|
412
|
+
rescue Parse::Error::DuplicateRequestError
|
|
413
|
+
# A transparently-retried create landed but lost its response;
|
|
414
|
+
# server idempotency rejected the replay. Re-find the row the
|
|
415
|
+
# original attempt created and return it.
|
|
416
|
+
winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
|
|
417
|
+
raise unless winner
|
|
418
|
+
winner
|
|
412
419
|
end
|
|
413
420
|
end
|
|
414
421
|
end
|
|
@@ -471,6 +478,12 @@ module Parse
|
|
|
471
478
|
winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
|
|
472
479
|
raise unless winner
|
|
473
480
|
obj = winner
|
|
481
|
+
rescue Parse::Error::DuplicateRequestError
|
|
482
|
+
# See #first_or_create! — recover the row a retried create
|
|
483
|
+
# already landed (it already carries resource_attrs).
|
|
484
|
+
winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
|
|
485
|
+
raise unless winner
|
|
486
|
+
obj = winner
|
|
474
487
|
end
|
|
475
488
|
end
|
|
476
489
|
|
|
@@ -480,7 +493,15 @@ module Parse
|
|
|
480
493
|
end
|
|
481
494
|
if has_changes
|
|
482
495
|
obj.apply_attributes!(resource_attrs, dirty_track: true)
|
|
483
|
-
|
|
496
|
+
begin
|
|
497
|
+
session ? obj.save!(session: session) : obj.save!
|
|
498
|
+
rescue Parse::Error::DuplicateRequestError
|
|
499
|
+
# A retried update (PUT) landed but lost its response; re-find
|
|
500
|
+
# the now-updated row and return it.
|
|
501
|
+
winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
|
|
502
|
+
raise unless winner
|
|
503
|
+
obj = winner
|
|
504
|
+
end
|
|
484
505
|
end
|
|
485
506
|
end
|
|
486
507
|
|
|
@@ -617,6 +638,21 @@ module Parse
|
|
|
617
638
|
_scoped_first(query_attrs, session: session, master_key: master_key)
|
|
618
639
|
end
|
|
619
640
|
|
|
641
|
+
# @!visibility private
|
|
642
|
+
# Recovery for a request-id idempotency duplicate
|
|
643
|
+
# ({Parse::Error::DuplicateRequestError}, Parse code 159): the create's
|
|
644
|
+
# POST was rejected as a duplicate, which means a prior — transparently
|
|
645
|
+
# retried — attempt already created the row but lost its response. Re-find
|
|
646
|
+
# the row by the identifying `query_attrs` and return it (the row was
|
|
647
|
+
# created with `query_attrs.merge(resource_attrs)`, so it already carries
|
|
648
|
+
# the resource attributes). Returns nil if it cannot be located, in which
|
|
649
|
+
# case the caller re-raises the original error. Relies on `query_attrs`
|
|
650
|
+
# actually identifying the row — the same assumption the duplicate-value
|
|
651
|
+
# recovery and `first_or_create!`'s own find already make.
|
|
652
|
+
def _recover_from_duplicate_request(query_attrs, session: nil, master_key: nil)
|
|
653
|
+
_scoped_first(query_attrs, session: session, master_key: master_key)
|
|
654
|
+
end
|
|
655
|
+
|
|
620
656
|
# @!visibility private
|
|
621
657
|
# The pre-synchronize behavior of `first_or_create!`, factored out so
|
|
622
658
|
# the synchronize wrapper can short-circuit when disabled. Preserves
|
|
@@ -627,7 +663,13 @@ module Parse
|
|
|
627
663
|
obj = self.new query_attrs.merge(resource_attrs)
|
|
628
664
|
end
|
|
629
665
|
if obj.new?
|
|
630
|
-
|
|
666
|
+
begin
|
|
667
|
+
session ? obj.save!(session: session) : obj.save!
|
|
668
|
+
rescue Parse::Error::DuplicateRequestError
|
|
669
|
+
winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
|
|
670
|
+
raise unless winner
|
|
671
|
+
obj = winner
|
|
672
|
+
end
|
|
631
673
|
end
|
|
632
674
|
obj
|
|
633
675
|
end
|
|
@@ -637,14 +679,26 @@ module Parse
|
|
|
637
679
|
obj = _scoped_first(query_attrs, session: session, master_key: master_key)
|
|
638
680
|
if obj.nil?
|
|
639
681
|
obj = self.new query_attrs.merge(resource_attrs)
|
|
640
|
-
|
|
682
|
+
begin
|
|
683
|
+
session ? obj.save!(session: session) : obj.save!
|
|
684
|
+
rescue Parse::Error::DuplicateRequestError
|
|
685
|
+
winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
|
|
686
|
+
raise unless winner
|
|
687
|
+
obj = winner
|
|
688
|
+
end
|
|
641
689
|
elsif !resource_attrs.empty?
|
|
642
690
|
has_changes = resource_attrs.any? do |key, value|
|
|
643
691
|
obj.respond_to?(key) && obj.send(key) != value
|
|
644
692
|
end
|
|
645
693
|
if has_changes
|
|
646
694
|
obj.apply_attributes!(resource_attrs, dirty_track: true)
|
|
647
|
-
|
|
695
|
+
begin
|
|
696
|
+
session ? obj.save!(session: session) : obj.save!
|
|
697
|
+
rescue Parse::Error::DuplicateRequestError
|
|
698
|
+
winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
|
|
699
|
+
raise unless winner
|
|
700
|
+
obj = winner
|
|
701
|
+
end
|
|
648
702
|
end
|
|
649
703
|
end
|
|
650
704
|
obj
|
|
@@ -47,27 +47,32 @@ module Parse
|
|
|
47
47
|
# provider can happen any time before the first save. Declaration
|
|
48
48
|
# never makes a network call.
|
|
49
49
|
#
|
|
50
|
-
# == Single vector per record
|
|
50
|
+
# == Single vector per record
|
|
51
51
|
#
|
|
52
52
|
# `embed` produces exactly one vector per record. All declared
|
|
53
53
|
# source fields are concatenated (joined with "\n\n", blank values
|
|
54
|
-
# skipped) and sent to the provider as a single string.
|
|
55
|
-
#
|
|
56
|
-
# exceeds the provider's per-call token budget
|
|
57
|
-
# provider-side, and the
|
|
58
|
-
# leading portion of the document.
|
|
54
|
+
# skipped) and sent to the provider as a single string. This
|
|
55
|
+
# directive is one-vector-per-record by design: long source text
|
|
56
|
+
# whose concatenation exceeds the provider's per-call token budget
|
|
57
|
+
# is truncated provider-side, and the stored vector represents only
|
|
58
|
+
# the leading portion of the document.
|
|
59
59
|
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
60
|
+
# Chunking happens at RETRIEVAL time, not embed time. As of v5.2 the
|
|
61
|
+
# SDK ships {Parse::Retrieval.retrieve} and the `semantic_search`
|
|
62
|
+
# agent tool, which fetch the top-k whole records and split each
|
|
63
|
+
# record's text field into overlapping chunks for presentation
|
|
64
|
+
# (every chunk inherits its parent record's single score). That is
|
|
65
|
+
# presentation chunking — it does not change how embeddings are
|
|
66
|
+
# computed here.
|
|
67
|
+
#
|
|
68
|
+
# If you instead want each passage to have its OWN embedding (true
|
|
69
|
+
# embed-time chunking), keep one of these patterns:
|
|
62
70
|
#
|
|
63
71
|
# 1. Pre-chunk client-side and write each chunk as its own
|
|
64
72
|
# Parse::Object record with its own `embed` declaration.
|
|
65
|
-
# 2. Maintain a dedicated
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# A built-in chunker + `semantic_search` agent tool are scheduled
|
|
70
|
-
# for v5.1.
|
|
73
|
+
# 2. Maintain a dedicated chunk subclass that belongs_to the parent
|
|
74
|
+
# record, with `embed :content, into: :embedding` on the chunk
|
|
75
|
+
# class itself.
|
|
71
76
|
module EmbedManaged
|
|
72
77
|
# Raised when user code tries to assign directly to a vector
|
|
73
78
|
# property that's managed by an {.embed} declaration. The intent
|