class Gems::RetryHandler
Sends a request again when the server, or the network, turns it away
A 429 Too Many Requests says a rate limiter turned the request away before it reached the endpoint, so sending it again is safe for any request, even one that is not idempotent, such as pushing a gem: the endpoint never saw the attempt before it.
A 502 Bad Gateway, a 503 Service Unavailable, a 504 Gateway Timeout, and a request lost to the network say less. A 502 or 504 comes from a gateway that could not read an answer from the origin behind it, a 503 can come from a CDN that gave up waiting on an origin still working on the request, as Fastly, which RubyGems.org is served through, answers a first byte timeout, and a request lost to the network may have arrived and been acted on before its answer went missing. The origin may have acted on the request in each case, so a request that asks the server to do something, such as yanking a version, is not sent again for one: sending a yank again after the origin acted on it answers with the error of the version the attempt before it yanked, which would be raised in place of the success the caller was owed.
Whether a request is one of those is the caller’s to say, with the ‘retry_unanswered` argument of {#handle}: {Client} says whether the method of the request is safe, since a request such as yanking a version cannot be sent a second time to find out whether the server acted on the first, and a caller that knows better, such as the trusted publishing token exchange, says what it knows instead. A 429 is sent again whatever the caller says, since no request reached the endpoint.
A request is sent again twice by default, which is enough for the moment of rate limiting or the lost connection that a retry is for, and {#max_retries} of zero turns retrying off, so that a request that was turned away raises {TooManyRequests}, {BadGateway}, {ServiceUnavailable}, {GatewayTimeout}, or {NetworkError} rather than pausing the caller’s thread. A response asking to wait longer than {#max_retry_delay} raises whatever {#max_retries} is, so the wait a caller can be held for is theirs to cap.
@api private
Constants
- DEFAULT_MAX_RETRIES
-
Default number of times a request is sent again
- DEFAULT_MAX_RETRY_DELAY
-
Default longest a request waits before it is sent again, in seconds
Attributes
The source of the randomness the backoff is jittered with @api private @return [Random, Class<Random>] the source of randomness @example Get the source of randomness
handler.random
Public Class Methods
Initialize a new RetryHandler
@api private @param max_retries [Integer] the number of times a request is sent again @param max_retry_delay [Numeric] the longest a request waits before it is sent again, in seconds @param random [Random, Class<Random>] the source of the randomness the backoff is jittered with, which
answers `rand` with a Float between zero and one
@return [RetryHandler] a new instance @example Create a retry handler
handler = Gems::RetryHandler.new(max_retries: 3)
# File lib/gems/retry_handler.rb, line 100 def initialize(max_retries: DEFAULT_MAX_RETRIES, max_retry_delay: DEFAULT_MAX_RETRY_DELAY, random: Random) self.max_retries = max_retries self.max_retry_delay = max_retry_delay @random = random end
Public Instance Methods
Send a request until the server and the network stop turning it away
The block is called again for each retry, so a request that was redirected is followed again from the start. The wait between attempts is the one the ‘Retry-After` header of the response asks for, and doubles from one second otherwise, up to {#max_retry_delay}, jittered as {#backoff} describes; that is the wait after a {NetworkError} too, since a request that never reached the server has no response to read a wait from. A response that asks to wait longer than {#max_retry_delay} is returned rather than waited for, so that the caller is told what happened instead of pausing for as long as the server likes, and a {NetworkError} that has no retry left is raised as it was.
A request a rate limiter turned away is told apart from one the origin may have acted on, since they are not equally safe to send again: a 429 says the request never reached the endpoint, where a 502, 503, or 504, or a request lost to the network, may have been acted on before the answer went missing. The caller says whether its request is safe to send again for those, so that the trusted publishing token exchange can be although it is a POST. A 429 is sent again for every request.
The request is not named, since the block builds one of its own each time it is called, so that a body read as a stream is sent from the start (see {Client#perform}).
@api private @param retry_unanswered [Boolean] whether a request the origin may have acted on is sent again @yield the response, each time the request is sent @return [Net::HTTPResponse] the last response @raise [NetworkError] if the request is lost to the network and is not sent again @example Send a safe request, which is safe to send again whatever happened to it
handler.handle(retry_unanswered: true) { connection.perform(request:) }
@example Send a request that is safe to send again only when a rate limiter turned it away
handler.handle(retry_unanswered: false) { connection.perform(request:) }
# File lib/gems/retry_handler.rb, line 134 def handle(retry_unanswered:) retries = 0 loop do response = attempt(retries:, retry_unanswered:) { yield } return response if response retries += 1 end end