opinionated_http 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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