faraday-highly_available_retries 0.1.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0d99996c87cd7bfc2792430d4a7be1dc810889e2d59c1460da5c56bf18a32990
4
+ data.tar.gz: 4eae695335d5848847bc4e9a34295575dd9ca306c5380b79c77cba059bdc643a
5
+ SHA512:
6
+ metadata.gz: 6262767b39d59b4a7a4e1b1ecf31a8160ca6b0c3bda88d74e6ec49379ee8508d6e7e7235e581ba9c72c6429074a786def5c4369a80f9566e268c8b05789aa020
7
+ data.tar.gz: b2a1fc4a67ea0c9616194183e164a458dac26e93d186d2ee15f22f076820641404080c4d539cebd815fee2daae907baa5b410c7b6a9f51700fbda984a2a6c4d9
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog for `faraday-highly_available_retries`
2
+
3
+ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
+
5
+ **Note:** this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - Unreleased
8
+
9
+ * Initial release.
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 James Ebentier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Faraday Highly Available Retries
2
+
3
+ [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/invoca/faraday-highly_available_retries/ci)](https://github.com/invoca/faraday-highly_available_retries/actions?query=branch%3Amain)
4
+
5
+ An extension for the Faraday::Retry middleware allowing retries to failover across multiple resolved endpoints.
6
+
7
+ ## Why should I use this gem?
8
+
9
+ At the time a request is made, the list of hosts pulled from the connection and request is resolved to an array of IP addresses.
10
+ This list of IP addresses is then shuffled, and if a request fails to connect to the first IP address, it will try the next one.
11
+ And so on and so forth, until the retries are exhausted. If all of the IPs fail, then we cycle back to the first one and try again.
12
+
13
+ The reason this is impactful and should be used in conjunction with the retry middleware is that the retry middleware will
14
+ leave DNS resolution to the OS, which has the potential of caching results and not try resolving to a different IP address.
15
+ This means that if a DNS entry resolves to three IPs, `[A, B, C]`, the retry middleware may try all three retries against
16
+ `A`, where as this middleware guarantees that it will try `A`, `B`, and `C`.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'faraday-highly_available_retries'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```shell
29
+ bundle install
30
+ ```
31
+
32
+ Or install it yourself as:
33
+
34
+ ```shell
35
+ gem install faraday-highly_available_retries
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ This extension can be used on its own but is ultimately meant to be used in tandem with the [Faraday::Retry](https://github.com/lostisland/faraday-retry) middleware.
41
+ You should make sure that this middleware is added in the request stack ***after*** the retry middleware to ensure
42
+ it can alter the environment on retries.
43
+
44
+ When you configure the connection, you can do it a few different ways for the failover to work.
45
+
46
+ ### Setting the URL in the request
47
+
48
+ This simplest way is to make a generic Faraday connection, and pass the full URI in the request.
49
+ When this is done, the failover middleware will parse the URI and use the host and port to determine
50
+ the failover endpoints using DNS resolution.
51
+
52
+ ```ruby
53
+ require 'faraday/retry/failover'
54
+
55
+ conn = Faraday.new do |f|
56
+ f.request :retry, { max: 2 } # This makes sure we retry 2, resulting in 3 attempts total before failing finally
57
+ f.request :highly_available_retries
58
+ end
59
+
60
+ conn.get('https://api.invoca.net/api/2020-10-01/transaction/33.json')
61
+ ```
62
+
63
+ ### Setting the base URL in the connection
64
+
65
+ This is the same as the previous example, but the base URL is set in the connection. This is useful
66
+ when the base URL is always the same, and you have a DNS entry that will resolve to multiple IPs.
67
+
68
+ ```ruby
69
+ require 'faraday/retry/failover'
70
+
71
+ conn = Faraday.new('https://api.invoca.net') do |f|
72
+ f.request :retry, { max: 2 }
73
+ f.request :highly_available_retries
74
+ end
75
+
76
+ conn.get('/api/2020-10-01/transaction/33.json')
77
+ ```
78
+
79
+ ### Specifying multiple hosts
80
+
81
+ This is the most useful when you already have multiple IPs or separate hostnames that you want to
82
+ use for failover. The hosts list provided can contain hostnames and IPs both with and without ports.
83
+
84
+ ```ruby
85
+ conn = Faraday.new do |f|
86
+ f.request :retry, { max: 2 }
87
+ f.request :highly_available_retries, { hosts: ['api.invoca.net', 'api.invoca.com'] }
88
+ end
89
+
90
+ conn.get('/api/2020-10-01/transaction/33.json')
91
+ ```
92
+
93
+ ## Development
94
+
95
+ After checking out the repo, run `bin/setup` to install dependencies.
96
+
97
+ Then, run `bin/test` to run the tests.
98
+
99
+ To install this gem onto your local machine, run `rake build`.
100
+
101
+ To release a new version, make a commit with a message such as "Bumped to 0.0.2" and then run `rake release`.
102
+ See how it works [here](https://bundler.io/guides/creating_gem.html#releasing-the-gem).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/invoca/faraday-highly_available_retries).
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+
5
+ require_relative 'ip_validator'
6
+
7
+ module Faraday
8
+ module HighlyAvailableRetries
9
+ class Endpoint
10
+ class DNSResolutionError < StandardError; end
11
+
12
+ include Comparable
13
+
14
+ class << self
15
+ def from_host_and_port(host, port)
16
+ host_ips = Socket.getaddrinfo(host, nil, Socket::AF_INET, Socket::SOCK_STREAM) or raise DNSResolutionError, "getaddrinfo(#{host}) failed"
17
+ host_ips.any? or raise DNSResolutionError, "No IP addrs returned from #{host}"
18
+ host_ips.map { |host_ip| new(host_ip[3], port, hostname: host) }
19
+ end
20
+ end
21
+
22
+ attr_reader :ip_addr, :port, :request_host
23
+
24
+ def initialize(ip_addr, port, hostname: nil)
25
+ IpValidator.ip_addr?(ip_addr) or raise ArgumentError, "ip_addr must be a valid IP address but received #{ip_addr.inspect}"
26
+
27
+ @ip_addr = ip_addr
28
+ @port = port
29
+ @request_host = if hostname && !IpValidator.ip_addr?(hostname)
30
+ "#{hostname}:#{port}"
31
+ end
32
+ end
33
+
34
+ def <=>(other)
35
+ ip_addr <=> other.ip_addr &&
36
+ port <=> other.port &&
37
+ request_host <=> other.request_host
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+
5
+ module Faraday
6
+ module HighlyAvailableRetries
7
+ class IpValidator
8
+ class << self
9
+ def ip_addr?(addr)
10
+ IPAddr.new(addr)
11
+ true
12
+ rescue
13
+ false
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+
5
+ require_relative 'endpoint'
6
+ require_relative 'ip_validator'
7
+ require_relative 'options'
8
+
9
+ module Faraday
10
+ module HighlyAvailableRetries
11
+ # This class provides the main implementation for your middleware.
12
+ # Your middleware can implement any of the following methods:
13
+ # * on_request - called when the request is being prepared
14
+ # * on_complete - called when the response is being processed
15
+ #
16
+ # Optionally, you can also override the following methods from Faraday::Middleware
17
+ # * initialize(app, options = {}) - the initializer method
18
+ # * call(env) - the main middleware invocation method.
19
+ # This already calls on_request and on_complete, so you normally don't need to override it.
20
+ # You may need to in case you need to "wrap" the request or need more control
21
+ # (see "retry" middleware: https://github.com/lostisland/faraday/blob/main/lib/faraday/request/retry.rb#L142).
22
+ # IMPORTANT: Remember to call `@app.call(env)` or `super` to not interrupt the middleware chain!
23
+ class Middleware < Faraday::Middleware
24
+ FAILOVER_ENDPOINTS_ENV_KEY = :failover_endpoints
25
+ FAILOVER_COUNTER_ENV_KEY = :failover_counter
26
+ FAILOVER_ORIGINAL_HOST_ENV_KEY = :failover_original_hostname
27
+ FAILOVER_ORIGINAL_PORT_ENV_KEY = :failover_original_port
28
+
29
+ def initialize(app, options = nil)
30
+ super(app)
31
+ @options = Faraday::HighlyAvailableRetries::Options.from(options)
32
+ end
33
+
34
+ # This method will be called when the request is being prepared.
35
+ # You can alter it as you like, accessing things like request_body, request_headers, and more.
36
+ # Refer to Faraday::Env for a list of accessible fields:
37
+ # https://github.com/lostisland/faraday/blob/main/lib/faraday/options/env.rb
38
+ #
39
+ # @param env [Faraday::Env] the environment of the request being processed
40
+ def on_request(env)
41
+ # If we have not already resolved the failover endpoints, do so now
42
+ setup_resolution_env(env) unless env[FAILOVER_ENDPOINTS_ENV_KEY]&.any?
43
+
44
+ # If there is already a response associated with the request, it means that the request failed and we're retrying it.
45
+ # Increment the failover counter so we push on to the next endpoint
46
+ env[FAILOVER_COUNTER_ENV_KEY] += 1 unless env[:response].nil?
47
+
48
+ # Assign the next Failover endpoint to the request
49
+ setup_next_failover_endpoint(env)
50
+ end
51
+
52
+ private
53
+
54
+ def setup_next_failover_endpoint(env)
55
+ next_failover_endpoint = env[FAILOVER_ENDPOINTS_ENV_KEY][env[FAILOVER_COUNTER_ENV_KEY] % env[FAILOVER_ENDPOINTS_ENV_KEY].size]
56
+
57
+ if next_failover_endpoint.request_host
58
+ env[:request_headers] ||= {}
59
+ env[:request_headers]['Host'] = next_failover_endpoint.request_host
60
+ end
61
+
62
+ env[:url].hostname = next_failover_endpoint.ip_addr
63
+ end
64
+
65
+ def setup_resolution_env(env)
66
+ env[FAILOVER_ORIGINAL_HOST_ENV_KEY] = env[:url].hostname
67
+ env[FAILOVER_ORIGINAL_PORT_ENV_KEY] = env[:url].port
68
+ env[FAILOVER_COUNTER_ENV_KEY] = 0
69
+ env[FAILOVER_ENDPOINTS_ENV_KEY] = resolve_endpoints(env)
70
+ end
71
+
72
+ def resolve_endpoints(env)
73
+ host_list = if env[FAILOVER_ORIGINAL_HOST_ENV_KEY]
74
+ [[env[FAILOVER_ORIGINAL_HOST_ENV_KEY], env[FAILOVER_ORIGINAL_PORT_ENV_KEY]]] + @options.hosts(refresh: true)
75
+ else
76
+ @options.hosts
77
+ end
78
+
79
+ host_list.map { |host, port| Endpoint.from_host_and_port(host, port) }.flatten
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module HighlyAvailableRetries
5
+ # Options contains the configurable parameters for the middleware.
6
+ class Options < Faraday::Options.new(:hosts, :default_port)
7
+ DEFAULT_PORT = 80
8
+
9
+ def hosts(refresh: false)
10
+ if refresh
11
+ @hosts = nil
12
+ end
13
+
14
+ @hosts ||= load_host_list
15
+ end
16
+
17
+ def default_port
18
+ self[:default_port] ||= DEFAULT_PORT
19
+ end
20
+
21
+ private
22
+
23
+ def load_host_list
24
+ hosts = self[:hosts] ||= []
25
+ host_list = case hosts
26
+ when Proc
27
+ hosts.call
28
+ when Array
29
+ hosts
30
+ else
31
+ [hosts]
32
+ end
33
+ host_list.map { |host| host_to_hostname_and_port(host) }
34
+ end
35
+
36
+ def host_to_hostname_and_port(host)
37
+ hostname, port = host.split(':', 2)
38
+ [hostname, port ? port.to_i : default_port]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module HighlyAvailableRetries
5
+ VERSION = '0.1.0.pre.1'
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'highly_available_retries/middleware'
4
+ require_relative 'highly_available_retries/version'
5
+
6
+ module Faraday
7
+ # This will be your middleware main module, though the actual middleware implementation will go
8
+ # into Faraday::HighlyAvailableRetries::Middleware for the correct namespacing.
9
+ module HighlyAvailableRetries
10
+ # Faraday allows you to register your middleware for easier configuration.
11
+ # This step is totally optional, but it basically allows users to use a
12
+ # custom symbol (in this case, `:highly_available_retries`), to use your middleware in their connections.
13
+ # After calling this line, the following are both valid ways to set the middleware in a connection:
14
+ # * conn.use Faraday::HighlyAvailableRetries::Middleware
15
+ # * conn.use :highly_available_retries
16
+ # Without this line, only the former method is valid.
17
+ # Faraday::Middleware.register_middleware(highly_available_retries: Faraday::HighlyAvailableRetries::Middleware)
18
+
19
+ # Alternatively, you can register your middleware under Faraday::Request or Faraday::Response.
20
+ # This will allow to load your middleware using the `request` or `response` methods respectively.
21
+ #
22
+ # Load middleware with conn.request :failover
23
+ Faraday::Request.register_middleware(highly_available_retries: Faraday::HighlyAvailableRetries::Middleware)
24
+ #
25
+ # Load middleware with conn.response :highly_available_retries
26
+ # Faraday::Response.register_middleware(highly_available_retries: Faraday::HighlyAvailableRetries::Middleware)
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faraday-highly_available_retries
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - Invoca Development
8
+ - James Ebentier
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2023-08-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.0'
28
+ description: An extension for the Faraday::Retry middleware allowing retries to failover
29
+ across multiple resolved endpoints.
30
+ email:
31
+ - development@invoca.com
32
+ - jebentier@invoca.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - CHANGELOG.md
38
+ - LICENSE.md
39
+ - README.md
40
+ - lib/faraday/highly_available_retries.rb
41
+ - lib/faraday/highly_available_retries/endpoint.rb
42
+ - lib/faraday/highly_available_retries/ip_validator.rb
43
+ - lib/faraday/highly_available_retries/middleware.rb
44
+ - lib/faraday/highly_available_retries/options.rb
45
+ - lib/faraday/highly_available_retries/version.rb
46
+ homepage: https://github.com/invoca/faraday-highly_available_retries
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ allowed_push_host: https://rubygems.org
51
+ bug_tracker_uri: https://github.com/invoca/faraday-highly_available_retries/issues
52
+ changelog_uri: https://github.com/invoca/faraday-highly_available_retries/blob/v0.1.0.pre.1/CHANGELOG.md
53
+ documentation_uri: http://www.rubydoc.info/gems/faraday-highly_available_retries/0.1.0.pre.1
54
+ homepage_uri: https://github.com/invoca/faraday-highly_available_retries
55
+ source_code_uri: https://github.com/invoca/faraday-highly_available_retries
56
+ wiki_uri: https://github.com/invoca/faraday-highly_available_retries/wiki
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '2.7'
66
+ - - "<"
67
+ - !ruby/object:Gem::Version
68
+ version: '4'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">"
72
+ - !ruby/object:Gem::Version
73
+ version: 1.3.1
74
+ requirements: []
75
+ rubygems_version: 3.4.17
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: An extension for the Faraday::Retry middleware allowing retries to failover
79
+ across multiple resolved endpoints.
80
+ test_files: []