Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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