hearth 1.0.0.pre1 → 1.0.0.pre2

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.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -4
  3. data/VERSION +1 -1
  4. data/lib/hearth/api_error.rb +15 -1
  5. data/lib/hearth/auth_option.rb +21 -0
  6. data/lib/hearth/auth_schemes/anonymous.rb +21 -0
  7. data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
  8. data/lib/hearth/auth_schemes/http_basic.rb +16 -0
  9. data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
  10. data/lib/hearth/auth_schemes/http_digest.rb +16 -0
  11. data/lib/hearth/auth_schemes.rb +32 -0
  12. data/lib/hearth/checksums.rb +31 -0
  13. data/lib/hearth/client_stubs.rb +130 -0
  14. data/lib/hearth/config/env_provider.rb +53 -0
  15. data/lib/hearth/config/resolver.rb +52 -0
  16. data/lib/hearth/configuration.rb +15 -0
  17. data/lib/hearth/connection_pool.rb +77 -0
  18. data/lib/hearth/context.rb +28 -4
  19. data/lib/hearth/dns/host_address.rb +23 -0
  20. data/lib/hearth/dns/host_resolver.rb +92 -0
  21. data/lib/hearth/dns.rb +48 -0
  22. data/lib/hearth/http/api_error.rb +4 -8
  23. data/lib/hearth/http/client.rb +208 -59
  24. data/lib/hearth/http/error_inspector.rb +85 -0
  25. data/lib/hearth/http/error_parser.rb +18 -20
  26. data/lib/hearth/http/field.rb +64 -0
  27. data/lib/hearth/http/fields.rb +117 -0
  28. data/lib/hearth/http/middleware/content_length.rb +5 -2
  29. data/lib/hearth/http/middleware/content_md5.rb +31 -0
  30. data/lib/hearth/http/middleware/request_compression.rb +157 -0
  31. data/lib/hearth/http/middleware.rb +12 -0
  32. data/lib/hearth/http/networking_error.rb +1 -14
  33. data/lib/hearth/http/request.rb +83 -56
  34. data/lib/hearth/http/response.rb +42 -13
  35. data/lib/hearth/http.rb +14 -5
  36. data/lib/hearth/identities/anonymous.rb +8 -0
  37. data/lib/hearth/identities/http_api_key.rb +16 -0
  38. data/lib/hearth/identities/http_bearer.rb +16 -0
  39. data/lib/hearth/identities/http_login.rb +20 -0
  40. data/lib/hearth/identities.rb +21 -0
  41. data/lib/hearth/identity_resolver.rb +17 -0
  42. data/lib/hearth/interceptor.rb +506 -0
  43. data/lib/hearth/interceptor_context.rb +36 -0
  44. data/lib/hearth/interceptor_list.rb +48 -0
  45. data/lib/hearth/interceptors.rb +75 -0
  46. data/lib/hearth/middleware/auth.rb +100 -0
  47. data/lib/hearth/middleware/build.rb +32 -0
  48. data/lib/hearth/middleware/host_prefix.rb +10 -6
  49. data/lib/hearth/middleware/initialize.rb +58 -0
  50. data/lib/hearth/middleware/parse.rb +45 -6
  51. data/lib/hearth/middleware/retry.rb +97 -23
  52. data/lib/hearth/middleware/send.rb +137 -25
  53. data/lib/hearth/middleware/sign.rb +65 -0
  54. data/lib/hearth/middleware/validate.rb +11 -1
  55. data/lib/hearth/middleware.rb +19 -8
  56. data/lib/hearth/middleware_stack.rb +1 -43
  57. data/lib/hearth/networking_error.rb +18 -0
  58. data/lib/hearth/number_helper.rb +2 -2
  59. data/lib/hearth/output.rb +8 -4
  60. data/lib/hearth/plugin_list.rb +53 -0
  61. data/lib/hearth/query/param.rb +52 -0
  62. data/lib/hearth/query/param_list.rb +54 -0
  63. data/lib/hearth/query/param_matcher.rb +32 -0
  64. data/lib/hearth/refreshing_identity_resolver.rb +63 -0
  65. data/lib/hearth/request.rb +22 -0
  66. data/lib/hearth/response.rb +33 -0
  67. data/lib/hearth/retry/adaptive.rb +60 -0
  68. data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
  69. data/lib/hearth/retry/client_rate_limiter.rb +143 -0
  70. data/lib/hearth/retry/exponential_backoff.rb +15 -0
  71. data/lib/hearth/retry/retry_quota.rb +56 -0
  72. data/lib/hearth/retry/standard.rb +46 -0
  73. data/lib/hearth/retry/strategy.rb +20 -0
  74. data/lib/hearth/retry.rb +16 -0
  75. data/lib/hearth/signers/anonymous.rb +16 -0
  76. data/lib/hearth/signers/http_api_key.rb +29 -0
  77. data/lib/hearth/signers/http_basic.rb +23 -0
  78. data/lib/hearth/signers/http_bearer.rb +19 -0
  79. data/lib/hearth/signers/http_digest.rb +19 -0
  80. data/lib/hearth/signers.rb +23 -0
  81. data/lib/hearth/stubs.rb +30 -0
  82. data/lib/hearth/time_helper.rb +5 -3
  83. data/lib/hearth/validator.rb +44 -5
  84. data/lib/hearth/waiters/poller.rb +6 -7
  85. data/lib/hearth/waiters/waiter.rb +17 -4
  86. data/lib/hearth/xml/formatter.rb +11 -2
  87. data/lib/hearth/xml/node.rb +2 -2
  88. data/lib/hearth.rb +32 -5
  89. data/sig/lib/hearth/aliases.rbs +4 -0
  90. data/sig/lib/hearth/api_error.rbs +13 -0
  91. data/sig/lib/hearth/auth_option.rbs +11 -0
  92. data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
  93. data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
  94. data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
  95. data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
  96. data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
  97. data/sig/lib/hearth/auth_schemes.rbs +13 -0
  98. data/sig/lib/hearth/block_io.rbs +9 -0
  99. data/sig/lib/hearth/client_stubs.rbs +5 -0
  100. data/sig/lib/hearth/configuration.rbs +7 -0
  101. data/sig/lib/hearth/dns/host_address.rbs +13 -0
  102. data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
  103. data/sig/lib/hearth/http/api_error.rbs +13 -0
  104. data/sig/lib/hearth/http/client.rbs +9 -0
  105. data/sig/lib/hearth/http/field.rbs +19 -0
  106. data/sig/lib/hearth/http/fields.rbs +43 -0
  107. data/sig/lib/hearth/http/request.rbs +25 -0
  108. data/sig/lib/hearth/http/response.rbs +21 -0
  109. data/sig/lib/hearth/identities/anonymous.rbs +6 -0
  110. data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
  111. data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
  112. data/sig/lib/hearth/identities/http_login.rbs +11 -0
  113. data/sig/lib/hearth/identities.rbs +9 -0
  114. data/sig/lib/hearth/identity_resolver.rbs +7 -0
  115. data/sig/lib/hearth/interceptor.rbs +9 -0
  116. data/sig/lib/hearth/interceptor_context.rbs +15 -0
  117. data/sig/lib/hearth/interceptor_list.rbs +16 -0
  118. data/sig/lib/hearth/interfaces.rbs +65 -0
  119. data/sig/lib/hearth/output.rbs +11 -0
  120. data/sig/lib/hearth/plugin_list.rbs +15 -0
  121. data/sig/lib/hearth/query/param.rbs +17 -0
  122. data/sig/lib/hearth/query/param_list.rbs +25 -0
  123. data/sig/lib/hearth/request.rbs +9 -0
  124. data/sig/lib/hearth/response.rbs +11 -0
  125. data/sig/lib/hearth/retry/adaptive.rbs +13 -0
  126. data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
  127. data/sig/lib/hearth/retry/standard.rbs +13 -0
  128. data/sig/lib/hearth/retry/strategy.rbs +11 -0
  129. data/sig/lib/hearth/retry.rbs +9 -0
  130. data/sig/lib/hearth/signers/anonymous.rbs +9 -0
  131. data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
  132. data/sig/lib/hearth/signers/http_basic.rbs +9 -0
  133. data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
  134. data/sig/lib/hearth/signers/http_digest.rbs +9 -0
  135. data/sig/lib/hearth/signers.rbs +9 -0
  136. data/sig/lib/hearth/structure.rbs +7 -0
  137. data/sig/lib/hearth/union.rbs +5 -0
  138. data/sig/lib/hearth/waiters/waiter.rbs +17 -0
  139. metadata +132 -22
  140. data/lib/hearth/http/headers.rb +0 -70
  141. data/lib/hearth/middleware/around_handler.rb +0 -24
  142. data/lib/hearth/middleware/request_handler.rb +0 -24
  143. data/lib/hearth/middleware/response_handler.rb +0 -25
  144. data/lib/hearth/middleware_builder.rb +0 -246
  145. data/lib/hearth/stubbing/client_stubs.rb +0 -115
  146. data/lib/hearth/stubbing/stubs.rb +0 -32
  147. data/lib/hearth/waiters/errors.rb +0 -15
  148. data/sig/lib/seahorse/api_error.rbs +0 -10
  149. data/sig/lib/seahorse/document.rbs +0 -2
  150. data/sig/lib/seahorse/http/api_error.rbs +0 -21
  151. data/sig/lib/seahorse/http/headers.rbs +0 -47
  152. data/sig/lib/seahorse/http/response.rbs +0 -21
  153. data/sig/lib/seahorse/simple_delegator.rbs +0 -3
  154. data/sig/lib/seahorse/structure.rbs +0 -18
  155. data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
  156. data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
  157. data/sig/lib/seahorse/union.rbs +0 -6
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Middleware
5
+ # A middleware that signs requests using the resolved identity.
6
+ # @api private
7
+ class Sign
8
+ include Middleware::Logging
9
+
10
+ # @param [Class] app The next middleware in the stack.
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ # @param input
16
+ # @param context
17
+ # @return [Output]
18
+ def call(input, context)
19
+ interceptor_error = Interceptors.invoke(
20
+ hook: Interceptor::MODIFY_BEFORE_SIGNING,
21
+ input: input,
22
+ context: context,
23
+ output: nil,
24
+ aggregate_errors: false
25
+ )
26
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
27
+
28
+ interceptor_error = Interceptors.invoke(
29
+ hook: Interceptor::READ_BEFORE_SIGNING,
30
+ input: input,
31
+ context: context,
32
+ output: nil,
33
+ aggregate_errors: false
34
+ )
35
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
36
+
37
+ sign_request(context)
38
+ output = @app.call(input, context)
39
+
40
+ interceptor_error = Interceptors.invoke(
41
+ hook: Interceptor::READ_AFTER_SIGNING,
42
+ input: input,
43
+ context: context,
44
+ output: output,
45
+ aggregate_errors: false
46
+ )
47
+ output.error = interceptor_error if interceptor_error
48
+
49
+ output
50
+ end
51
+
52
+ private
53
+
54
+ def sign_request(context)
55
+ log_debug(context, "Signing request with: #{context.auth.signer}")
56
+ context.auth.signer.sign(
57
+ request: context.request,
58
+ identity: context.auth.identity,
59
+ properties: context.auth.signer_properties
60
+ )
61
+ log_debug(context, "Signed request: #{context.request.inspect}")
62
+ end
63
+ end
64
+ end
65
+ end
@@ -5,6 +5,8 @@ module Hearth
5
5
  # A middleware used to validate input.
