Skip to content

Commit

Permalink
add retries with exponential backoff on network errors
Browse files Browse the repository at this point in the history
  • Loading branch information
anmarchenko committed Apr 16, 2024
1 parent 3030945 commit 0f7aad8
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 5 deletions.
1 change: 1 addition & 0 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ target :lib do
library "securerandom"
library "tmpdir"
library "fileutils"
library "socket"

repo_path "vendor/rbs"
library "ddtrace"
Expand Down
28 changes: 24 additions & 4 deletions lib/datadog/ci/transport/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "datadog/core/transport/http/adapters/net"
require "datadog/core/transport/http/env"
require "datadog/core/transport/request"
require "socket"

require_relative "gzip"
require_relative "../ext/transport"
Expand All @@ -20,6 +21,8 @@ class HTTP
:compress

DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
INITIAL_BACKOFF = 1

def initialize(host:, timeout: DEFAULT_TIMEOUT, port: nil, ssl: true, compress: false)
@host = host
Expand All @@ -29,7 +32,7 @@ def initialize(host:, timeout: DEFAULT_TIMEOUT, port: nil, ssl: true, compress:
@compress = compress.nil? ? false : compress
end

def request(path:, payload:, headers:, verb: "post")
def request(path:, payload:, headers:, verb: "post", retries: MAX_RETRIES, backoff: INITIAL_BACKOFF)
if compress
headers[Ext::Transport::HEADER_CONTENT_ENCODING] = Ext::Transport::CONTENT_ENCODING_GZIP
payload = Gzip.compress(payload)
Expand All @@ -41,9 +44,7 @@ def request(path:, payload:, headers:, verb: "post")
end

response = ResponseDecorator.new(
adapter.call(
build_env(path: path, payload: payload, headers: headers, verb: verb)
)
perform_http_call(path: path, payload: payload, headers: headers, verb: verb, retries: retries, backoff: backoff)
)

Datadog.logger.debug do
Expand All @@ -55,6 +56,25 @@ def request(path:, payload:, headers:, verb: "post")

private

def perform_http_call(path:, payload:, headers:, verb:, retries: MAX_RETRIES, backoff: INITIAL_BACKOFF)
adapter.call(
build_env(path: path, payload: payload, headers: headers, verb: verb)
)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, SocketError, Net::HTTPBadResponse => e
Datadog.logger.debug("Failed to send request with #{e} (#{e.message})")

if retries.positive?
sleep(backoff)

perform_http_call(
path: path, payload: payload, headers: headers, verb: verb, retries: retries - 1, backoff: backoff * 2
)
else
Datadog.logger.error("Failed to send request after #{MAX_RETRIES} retries")
raise e
end
end

def build_env(path:, payload:, headers:, verb:)
env = Datadog::Core::Transport::HTTP::Env.new(
Datadog::Core::Transport::Request.new
Expand Down
6 changes: 5 additions & 1 deletion sig/datadog/ci/transport/http.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ module Datadog
attr_reader compress: bool

DEFAULT_TIMEOUT: 30
MAX_RETRIES: 3
INITIAL_BACKOFF: 1

def initialize: (host: String, ?port: Integer?, ?ssl: bool, ?timeout: Integer, ?compress: bool) -> void

def request: (?verb: String, payload: String, headers: Hash[String, String], path: String) -> ResponseDecorator
def request: (?verb: String, payload: String, headers: Hash[String, String], path: String, ?retries: Integer, ?backoff: Integer) -> ResponseDecorator

private

def adapter: () -> Datadog::Core::Transport::HTTP::Adapters::Net

def build_env: (payload: String, headers: Hash[String, String], path: String, verb: String) -> Datadog::Core::Transport::HTTP::Env

def perform_http_call: (payload: String, headers: Hash[String, String], path: String, verb: String, ?retries: Integer, ?backoff: Integer) -> Datadog::Core::Transport::Response

class AdapterSettings
attr_reader hostname: String
attr_reader port: Integer?
Expand Down
27 changes: 27 additions & 0 deletions spec/datadog/ci/transport/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,32 @@
expect(response.code).to eq(200)
end
end

context "when request fails" do
let(:request_options) { {backoff: 0} }

context "when succeeds after retries" do
before do
expect(adapter).to receive(:call).and_raise(Errno::ECONNRESET).exactly(described_class::MAX_RETRIES).times
expect(adapter).to receive(:call).and_return(http_response)
end

it "produces a response" do
is_expected.to be_a_kind_of(described_class::ResponseDecorator)

expect(response.code).to eq(200)
end
end

context "when retries are exhausted" do
before do
expect(adapter).to receive(:call).and_raise(Errno::ECONNRESET).exactly(described_class::MAX_RETRIES + 1).times
end

it "raises" do
expect { response }.to raise_error(Errno::ECONNRESET)
end
end
end
end
end

0 comments on commit 0f7aad8

Please sign in to comment.