Class: GraphWeaver::Retry

Inherits:
Object
  • Object
show all
Defined in:
lib/graph_weaver/retry.rb

Overview

Wraps any client/transport with configurable retries — it satisfies the same execute contract, so it layers over HTTP, Faraday, or anything else:

 client = GraphWeaver::Retry.new(
   GraphWeaver::Transport::HTTP.new(url),
   tries: 5,                        # total attempts, first included
   on: [GraphWeaver::TransportError, GraphWeaver::ServerError],
   backoff: :exponential,           # or :linear, or ->(attempt) { seconds }
   base: 0.5, max: 30,              # seconds; delays clamp at max:
   jitter: true,                    # randomize each delay by 50-100%
   retry_codes: ["THROTTLED"],      # also retry GraphQL errors by code
 )

What retries, by default:

- TransportError: always (the request never arrived)
- ServerError: only 5xx — a 4xx is a bug in the request, retrying
won't fix it. Override with retry_if: ->(error) { ... }
- responses whose GraphQL error codes intersect retry_codes: (off by
default — pass the codes your API uses for transient failures)

Exhausting tries re-raises the last error (or returns the last code-matched response).

Constant Summary collapse

BACKOFFS =
{
  exponential: ->(base, attempt) { base * (2**(attempt - 1)) },
  linear: ->(base, attempt) { base * attempt },
}.freeze
DEFAULT_RETRY_IF =

retry 5xx, not 4xx; everything else listed in on: retries

lambda do |error|
  !error.is_a?(GraphWeaver::ServerError) || error.status >= 500
end

Instance Method Summary collapse

Constructor Details

#initialize(client, tries: 3, on: [GraphWeaver::TransportError, GraphWeaver::ServerError], backoff: :exponential, base: 0.5, max: 30, jitter: true, retry_if: DEFAULT_RETRY_IF, retry_codes: [], sleeper: nil) ⇒ Retry

Returns a new instance of Retry.

Raises:

  • (ArgumentError)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/graph_weaver/retry.rb', line 40

def initialize(client, tries: 3, on: [GraphWeaver::TransportError, GraphWeaver::ServerError],
  backoff: :exponential, base: 0.5, max: 30, jitter: true, retry_if: DEFAULT_RETRY_IF,
  retry_codes: [], sleeper: nil)
  raise ArgumentError, "tries: must be >= 1" unless tries >= 1

  @client = client
  @tries = tries
  @on = on
  @backoff = if backoff.is_a?(Proc)
    ->(_base, attempt) { backoff.call(attempt) } # custom: ->(attempt) { seconds }
  else
    BACKOFFS.fetch(backoff) {
      raise ArgumentError, "backoff: must be :exponential, :linear, or a Proc, got #{backoff.inspect}"
    }
  end
  @base = base
  @max = max
  @jitter = jitter
  @retry_if = retry_if
  @retry_codes = retry_codes
  @sleeper = sleeper || ->(seconds) { sleep(seconds) }
end

Instance Method Details

#execute(query, variables: {}) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/graph_weaver/retry.rb', line 68

def execute(query, variables: {})
  attempt = 0

  loop do
    attempt += 1
    begin
      response = @client.execute(query, variables:)
      return response unless attempt < @tries && retryable_response?(response)
    rescue *@on => e
      raise if attempt >= @tries || !@retry_if.call(e)
    end

    @sleeper.call(delay(attempt))
  end
end

#urlObject

surface the wrapped transport's endpoint (schema-dump provenance)



64
65
66
# File 'lib/graph_weaver/retry.rb', line 64

def url
  @client.url if @client.respond_to?(:url)
end