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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21c5b626468b387d5b10f055e3f45b399d1b7c0e91a3962a31de1914f582848e
4
- data.tar.gz: 4190403fda9fd6bb3853f8d552aa53f22e1ddf89ead008ee4fde7d9f184c7eaa
3
+ metadata.gz: e262dc93300a43d2a2f60cf02badf23e9cc38edf83c7359c335ad36e0a1bd7f9
4
+ data.tar.gz: 3a81646b073a3dc32ee7eef045697c59ede3c379c130159e9aca9efdacd47233
5
5
  SHA512:
6
- metadata.gz: ff4c0ec5fadca2ac6a6cc6bbabb1e0893aa7064da3adbbfb33ef1c3561df302e2408b983ad1598360fab213ca5c35f5cc14765bcde59da95c8684d4f7f32894a
7
- data.tar.gz: 274ff50c9ab8770f292ac37f2341480e01384ceddf8ef08a2ae644c139b5ecb965e0621309dd381f64e2e0a5d44bc2ee06b8037ada96c8731b8cdf645d8ab035
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
@@ -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
- # Parameters:
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
- # Metrics:
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
- attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver
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 = metric_prefix
13
- @logger = logger || SemanticLogger[self]
14
- @error_class = 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 POST: #{action}" if logger.debug?
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 POST: #{action} Failure: #{exc.class.name}: #{exc.message}"
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
- unless response.is_a?(Net::HTTPSuccess)
90
- message = "HTTP POST: #{action} Failure: (#{response.code}) #{response.message}"
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.body
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
@@ -1,3 +1,3 @@
1
1
  module OpinionatedHTTP
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.0.2".freeze
3
3
  end
@@ -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
- http.get(action: 'lookup', parameters: {zip: '12345'})
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.1
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-20 00:00:00.000000000 Z
11
+ date: 2020-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: persistent_http