parse-stack-next 5.7.3 → 5.7.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4017efda309de14faf52b86e26274cc097ee029d282a975f0144e324ac68250
4
- data.tar.gz: e46cbf422955acc46ef7ceff8348a564712a9109051511dfec1bd2dce96356f0
3
+ metadata.gz: ef10cccc964e68942152a64d489ea8044a99eb0b76a3fea00d75c311ddf94d23
4
+ data.tar.gz: 357793ece65c1bb58147e160fb8c95782e3496deac3a1b843a82faf9dcd06737
5
5
  SHA512:
6
- metadata.gz: 1cee46e278c9ad77fab0d4f2869636c623b43e9e1b7d2ea0ea77f92be9ad8f6631bb6e5f92421997f92d42ba73470db659486595fe0993a50de93140eb6051fb
7
- data.tar.gz: 36322b4f93132dd277476d999fc71511324d5a726979896ae08e4f6aea31d3aa4454fd41910a1c7758d229c9ff1000cb43de12ffaf8f795403e1ab7cf96f71ef
6
+ metadata.gz: 44ed4c55b5388cd068c723f01bfa02c3efa4fafe61adfa328197a76d0bcf4006eeab313d7d3a42a8b2088b43a4a6a25bd9b89868fe183daa744c3f324c0ef0f2
7
+ data.tar.gz: 721eebb0297114c0e439babab1c0c39bb1bb9697788ff4197b6d9cf072d80b1fb7d3783f1c629ea0062314321b0695565bd7e252c4e7c39e1940bcf93adabcb7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  ## parse-stack-next Changelog
2
2
 
3
+ ### 5.7.4
4
+
5
+ #### Reset connections are retried instead of surfacing a raw Faraday error
6
+
7
+ A focused fix release for the request retry mechanism. A stale keep-alive
8
+ connection (a pooled persistent connection closed by the server or a load
9
+ balancer after idling) failed the next request with a raw
10
+ `Faraday::ConnectionFailed` / `Errno::ECONNRESET` instead of retrying, even
11
+ though an immediate re-send on a fresh connection succeeds. Reset connections
12
+ now retry under the same idempotency rules as read timeouts.
13
+
14
+ - **FIXED**: `Parse::Client#request` now rescues `Faraday::ConnectionFailed`
15
+ and inspects the wrapped cause. Reset-class causes (`Errno::ECONNRESET`,
16
+ `Errno::EPIPE`, `Errno::ECONNABORTED`, and the `EOFError` raised when the
17
+ remote end closes a keep-alive socket cleanly) are transient, so idempotent
18
+ requests (GET, DELETE, op-free PUT, and any write covered by asserted
19
+ server-side request-id dedup) retry with the standard backoff. Connection
20
+ refused and DNS failures keep the previous fail-fast behavior and propagate
21
+ the raw `Faraday::ConnectionFailed` with no retry latency.
22
+ - **CHANGED**: A reset connection that persists through the whole retry budget
23
+ now raises `Parse::Error::ConnectionError` (consistent with the read-timeout
24
+ path) instead of the raw `Faraday::ConnectionFailed`. Code that rescued
25
+ `Faraday::ConnectionFailed` to catch resets should rescue
26
+ `Parse::Error::ConnectionError` instead; refused and DNS failures still
27
+ raise `Faraday::ConnectionFailed`.
28
+
3
29
  ### 5.7.3
4
30
 
5
31
  #### Stored values can no longer drive the operator's terminal
data/lib/parse/client.rb CHANGED
@@ -1424,6 +1424,30 @@ module Parse
1424
1424
  retry
1425
1425
  end
1426
1426
  raise
1427
+ rescue Faraday::ConnectionFailed => e
1428
+ # `Faraday::ConnectionFailed` covers two very different failures under
1429
+ # one class, so it is split on the wrapped cause (see
1430
+ # #connection_reset_error?):
1431
+ #
1432
+ # - RESET mid-flight (`Errno::ECONNRESET` / `Errno::EPIPE` /
1433
+ # `Errno::ECONNABORTED` / `EOFError`): the classic stale
1434
+ # keep-alive failure. A pooled persistent connection idled past
1435
+ # the server's (or an LB's) keep-alive window and was closed
1436
+ # remotely, and the next request on it dies at the socket.
1437
+ # Transient by nature (a fresh connection succeeds immediately),
1438
+ # so it retries under the same idempotency rules as a read
1439
+ # timeout: the outcome is unknown, so only idempotent requests
1440
+ # are re-sent.
1441
+ #
1442
+ # - REFUSED (and DNS failure): the server is down or misconfigured.
1443
+ # Retrying only adds backoff latency and `[Parse:Retry]` noise
1444
+ # before the inevitable error, so it propagates raw and fast.
1445
+ raise unless connection_reset_error?(e)
1446
+ if _retry_count > 0 && idempotent_retry?(method, body, headers)
1447
+ _retry_count = consume_retry_with_backoff(_retry_count, _retry_max, _request)
1448
+ retry
1449
+ end
1450
+ raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.message}"
1427
1451
  rescue Faraday::ClientError, Faraday::TimeoutError, Net::OpenTimeout => e
1428
1452
  # Request timed out mid-flight: the outcome is unknown (the server may
1429
1453
  # have received and applied the write but never answered), so only
@@ -1431,25 +1455,74 @@ module Parse
1431
1455
  #
1432
1456
  # Faraday 2.x raises `Faraday::TimeoutError` for a read timeout
