faraday-highly_available_retries 0.1.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []