Skip to content

Commit

Permalink
Add .retriable feature to Http - Rebased (#775)
Browse files Browse the repository at this point in the history
* Delay by default backs backs off over time
* Maximum delay time
* Exceptions to retry from
* Status codes to retry from
* Custom retry logic
* Respect Retry-After header if present
* on_retry callback

---------

Co-authored-by: Alexey Zapparov <[email protected]>
Co-authored-by: Bert Goethals <[email protected]>
  • Loading branch information
3 people authored Jul 22, 2024
1 parent cb13273 commit d6313d7
Show file tree
Hide file tree
Showing 12 changed files with 731 additions and 16 deletions.
39 changes: 24 additions & 15 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100`
# on 2023-10-18 14:33:19 UTC using RuboCop version 1.57.1.
# on 2024-01-05 21:03:44 UTC using RuboCop version 1.57.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -54,17 +54,20 @@ Layout/SpaceInsideHashLiteralBraces:
- 'spec/support/http_handling_shared.rb'
- 'spec/support/ssl_helper.rb'

# Offense count: 6
# Offense count: 3
# Configuration parameters: AllowedParentClasses.
Lint/MissingSuper:
Exclude:
- 'lib/http/features/auto_deflate.rb'
- 'lib/http/features/instrumentation.rb'
- 'lib/http/features/logging.rb'
- 'lib/http/features/normalize_uri.rb'
- 'spec/lib/http/client_spec.rb'
- 'spec/lib/http/features/instrumentation_spec.rb'

# Offense count: 6
# Configuration parameters: AllowComments, AllowNil.
Lint/SuppressedException:
Exclude:
- 'spec/lib/http/retriable/performer_spec.rb'

# Offense count: 8
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
Metrics/AbcSize:
Expand Down Expand Up @@ -93,7 +96,7 @@ Metrics/CyclomaticComplexity:
- 'lib/http/chainable.rb'
- 'lib/http/client.rb'

# Offense count: 15
# Offense count: 16
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Exclude:
Expand All @@ -106,6 +109,7 @@ Metrics/MethodLength:
- 'lib/http/redirector.rb'
- 'lib/http/response.rb'
- 'lib/http/response/body.rb'
- 'lib/http/retriable/performer.rb'
- 'lib/http/timeout/global.rb'

# Offense count: 1
Expand Down Expand Up @@ -183,7 +187,7 @@ RSpec/DescribeMethod:
- 'spec/lib/http/options/new_spec.rb'
- 'spec/lib/http/options/proxy_spec.rb'

# Offense count: 132
# Offense count: 136
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SkipBlocks, EnforcedStyle.
# SupportedStyles: described_class, explicit
Expand All @@ -208,7 +212,7 @@ RSpec/DescribedClass:
- 'spec/lib/http/uri/normalizer_spec.rb'
- 'spec/lib/http_spec.rb'

# Offense count: 41
# Offense count: 49
# Configuration parameters: Max, CountAsOne.
RSpec/ExampleLength:
Exclude:
Expand All @@ -219,6 +223,7 @@ RSpec/ExampleLength:
- 'spec/lib/http/redirector_spec.rb'
- 'spec/lib/http/request/body_spec.rb'
- 'spec/lib/http/response/body_spec.rb'
- 'spec/lib/http/retriable/performer_spec.rb'
- 'spec/lib/http/uri_spec.rb'
- 'spec/lib/http_spec.rb'
- 'spec/support/http_handling_shared.rb'
Expand Down Expand Up @@ -256,13 +261,13 @@ RSpec/InstanceVariable:
Exclude:
- 'spec/lib/http/redirector_spec.rb'

# Offense count: 40
# Offense count: 43
# Configuration parameters: .
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
EnforcedStyle: receive

# Offense count: 59
# Offense count: 74
# Configuration parameters: Max.
RSpec/MultipleExpectations:
Exclude:
Expand All @@ -280,15 +285,18 @@ RSpec/MultipleExpectations:
- 'spec/lib/http/redirector_spec.rb'
- 'spec/lib/http/response/body_spec.rb'
- 'spec/lib/http/response/parser_spec.rb'
- 'spec/lib/http/retriable/delay_calculator_spec.rb'
- 'spec/lib/http/retriable/performer_spec.rb'
- 'spec/lib/http/uri_spec.rb'
- 'spec/lib/http_spec.rb'
- 'spec/support/http_handling_shared.rb'

# Offense count: 8
# Offense count: 9
# Configuration parameters: AllowSubject, Max.
RSpec/MultipleMemoizedHelpers:
Exclude:
- 'spec/lib/http/response_spec.rb'
- 'spec/lib/http/retriable/performer_spec.rb'
- 'spec/lib/http/uri_spec.rb'

# Offense count: 58
Expand All @@ -305,13 +313,14 @@ RSpec/NamedSubject:
- 'spec/lib/http/response/status_spec.rb'
- 'spec/lib/http/response_spec.rb'

# Offense count: 15
# Offense count: 16
# Configuration parameters: Max, AllowedGroups.
RSpec/NestedGroups:
Exclude:
- 'spec/lib/http/client_spec.rb'
- 'spec/lib/http/redirector_spec.rb'
- 'spec/lib/http/request_spec.rb'
- 'spec/lib/http/retriable/performer_spec.rb'
- 'spec/lib/http_spec.rb'

# Offense count: 2
Expand Down Expand Up @@ -355,7 +364,7 @@ RSpec/VariableName:
Exclude:
- 'spec/lib/http/headers_spec.rb'

# Offense count: 11
# Offense count: 12
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Exclude:
Expand All @@ -364,6 +373,7 @@ RSpec/VerifiedDoubles:
- 'spec/lib/http/response/body_spec.rb'
- 'spec/lib/http/response/status_spec.rb'
- 'spec/lib/http/response_spec.rb'
- 'spec/lib/http/retriable/performer_spec.rb'
- 'spec/lib/http_spec.rb'

# Offense count: 1
Expand Down Expand Up @@ -404,14 +414,13 @@ Style/Encoding:
- 'spec/lib/http_spec.rb'
- 'spec/support/dummy_server/servlet.rb'

# Offense count: 17
# Offense count: 16
# Configuration parameters: SuspiciousParamNames, Allowlist.
# SuspiciousParamNames: options, opts, args, params, parameters
Style/OptionHash:
Exclude:
- 'lib/http/chainable.rb'
- 'lib/http/client.rb'
- 'lib/http/feature.rb'
- 'lib/http/options.rb'
- 'lib/http/redirector.rb'
- 'lib/http/timeout/null.rb'
Expand Down
1 change: 1 addition & 0 deletions lib/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "http/timeout/global"
require "http/chainable"
require "http/client"
require "http/retriable/client"
require "http/connection"
require "http/options"
require "http/feature"
Expand Down
22 changes: 22 additions & 0 deletions lib/http/chainable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,28 @@ def use(*features)
branch default_options.with_features(features)
end

# Returns retriable client instance, which retries requests if they failed
# due to some socket errors or response status is `5xx`.
#
# @example Usage
#
# # Retry max 5 times with randomly growing delay between retries
# HTTP.retriable.get(url)
#
# # Retry max 3 times with randomly growing delay between retries
# HTTP.retriable(times: 3).get(url)
#
# # Retry max 3 times with 1 sec delay between retries
# HTTP.retriable(times: 3, delay: proc { 1 }).get(url)
#
# # Retry max 3 times with geometrically progressed delay between retries
# HTTP.retriable(times: 3, delay: proc { |i| 1 + i*i }).get(url)
#
# @param (see Performer#initialize)
def retriable(**options)
Retriable::Client.new(Retriable::Performer.new(options), default_options)
end

private

# :nodoc:
Expand Down
37 changes: 37 additions & 0 deletions lib/http/retriable/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require "http/retriable/performer"

module HTTP
module Retriable
# Retriable version of HTTP::Client.
#
# @see http://www.rubydoc.info/gems/http/HTTP/Client
class Client < HTTP::Client
# @param [Performer] performer
# @param [HTTP::Options, Hash] options
def initialize(performer, options)
@performer = performer
super(options)
end

# Overriden version of `HTTP::Client#make_request`.
#
# Monitors request/response phase with performer.
#
# @see http://www.rubydoc.info/gems/http/HTTP/Client:perform
def perform(req, options)
@performer.perform(self, req) { super(req, options) }
end

private

# Overriden version of `HTTP::Chainable#branch`.
#
# @return [HTTP::Retriable::Client]
def branch(options)
Retriable::Client.new(@performer, options)
end
end
end
end
64 changes: 64 additions & 0 deletions lib/http/retriable/delay_calculator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module HTTP
module Retriable
# @api private
class DelayCalculator
def initialize(opts)
@max_delay = opts.fetch(:max_delay, Float::MAX).to_f
if (delay = opts[:delay]).respond_to?(:call)
@delay_proc = opts.fetch(:delay)
else
@delay = delay
end
end

def call(iteration, response)
delay = if response && (retry_header = response.headers["Retry-After"])
delay_from_retry_header(retry_header)
else
calculate_delay_from_iteration(iteration)
end

ensure_dealy_in_bounds(delay)
end

RFC2822_DATE_REGEX = /^
(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+
(?:0[1-9]|[1-2]?[0-9]|3[01])\s+
(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+
(?:19[0-9]{2}|[2-9][0-9]{3})\s+
(?:2[0-3]|[0-1][0-9]):(?:[0-5][0-9]):(?:60|[0-5][0-9])\s+
GMT
$/x

# Spec for Retry-After header
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
def delay_from_retry_header(value)
value = value.to_s.strip

case value
when RFC2822_DATE_REGEX then DateTime.rfc2822(value).to_time - Time.now.utc
when /^\d+$/ then value.to_i
else 0
end
end

def calculate_delay_from_iteration(iteration)
if @delay_proc
@delay_proc.call(iteration)
elsif @delay
@delay
else
delay = (2**(iteration - 1)) - 1
delay_noise = rand
delay + delay_noise
end
end

def ensure_dealy_in_bounds(delay)
delay.clamp(0, @max_delay)
end
end
end
end
14 changes: 14 additions & 0 deletions lib/http/retriable/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module HTTP
# Retriable performance ran out of attempts
class OutOfRetriesError < Error
attr_accessor :response

attr_writer :cause

def cause
@cause || super
end
end
end
Loading

0 comments on commit d6313d7

Please sign in to comment.