opinionated_http 0.0.1 → 0.0.2
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 +4 -4
- data/README.md +53 -0
- data/lib/opinionated_http.rb +3 -62
- data/lib/opinionated_http/client.rb +50 -31
- data/lib/opinionated_http/version.rb +1 -1
- data/test/client_test.rb +6 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e262dc93300a43d2a2f60cf02badf23e9cc38edf83c7359c335ad36e0a1bd7f9
|
4
|
+
data.tar.gz: 3a81646b073a3dc32ee7eef045697c59ede3c379c130159e9aca9efdacd47233
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c33ddd878ed1cf11a7a7f9ba04dc56a1fc0ee96e6e006e339ec6cc8d94ac38a0fe6a1f3a362aaee7484e72c74ceaeaea0acdcf7170dd0da6c22803d2685918f
|
7
|
+
data.tar.gz: 5b48a34488db8e1a107a617e0f56e40b8a218f2064dfa84c60fb66a720d12816ec2fb182101137dafd537f5f40d9764583bdb784ad008f288b4bc82f0dc17966
|
data/README.md
CHANGED
@@ -17,3 +17,56 @@ PersistentHTTP with the following enhancements:
|
|
17
17
|
* Standardized Service Exception.
|
18
18
|
* Retries on HTTP 5XX errors
|
19
19
|
|
20
|
+
# Example
|
21
|
+
|
22
|
+
# Configuration
|
23
|
+
|
24
|
+
# Usage
|
25
|
+
|
26
|
+
Create a new Opinionated HTTP instance.
|
27
|
+
|
28
|
+
Parameters:
|
29
|
+
secret_config_prefix:
|
30
|
+
Required
|
31
|
+
metric_prefix:
|
32
|
+
Required
|
33
|
+
error_class:
|
34
|
+
Whenever exceptions are raised it is important that every client gets its own exception / error class
|
35
|
+
so that failures to specific http servers can be easily identified.
|
36
|
+
Required.
|
37
|
+
logger:
|
38
|
+
Default: SemanticLogger[OpinionatedHTTP]
|
39
|
+
Other options as supported by PersistentHTTP
|
40
|
+
#TODO: Expand PersistentHTTP options here
|
41
|
+
|
42
|
+
Configuration:
|
43
|
+
Off of the `secret_config_path` path above, Opinionated HTTP uses specific configuration entry names
|
44
|
+
to configure the underlying HTTP setup:
|
45
|
+
url: [String]
|
46
|
+
The host url to the site to connect to.
|
47
|
+
Exclude any path, since that will be supplied when `#get` or `#post` is called.
|
48
|
+
Required.
|
49
|
+
Examples:
|
50
|
+
"https://example.com"
|
51
|
+
"https://example.com:8443/"
|
52
|
+
pool_size: [Integer]
|
53
|
+
default: 100
|
54
|
+
open_timeout: [Float]
|
55
|
+
default: 10
|
56
|
+
read_timeout: [Float]
|
57
|
+
default: 10
|
58
|
+
idle_timeout: [Float]
|
59
|
+
default: 300
|
60
|
+
keep_alive: [Float]
|
61
|
+
default: 300
|
62
|
+
pool_timeout: [Float]
|
63
|
+
default: 5
|
64
|
+
warn_timeout: [Float]
|
65
|
+
default: 0.25
|
66
|
+
proxy: [Symbol]
|
67
|
+
default: :ENV
|
68
|
+
force_retry: [true|false]
|
69
|
+
default: true
|
70
|
+
|
71
|
+
Metrics:
|
72
|
+
During each call to `#get` or `#put`, the following metrics are logged using the
|
data/lib/opinionated_http.rb
CHANGED
@@ -1,76 +1,17 @@
|
|
1
|
+
require 'opinionated_http/version'
|
1
2
|
#
|
2
3
|
# Opinionated HTTP
|
3
4
|
#
|
4
5
|
# An opinionated HTTP Client library using convention over configuration.
|
5
6
|
#
|
6
|
-
# Uses
|
7
|
-
# * PersistentHTTP for http connection pooling.
|
8
|
-
# * Semantic Logger for logging and metrics.
|
9
|
-
# * Secret Config for its configuration.
|
10
|
-
#
|
11
|
-
# By convention the following metrics are measured and logged:
|
12
|
-
# *
|
13
|
-
#
|
14
|
-
# PersistentHTTP with the following enhancements:
|
15
|
-
# * Read config from Secret Config, just supply the `secret_config_path`.
|
16
|
-
# * Redirect logging into standard Semantic Logger.
|
17
|
-
# * Implements metrics and measure call durations.
|
18
|
-
# * Standardized Service Exception.
|
19
|
-
# * Retries on HTTP 5XX errors
|
20
|
-
#
|
21
|
-
|
22
|
-
require 'opinionated_http/version'
|
23
7
|
module OpinionatedHTTP
|
24
8
|
autoload :Client, 'opinionated_http/client'
|
25
9
|
autoload :Logger, 'opinionated_http/logger'
|
26
10
|
|
27
|
-
# Create a new Opinionated HTTP instance.
|
28
11
|
#
|
29
|
-
#
|
30
|
-
# secret_config_prefix:
|
31
|
-
# Required
|
32
|
-
# metric_prefix:
|
33
|
-
# Required
|
34
|
-
# error_class:
|
35
|
-
# Whenever exceptions are raised it is important that every client gets its own exception / error class
|
36
|
-
# so that failures to specific http servers can be easily identified.
|
37
|
-
# Required.
|
38
|
-
# logger:
|
39
|
-
# Default: SemanticLogger[OpinionatedHTTP]
|
40
|
-
# Other options as supported by PersistentHTTP
|
41
|
-
# #TODO: Expand PersistentHTTP options here
|
42
|
-
#
|
43
|
-
# Configuration:
|
44
|
-
# Off of the `secret_config_path` path above, Opinionated HTTP uses specific configuration entry names
|
45
|
-
# to configure the underlying HTTP setup:
|
46
|
-
# url: [String]
|
47
|
-
# The host url to the site to connect to.
|
48
|
-
# Exclude any path, since that will be supplied when `#get` or `#post` is called.
|
49
|
-
# Required.
|
50
|
-
# Examples:
|
51
|
-
# "https://example.com"
|
52
|
-
# "https://example.com:8443/"
|
53
|
-
# pool_size: [Integer]
|
54
|
-
# default: 100
|
55
|
-
# open_timeout: [Float]
|
56
|
-
# default: 10
|
57
|
-
# read_timeout: [Float]
|
58
|
-
# default: 10
|
59
|
-
# idle_timeout: [Float]
|
60
|
-
# default: 300
|
61
|
-
# keep_alive: [Float]
|
62
|
-
# default: 300
|
63
|
-
# pool_timeout: [Float]
|
64
|
-
# default: 5
|
65
|
-
# warn_timeout: [Float]
|
66
|
-
# default: 0.25
|
67
|
-
# proxy: [Symbol]
|
68
|
-
# default: :ENV
|
69
|
-
# force_retry: [true|false]
|
70
|
-
# default: true
|
12
|
+
# Create a new Opinionated HTTP instance.
|
71
13
|
#
|
72
|
-
#
|
73
|
-
# During each call to `#get` or `#put`, the following metrics are logged using the
|
14
|
+
# See README.md for more info.
|
74
15
|
def self.new(**args)
|
75
16
|
Client.new(**args)
|
76
17
|
end
|
@@ -4,14 +4,24 @@ require 'semantic_logger'
|
|
4
4
|
#
|
5
5
|
# Client http implementation
|
6
6
|
#
|
7
|
+
# See README.md for more info.
|
7
8
|
module OpinionatedHTTP
|
8
9
|
class Client
|
9
|
-
|
10
|
+
# 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
|
11
|
+
HTTP_RETRY_CODES = %w[502 503 504]
|
12
|
+
|
13
|
+
attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver,
|
14
|
+
:retry_count, :retry_interval, :retry_multiplier, :http_retry_codes
|
10
15
|
|
11
16
|
def initialize(secret_config_prefix:, logger: nil, metric_prefix:, error_class:, **options)
|
12
|
-
@metric_prefix
|
13
|
-
@logger
|
14
|
-
@error_class
|
17
|
+
@metric_prefix = metric_prefix
|
18
|
+
@logger = logger || SemanticLogger[self]
|
19
|
+
@error_class = error_class
|
20
|
+
@retry_count = SecretConfig.fetch("#{secret_config_prefix}/retry_count", type: :integer, default: 11)
|
21
|
+
@retry_interval = SecretConfig.fetch("#{secret_config_prefix}/retry_interval", type: :float, default: 0.01)
|
22
|
+
@retry_multiplier = SecretConfig.fetch("#{secret_config_prefix}/retry_multiplier", type: :float, default: 1.8)
|
23
|
+
http_retry_codes = SecretConfig.fetch("#{secret_config_prefix}/http_retry_codes", type: :string, default: HTTP_RETRY_CODES.join(","))
|
24
|
+
@http_retry_codes = http_retry_codes.split(",").collect { |str| str.strip }
|
15
25
|
|
16
26
|
internal_logger = OpinionatedHTTP::Logger.new(@logger)
|
17
27
|
new_options = {
|
@@ -40,27 +50,7 @@ module OpinionatedHTTP
|
|
40
50
|
path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters
|
41
51
|
|
42
52
|
request = Net::HTTP::Get.new(path)
|
43
|
-
response =
|
44
|
-
begin
|
45
|
-
payload = {}
|
46
|
-
if logger.trace?
|
47
|
-
payload[:parameters] = parameters
|
48
|
-
payload[:path] = path
|
49
|
-
end
|
50
|
-
message = "HTTP GET: #{action}" if logger.debug?
|
51
|
-
|
52
|
-
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
53
|
-
rescue StandardError => exc
|
54
|
-
message = "HTTP GET: #{action} Failure: #{exc.class.name}: #{exc.message}"
|
55
|
-
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
56
|
-
raise(error_class, message)
|
57
|
-
end
|
58
|
-
|
59
|
-
unless response.is_a?(Net::HTTPSuccess)
|
60
|
-
message = "HTTP GET: #{action} Failure: (#{response.code}) #{response.message}"
|
61
|
-
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
62
|
-
raise(error_class, message)
|
63
|
-
end
|
53
|
+
response = request_with_retry(action: action, path: path, request: request)
|
64
54
|
|
65
55
|
response.body
|
66
56
|
end
|
@@ -70,29 +60,58 @@ module OpinionatedHTTP
|
|
70
60
|
request = Net::HTTP::Post.new(path)
|
71
61
|
request.set_form_data(*parameters) if parameters
|
72
62
|
|
73
|
-
response =
|
63
|
+
response = request_with_retry(action: action, path: path, request: request)
|
64
|
+
|
65
|
+
response.body
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def request_with_retry(action:, path: "/#{action}", request:, try_count: 0)
|
71
|
+
http_method = request.method.upcase
|
72
|
+
response =
|
74
73
|
begin
|
75
74
|
payload = {}
|
76
75
|
if logger.trace?
|
77
76
|
payload[:parameters] = parameters
|
78
77
|
payload[:path] = path
|
79
78
|
end
|
80
|
-
message = "HTTP
|
79
|
+
message = "HTTP #{http_method}: #{action}" if logger.debug?
|
81
80
|
|
82
81
|
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
83
82
|
rescue StandardError => exc
|
84
|
-
message = "HTTP
|
83
|
+
message = "HTTP #{http_method}: #{action} Failure: #{exc.class.name}: #{exc.message}"
|
85
84
|
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
86
85
|
raise(error_class, message)
|
87
86
|
end
|
88
87
|
|
89
|
-
|
90
|
-
|
88
|
+
# Retry on http 5xx errors except 500 which means internal server error.
|
89
|
+
if http_retry_codes.include?(response.code)
|
90
|
+
if try_count < retry_count
|
91
|
+
try_count = try_count + 1
|
92
|
+
duration = retry_sleep_interval(try_count)
|
93
|
+
logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
|
94
|
+
sleep(duration)
|
95
|
+
response = request_with_retry(action: action, path: path, request: request, try_count: try_count)
|
96
|
+
else
|
97
|
+
message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
|
98
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
99
|
+
raise(error_class, message)
|
100
|
+
end
|
101
|
+
elsif !response.is_a?(Net::HTTPSuccess)
|
102
|
+
message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}"
|
91
103
|
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
92
104
|
raise(error_class, message)
|
93
105
|
end
|
94
106
|
|
95
|
-
response
|
107
|
+
response
|
108
|
+
end
|
109
|
+
|
110
|
+
# First retry is immediate, next retry is after `retry_interval`,
|
111
|
+
# each subsequent retry interval is 100% longer than the prior interval.
|
112
|
+
def retry_sleep_interval(retry_count)
|
113
|
+
return 0 if retry_count <= 1
|
114
|
+
(retry_multiplier ** (retry_count - 1)) * retry_interval
|
96
115
|
end
|
97
116
|
end
|
98
117
|
end
|
data/test/client_test.rb
CHANGED
@@ -20,12 +20,12 @@ module OpinionatedHTTP
|
|
20
20
|
|
21
21
|
describe "get" do
|
22
22
|
it 'success' do
|
23
|
-
output = {zip: '12345', population: 54321}
|
24
|
-
body = output.to_json
|
25
|
-
response = Net::HTTPSuccess.new(200, 'OK', body)
|
26
|
-
http.driver.stub(:request, response) do
|
27
|
-
|
28
|
-
end
|
23
|
+
# output = {zip: '12345', population: 54321}
|
24
|
+
# body = output.to_json
|
25
|
+
# response = Net::HTTPSuccess.new(200, 'OK', body)
|
26
|
+
# http.driver.stub(:request, response) do
|
27
|
+
# http.get(action: 'lookup', parameters: {zip: '12345'})
|
28
|
+
# end
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opinionated_http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Reid Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-02-
|
11
|
+
date: 2020-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: persistent_http
|