1433
1457
  # (`Timeout::Error` / `Errno::ETIMEDOUT`); it subclasses `Faraday::Error`,
1434
- # not `ClientError`, so it must be listed explicitly to be caught. We
1435
- # deliberately do NOT catch `Faraday::ConnectionFailed` (connection
1436
- # refused/reset, plus the wrapped connect-timeout): refused is a
1437
- # non-transient "server down / misconfigured" failure, and auto-retrying
1438
- # it only adds backoff latency before the inevitable error. Broadening to
1439
- # reset connections safely (retry reset, fail fast on refused) is tracked
1440
- # as a follow-up.
1458
+ # not `ClientError`, so it must be listed explicitly to be caught.
1459
+ # `Faraday::ConnectionFailed` is handled in its own rescue above,
1460
+ # split into retry-reset / fail-fast-refused.
1441
1461
  if _retry_count > 0 && idempotent_retry?(method, body, headers)
1442
- warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}"
1443
- _retry_count -= 1
1444
- backoff_delay = RETRY_DELAY * (_retry_max - _retry_count)
1445
- _retry_delay = backoff_delay * (0.75 + rand * 0.5)
1446
- sleep _retry_delay if _retry_delay > 0
1462
+ _retry_count = consume_retry_with_backoff(_retry_count, _retry_max, _request)
1447
1463
  retry
1448
1464
  end
1449
1465
  raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.message}"
1450
1466
  end
1451
1467
  end
1452
1468
 
1469
+ # Consumes one attempt from the retry budget: logs the remaining count,
1470
+ # sleeps the linear backoff (RETRY_DELAY x attempt number, +/-25% jitter,
1471
+ # never zero), and returns the decremented budget. Shared by the
1472
+ # connection-reset and timeout rescue branches in {#request} so their
1473
+ # backoff behavior cannot drift apart; the `retry` keyword itself must
1474
+ # stay lexically inside each rescue clause, so it remains at the call
1475
+ # sites. The 429/503 branch keeps its own inline version because it also
1476
+ # honors a server-supplied Retry-After header.
1477
+ # @param retry_count [Integer] the remaining retry budget (must be > 0).
1478
+ # @param retry_max [Integer] the effective starting budget.
1479
+ # @param request [Parse::Request] the request being retried (for logging).
1480
+ # @return [Integer] the decremented retry budget.
1481
+ def consume_retry_with_backoff(retry_count, retry_max, request)
1482
+ warn "[Parse:Retry] Retries remaining #{retry_count} : #{request}"
1483
+ retry_count -= 1
1484
+ backoff_delay = RETRY_DELAY * (retry_max - retry_count)
1485
+ retry_delay = backoff_delay * (0.75 + rand * 0.5)
1486
+ sleep retry_delay if retry_delay > 0
1487
+ retry_count
1488
+ end
1489
+
1490
+ # The wrapped causes that mark a `Faraday::ConnectionFailed` as a RESET
1491
+ # connection (transient, retry-safe for idempotent requests) rather than a
1492
+ # REFUSED one (server down, fail fast). `EOFError` is what net/http raises
1493
+ # when the remote end closes a keep-alive socket cleanly between requests;
1494
+ # ECONNRESET/EPIPE/ECONNABORTED are the unclean variants.
1495
+ # @!visibility private
1496
+ CONNECTION_RESET_CAUSES = [
1497
+ Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, EOFError,
1498
+ ].freeze
1499
+
1500
+ # Message fallback for adapters that raise `Faraday::ConnectionFailed`
1501
+ # with the cause flattened into the message instead of wrapped.
1502
+ # @!visibility private
1503
+ CONNECTION_RESET_MESSAGE = /connection reset|broken pipe|end of file reached/i
1504
+
1505
+ # Whether a `Faraday::ConnectionFailed` was caused by a reset/dropped
1506
+ # connection (retryable) as opposed to connection-refused or a DNS
1507
+ # failure (fail fast). Walks the wrapped exception and the `#cause`
1508
+ # chain looking for a reset-class error.
1509
+ # @param error [Exception] the rescued `Faraday::ConnectionFailed`.
1510
+ # @return [Boolean]
1511
+ def connection_reset_error?(error)
1512
+ inner = error.respond_to?(:wrapped_exception) ? error.wrapped_exception : nil
1513
+ inner ||= error.cause
1514
+ seen = 0
1515
+ while inner && seen < 8
1516
+ return true if CONNECTION_RESET_CAUSES.any? { |klass| inner.is_a?(klass) }
1517
+ inner = inner.cause
1518
+ seen += 1
1519
+ end
1520
+ CONNECTION_RESET_MESSAGE.match?(error.message.to_s)
1521
+ end
1522
+
1523
+ private :consume_retry_with_backoff, :connection_reset_error?
1524
+ private_constant :CONNECTION_RESET_CAUSES, :CONNECTION_RESET_MESSAGE
1525
+
1453
1526
  # Whether a request whose outcome is UNKNOWN (a 500/503 or a dropped
1454
1527
  # connection) is safe to transparently re-send.
1455
1528
  #
@@ -6,6 +6,6 @@ module Parse
6
6
  # The Parse Server SDK for Ruby
7
7
  module Stack
8
8
  # The current version.
9
- VERSION = "5.7.3"
9
+ VERSION = "5.7.4"
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parse-stack-next
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.7.3
4
+ version: 5.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Curtin