6
6
  # @api private
7
7
  class Validate
8
+ include Middleware::Logging
9
+
8
10
  # @param [Class] app The next middleware in the stack.
9
11
  # @param [Boolean] validate_input If true, the input is validated against
10
12
  # the model and an error is raised for unexpected types.
@@ -21,9 +23,17 @@ module Hearth
21
23
  # @param context
22
24
  # @return [Output]
23
25
  def call(input, context)
24
- @validator.validate!(input, context: 'input') if @validate_input
26
+ validate_input(input, context) if @validate_input
25
27
  @app.call(input, context)
26
28
  end
29
+
30
+ private
31
+
32
+ def validate_input(input, context)
33
+ log_debug(context, "Validating input with: #{input}")
34
+ @validator.validate!(input, context: 'input')
35
+ log_debug(context, 'Validated input')
36
+ end
27
37
  end
28
38
  end
29
39
  end
@@ -1,16 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'middleware/around_handler'
3
+ module Hearth
4
+ # @api private
5
+ module Middleware
6
+ # Helper methods for logging to middleware.
7
+ module Logging
8
+ # Logs a debug message to the logger on context. The message is prefixed
9
+ # with the invocation id and the class name.
10
+ def log_debug(context, message)
11
+ context.logger.debug(
12
+ "[#{context.invocation_id}] [#{self.class}] #{message}"
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require_relative 'middleware/auth'
4
20
  require_relative 'middleware/build'
5
21
  require_relative 'middleware/host_prefix'
6
22
  require_relative 'middleware/parse'
7
- require_relative 'middleware/request_handler'
8
- require_relative 'middleware/response_handler'
9
23
  require_relative 'middleware/retry'
10
24
  require_relative 'middleware/send'
25
+ require_relative 'middleware/sign'
11
26
  require_relative 'middleware/validate'
12
-
13
- module Hearth
14
- # @api private
15
- module Middleware; end
16
- end
27
+ require_relative 'middleware/initialize'
@@ -11,52 +11,10 @@ module Hearth
11
11
  @middleware.push([middleware, middleware_kwargs])
12
12
  end
13
13
 
14
- def use_before(before, middleware, **middleware_kwargs)
15
- new_middleware = []
16
- @middleware.each do |klass, args|
17
- new_middleware << [middleware, middleware_kwargs] if before == klass
18
- new_middleware << [klass, args]
19
- end
20
- unless new_middleware.size == @middleware.size + 1
21
- raise ArgumentError,
22
- "Failed to insert #{middleware} before #{before}"
23
- end
24
-
25
- @middleware = new_middleware
26
- end
27
-
28
- def use_after(after, middleware, **middleware_kwargs)
29
- new_middleware = []
30
- @middleware.each do |klass, args|
31
- new_middleware << [klass, args]
32
- new_middleware << [middleware, middleware_kwargs] if after == klass
33
- end
34
- unless new_middleware.size == @middleware.size + 1
35
- raise ArgumentError,
36
- "Failed to insert #{middleware} after #{after}"
37
- end
38
-
39
- @middleware = new_middleware
40
- end
41
-
42
- def remove(remove)
43
- new_middleware = []
44
- @middleware.each do |klass, args|
45
- new_middleware << [klass, args] unless klass == remove
46
- end
47
-
48
- unless new_middleware.size == @middleware.size - 1
49
- raise ArgumentError,
50
- "Failed to remove #{remove}"
51
- end
52
-
53
- @middleware = new_middleware
54
- end
55
-
56
14
  # @param input
57
15
  # @param context
58
16
  # @return [Output]
59
- def run(input:, context:)
17
+ def run(input, context)
60
18
  stack.call(input, context)
61
19
  end
62
20
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Thrown by a Client when encountering a networking error while transmitting
5
+ # a request or receiving a response. You can access the original error
6
+ # by calling {#original_error}.
7
+ class NetworkingError < StandardError
8
+ MSG = 'Encountered an error while transmitting the request: %<message>s'
9
+
10
+ def initialize(original_error)
11
+ @original_error = original_error
12
+ super(format(MSG, message: original_error.message))
13
+ end
14
+
15
+ # @return [StandardError]
16
+ attr_reader :original_error
17
+ end
18
+ end
@@ -6,8 +6,8 @@ module Hearth
6
6
  # @api private
7
7
  module NumberHelper
8
8
  class << self
9
- # @param [Number,String] input
10
- # @return [String] The serialized number
9
+ # @param [Number] input
10
+ # @return [Number, String] The serialized number
11
11
  def serialize(input)
12
12
  if input == ::Float::INFINITY then 'Infinity'
13
13
  elsif input == -::Float::INFINITY then '-Infinity'
data/lib/hearth/output.rb CHANGED
@@ -2,19 +2,23 @@
2
2
 
3
3
  module Hearth
4
4
  # A wrapper class that contains an error or data from the response.
5
- # @api private
6
5
  class Output
7
- # @param [StandardError] error The error class to be raised.
6
+ # @param [ApiError] error The error class to be raised.
8
7
  # @param [Struct] data The data returned by a client.
9
- def initialize(error: nil, data: nil)
8
+ # @param [Hash] metadata Response metadata set by client middleware.
9
+ def initialize(error: nil, data: nil, metadata: {})
10
10
  @error = error
11
11
  @data = data
12
+ @metadata = metadata
12
13
  end
13
14
 
14
- # @return [StandardError, nil]
15
+ # @return [ApiError, nil]
15
16
  attr_accessor :error
16
17
 
17
18
  # @return [Struct, nil]
18
19
  attr_accessor :data
20
+
21
+ # @return [Hash]
22
+ attr_accessor :metadata
19
23
  end
20
24
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # A list of plugins that can be applied to config. Plugins are callables
5
+ # that take one argument (config) and are called during client initialization
6
+ # or operation invocation to modify config.
7
+ class PluginList
8
+ include Enumerable
9
+
10
+ # Initialize a PluginList.
11
+ #
12
+ # @param [Array] plugins ([]) A list of plugins to initialize with
13
+ def initialize(plugins = [])
14
+ unless plugins.respond_to?(:each)
15
+ raise ArgumentError, 'Plugins must be an enumerable'
16
+ end
17
+
18
+ @plugins = []
19
+ plugins.each { |p| add(p) }
20
+ end
21
+
22
+ # Add a plugin
23
+ #
24
+ # @param [Callable] plugin
25
+ def add(plugin)
26
+ unless valid_plugin?(plugin)
27
+ raise ArgumentError,
28
+ 'Plugin must be callable and take one argument (config)'
29
+ end
30
+
31
+ @plugins << plugin
32
+ end
33
+
34
+ alias << add
35
+
36
+ def each(&block)
37
+ @plugins.each(&block)
38
+ end
39
+
40
+ private
41
+
42
+ # plugins must be callable and take exactly 1 argument (config)
43
+ def valid_plugin?(plugin)
44
+ case plugin
45
+ when Proc
46
+ # the arity of a proc.method(:call) is -1, need to special case
47
+ plugin.arity == 1
48
+ else
49
+ plugin.respond_to?(:call) && plugin.method(:call).arity == 1
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Query
5
+ # A class used to represent a query parameter before serialization.
6
+ class Param
7
+ # @param [String] name
8
+ # @param [String, Array<String>] value (nil)
9
+ def initialize(name, value = nil)
10
+ @name = name
11
+ @value = value
12
+ end
13
+
14
+ # @return [String]
15
+ attr_reader :name
16
+
17
+ # @return [String, Array<String>, nil]
18
+ attr_reader :value
19
+
20
+ # @return [String]
21
+ def to_s
22
+ if value.is_a?(Array)
23
+ value.map { |v| serialize(name, v) }.join('&')
24
+ else
25
+ serialize(name, value)
26
+ end
27
+ end
28
+
29
+ # @return [Boolean]
30
+ def ==(other)
31
+ other.is_a?(Param) &&
32
+ other.name == name &&
33
+ other.value == value
34
+ end
35
+
36
+ # @return [Boolean]
37
+ def <=>(other)
38
+ name <=> other.name
39
+ end
40
+
41
+ private
42
+
43
+ def serialize(name, value)
44
+ value.nil? ? escape(name) : "#{escape(name)}=#{escape(value)}"
45
+ end
46
+
47
+ def escape(value)
48
+ Hearth::HTTP.uri_escape(value.to_s)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Query
5
+ # A class used to collect query params before serialization.
6
+ class ParamList
7
+ include Enumerable
8
+
9
+ def initialize
10
+ @params = {}
11
+ end
12
+
13
+ # @param [String] param_name
14
+ # @param [String, nil] param_value
15
+ # @return [Param]
16
+ def set(param_name, param_value = nil)
17
+ param = Param.new(param_name, param_value)
18
+ @params[param.name] = param
19
+ end
20
+ alias []= set
21
+
22
+ # @return [Param, nil]
23
+ def [](param_name)
24
+ @params[param_name]
25
+ end
26
+
27
+ # @param [String] param_name
28
+ # @return [Param, nil]
29
+ def delete(param_name)
30
+ @params.delete(param_name)
31
+ end
32
+
33
+ # @return [Enumerable]
34
+ def each(&block)
35
+ to_a.each(&block)
36
+ end
37
+
38
+ # @return [Boolean]
39
+ def empty?
40
+ @params.empty?
41
+ end
42
+
43
+ # @return [Array<Param>] Returns an array of sorted {Param} objects.
44
+ def to_a
45
+ @params.values.sort
46
+ end
47
+
48
+ # @return [String]
49
+ def to_s
50
+ to_a.map(&:to_s).join('&')
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ # Provides an rspec matcher for CGI.parse to check Float precision.
6
+ # @api private
7
+ RSpec::Matchers.define :match_query_params do |expected|
8
+ match do |actual|
9
+ return true if actual == expected
10
+ return false unless actual.instance_of?(expected.class)
11
+
12
+ expect(actual.keys.length).to eq(expected.keys.length)
13
+ expect(actual.values.length).to eq(expected.values.length)
14
+
15
+ expected.each do |ek, e|
16
+ expect(actual.keys).to include(ek)
17
+ a = actual[ek]
18
+ expect(e.length).to eq(a.length)
19
+
20
+ a.zip(e).each do |a0, e0|
21
+ # Timestamps can have optional precision.
22
+ if (float = Float(a0) rescue false)
23
+ expect(float).to eq(e0.to_f)
24
+ else
25
+ expect(a0).to eq(e0)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ diffable
32
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # A module that can be included in a class to provide a #identity method.
5
+ # The class must implement #refresh(properties) that sets @identity. The
6
+ # refresh method will be called when #identity is called and the identity
7
+ # is nil or near expiration.
8
+ module RefreshingIdentityResolver
9
+ SYNC_EXPIRATION_LENGTH = 300 # 5 minutes
10
+ ASYNC_EXPIRATION_LENGTH = 600 # 10 minutes
11
+
12
+ def initialize
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ # @return [Identity]
17
+ def identity(properties = {})
18
+ if @identity
19
+ refresh_if_near_expiration!(properties)
20
+ else # initialization
21
+ @mutex.synchronize { refresh(properties) }
22
+ end
23
+ @identity
24
+ end
25
+
26
+ private
27
+
28
+ def sync_expiration_length
29
+ self.class::SYNC_EXPIRATION_LENGTH
30
+ end
31
+
32
+ def async_expiration_length
33
+ self.class::ASYNC_EXPIRATION_LENGTH
34
+ end
35
+
36
+ # Refreshes identity asynchronously and synchronously.
37
+ # If we are near to expiration, block while refreshing the identity.
38
+ # Otherwise, if we're approaching expiration, use the existing identity
39
+ # but attempt a refresh in the background.
40
+ def refresh_if_near_expiration!(properties)
41
+ # NOTE: This check is an optimization. Rather than acquire the mutex on
42
+ # every #refresh_if_near_expiration call, we check before doing so, and
43
+ # then we check within the mutex to avoid a race condition.
44
+ if near_expiration?(sync_expiration_length)
45
+ @mutex.synchronize do
46
+ refresh(properties) if near_expiration?(sync_expiration_length)
47
+ end
48
+ elsif @async_refresh && near_expiration?(async_expiration_length)
49
+ unless @mutex.locked?
50
+ Thread.new do
51
+ @mutex.synchronize do
52
+ refresh(properties) if near_expiration?(async_expiration_length)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def near_expiration?(expiration_length)
60
+ (Time.now.to_i + expiration_length) > @identity.expiration.to_i
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'uri'
5
+
6
+ module Hearth
7
+ # Represents a base request.
8
+ class Request
9
+ # @param [URI] uri (URI(''))
10
+ # @param [IO] body (StringIO.new)
11
+ def initialize(uri: URI(''), body: StringIO.new)
12
+ @uri = uri
13
+ @body = body
14
+ end
15
+
16
+ # @return [URI]
17
+ attr_accessor :uri
18
+
19
+ # @return [IO]
20
+ attr_accessor :body
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Hearth
6
+ # Represents a base response.
7
+ class Response
8
+ # @param [IO] body (StringIO.new)
9
+ def initialize(body: StringIO.new)
10
+ @body = body
11
+ end
12
+
13
+ # @return [IO]
14
+ attr_accessor :body
15
+
16
+ # Replace attributes from other response
17
+ # @param [Response] other
18
+ # @return [Response]
19
+ def replace(other)
20
+ @body.truncate(0)
21
+ IO.copy_stream(other.body, @body)
22
+ @body.rewind
23
+ self
24
+ end
25
+
26
+ # Resets the response.
27
+ # @return [Response]
28
+ def reset
29
+ @body.truncate(0) if @body.respond_to?(:truncate)
30
+ self
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # Adaptive retry strategy for retrying requests.
6
+ class Adaptive < Strategy
7
+ # @param [#call] backoff (ExponentialBackoff) A callable object that
8
+ # calculates a backoff delay for a retry attempt.
9
+ # @param [Integer] max_attempts (3) The maximum number of attempts that
10
+ # will be made for a single request, including the initial attempt.
11
+ # @param [Boolean] wait_to_fill When true, the request will sleep until
12
+ # there is sufficient client side capacity to retry the request. When
13
+ # false, the request will raise a `CapacityNotAvailableError` and will
14
+ # not retry instead of sleeping.
15
+ def initialize(backoff: ExponentialBackoff.new, max_attempts: 3,
16
+ wait_to_fill: true)
17
+ super()
18
+ @backoff = backoff
19
+ @max_attempts = max_attempts
20
+ @wait_to_fill = wait_to_fill
21
+
22
+ # instance state
23
+ @client_rate_limiter = ClientRateLimiter.new
24
+ @retry_quota = RetryQuota.new
25
+ @capacity_amount = nil
26
+ end
27
+
28
+ def acquire_initial_retry_token(_token_scope = nil)
29
+ @client_rate_limiter.token_bucket_acquire(
30
+ 1, wait_to_fill: @wait_to_fill
31
+ )
32
+ Token.new(retry_count: 0)
33
+ end
34
+
35
+ def refresh_retry_token(retry_token, error_info)
36
+ return unless error_info.retryable?
37
+
38
+ @client_rate_limiter.update_sending_rate(
39
+ error_info.error_type == 'Throttling'
40
+ )
41
+ return if retry_token.retry_count >= @max_attempts - 1
42
+
43
+ @capacity_amount = @retry_quota.checkout_capacity(error_info)
44
+ return unless @capacity_amount.positive?
45
+
46
+ delay = error_info.hints[:retry_after]
47
+ delay ||= @backoff.call(retry_token.retry_count)
48
+ retry_token.retry_count += 1
49
+ retry_token.retry_delay = delay
50
+ retry_token
51
+ end
52
+
53
+ def record_success(retry_token)
54
+ @client_rate_limiter.update_sending_rate(false)
55
+ @retry_quota.release(@capacity_amount)
56
+ retry_token
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # Raised by Retry::Adaptive when the client rate limiter
6
+ # doesn't have capacity.
7
+ class CapacityNotAvailableError < RuntimeError; end
8
+ end
9
+ end