stripe 5.1.1 → 5.5.0

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: 3317e27f361af5e5506ab608ccdc9c18b3746c76e6244a5db15b149135b7deee
4
- data.tar.gz: 65f8bd1faf0c64125bfd0badeb835e555cf3a23437bf8d62c3101058b153de76
3
+ metadata.gz: 04bf14074bb94dbb41edf733c9240f770026a9e51647359d4d1d55820c1f3c46
4
+ data.tar.gz: e5a8433b4284faf095da5ad4ebd102127b005a52b56b7651dadfa579a06188f1
5
5
  SHA512:
6
- metadata.gz: b5123ec85b0009c8ae4b3ae7c3532fc59f6501abaaf350f106cd0e64f9d2fec221bd54ca9384636edec370922391d48ceafdb044394a1367a45fdef4e372888e
7
- data.tar.gz: 5fa03e4c45c1b1e82b617fb50f79339740e3c431a0fefbda0c6af78ce4bb0e1e2539ae67b9494fc5fa694ccec1c8444772f179c6f283f6cc6ea20abe71eb850f
6
+ metadata.gz: e905a1f7fa40887d5c8bd78c428f1d377b42ead6586cf147f6334fafc6264d81ccbdb504e6ef2c8da3e473f40ad18b275f372cc70eea147435252d813ac948b8
7
+ data.tar.gz: 3d03fb7104356a787b85287455e74157761e92b159db33414685af565fd99e7ff596e9cd1f4ce7dd0e00f44e2c342618fc65f52ad91ed7ccedabd42db6d990c4
data/.rubocop.yml CHANGED
@@ -19,8 +19,17 @@ Layout/IndentFirstHashElement:
19
19
  Layout/IndentHeredoc:
20
20
  Enabled: false
21
21
 
22
+ Metrics/BlockLength:
23
+ Max: 40
24
+ Exclude:
25
+ # `context` in tests are blocks and get quite large, so exclude the test
26
+ # directory from having to adhere to this rule.
27
+ - "test/**/*.rb"
28
+
22
29
  Metrics/ClassLength:
23
30
  Exclude:
31
+ # Test classes get quite large, so exclude the test directory from having
32
+ # to adhere to this rule.
24
33
  - "test/**/*.rb"
25
34
 
26
35
  Metrics/LineLength:
data/.rubocop_todo.yml CHANGED
@@ -10,12 +10,6 @@
10
10
  Metrics/AbcSize:
11
11
  Max: 51
12
12
 
13
- # Offense count: 33
14
- # Configuration parameters: CountComments, ExcludedMethods.
15
- # ExcludedMethods: refine
16
- Metrics/BlockLength:
17
- Max: 509
18
-
19
13
  # Offense count: 12
20
14
  # Configuration parameters: CountComments.
21
15
  Metrics/ClassLength:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.5.0 - 2019-10-03
4
+ * [#859](https://github.com/stripe/stripe-ruby/pull/859) User-friendly messages and retries for `EOFError`, `Errno::ECONNRESET`, `Errno::ETIMEDOUT`, and `Errno::EHOSTUNREACH` network errors
5
+
6
+ ## 5.4.1 - 2019-10-01
7
+ * [#858](https://github.com/stripe/stripe-ruby/pull/858) Drop Timecop dependency
8
+
9
+ ## 5.4.0 - 2019-10-01
10
+ * [#857](https://github.com/stripe/stripe-ruby/pull/857) Move to monotonic time for duration calculations
11
+
12
+ ## 5.3.0 - 2019-10-01
13
+ * [#853](https://github.com/stripe/stripe-ruby/pull/853) Support `Stripe-Should-Retry` header
14
+
15
+ ## 5.2.0 - 2019-09-19
16
+ * [#851](https://github.com/stripe/stripe-ruby/pull/851) Introduce system for garbage collecting connection managers
17
+
3
18
  ## 5.1.1 - 2019-09-04
4
19
  * [#845](https://github.com/stripe/stripe-ruby/pull/845) Transfer the request_id from the http_headers to error.
5
20
 
data/Gemfile CHANGED
@@ -11,7 +11,6 @@ group :development do
11
11
  gem "rake"
12
12
  gem "shoulda-context"
13
13
  gem "test-unit"
14
- gem "timecop"
15
14
  gem "webmock"
16
15
 
17
16
  # Rubocop changes pretty quickly: new cops get added and old cops change
data/VERSION CHANGED
@@ -1 +1 @@
1
- 5.1.1
1
+ 5.5.0
@@ -6,7 +6,7 @@ module Stripe
6
6
  # that it's possible to do so from a static context (i.e. without a
7
7
  # pre-existing collection of subresources on the parent).
8
8
  #
9
- # For examle, a transfer gains the static methods for reversals so that the
9
+ # For example, a transfer gains the static methods for reversals so that the
10
10
  # methods `.create_reversal`, `.retrieve_reversal`, `.update_reversal`,
11
11
  # etc. all become available.
12
12
  module NestedResource
@@ -14,9 +14,11 @@ module Stripe
14
14
  resource_plural: nil)
15
15
  resource_plural ||= "#{resource}s"
16
16
  path ||= resource_plural
17
+
17
18
  raise ArgumentError, "operations array required" if operations.nil?
18
19
 
19
20
  resource_url_method = :"#{resource}s_url"
21
+
20
22
  define_singleton_method(resource_url_method) do |id, nested_id = nil|
21
23
  url = "#{resource_url}/#{CGI.escape(id)}/#{CGI.escape(path)}"
22
24
  url += "/#{CGI.escape(nested_id)}" unless nested_id.nil?
@@ -27,39 +29,39 @@ module Stripe
27
29
  case operation
28
30
  when :create
29
31
  define_singleton_method(:"create_#{resource}") \
30
- do |id, params = {}, opts = {}|
31
- url = send(resource_url_method, id)
32
- resp, opts = request(:post, url, params, opts)
33
- Util.convert_to_stripe_object(resp.data, opts)
34
- end
32
+ do |id, params = {}, opts = {}|
33
+ url = send(resource_url_method, id)
34
+ resp, opts = request(:post, url, params, opts)
35
+ Util.convert_to_stripe_object(resp.data, opts)
36
+ end
35
37
  when :retrieve
36
38
  define_singleton_method(:"retrieve_#{resource}") \
37
- do |id, nested_id, opts = {}|
38
- url = send(resource_url_method, id, nested_id)
39
- resp, opts = request(:get, url, {}, opts)
40
- Util.convert_to_stripe_object(resp.data, opts)
41
- end
39
+ do |id, nested_id, opts = {}|
40
+ url = send(resource_url_method, id, nested_id)
41
+ resp, opts = request(:get, url, {}, opts)
42
+ Util.convert_to_stripe_object(resp.data, opts)
43
+ end
42
44
  when :update
43
45
  define_singleton_method(:"update_#{resource}") \
44
- do |id, nested_id, params = {}, opts = {}|
45
- url = send(resource_url_method, id, nested_id)
46
- resp, opts = request(:post, url, params, opts)
47
- Util.convert_to_stripe_object(resp.data, opts)
48
- end
46
+ do |id, nested_id, params = {}, opts = {}|
47
+ url = send(resource_url_method, id, nested_id)
48
+ resp, opts = request(:post, url, params, opts)
49
+ Util.convert_to_stripe_object(resp.data, opts)
50
+ end
49
51
  when :delete
50
52
  define_singleton_method(:"delete_#{resource}") \
51
- do |id, nested_id, params = {}, opts = {}|
52
- url = send(resource_url_method, id, nested_id)
53
- resp, opts = request(:delete, url, params, opts)
54
- Util.convert_to_stripe_object(resp.data, opts)
55
- end
53
+ do |id, nested_id, params = {}, opts = {}|
54
+ url = send(resource_url_method, id, nested_id)
55
+ resp, opts = request(:delete, url, params, opts)
56
+ Util.convert_to_stripe_object(resp.data, opts)
57
+ end
56
58
  when :list
57
59
  define_singleton_method(:"list_#{resource_plural}") \
58
- do |id, params = {}, opts = {}|
59
- url = send(resource_url_method, id)
60
- resp, opts = request(:get, url, params, opts)
61
- Util.convert_to_stripe_object(resp.data, opts)
62
- end
60
+ do |id, params = {}, opts = {}|
61
+ url = send(resource_url_method, id)
62
+ resp, opts = request(:get, url, params, opts)
63
+ Util.convert_to_stripe_object(resp.data, opts)
64
+ end
63
65
  else
64
66
  raise ArgumentError, "Unknown operation: #{operation.inspect}"
65
67
  end
@@ -9,24 +9,36 @@ module Stripe
9
9
  #
10
10
  # Note that this class in itself is *not* thread safe. We expect it to be
11
11
  # instantiated once per thread.
12
- #
13
- # Note also that this class doesn't currently clean up after itself because
14
- # it expects to only ever have a few connections (unless `.clear` is called
15
- # manually). It'd be possible to tank memory by constantly changing the value
16
- # of `Stripe.api_base` or the like. A possible improvement might be to detect
17
- # and prune old connections whenever a request is executed.
18
12
  class ConnectionManager
13
+ # Timestamp (in seconds procured from the system's monotonic clock)
14
+ # indicating when the connection manager last made a request. This is used
15
+ # by `StripeClient` to determine whether a connection manager should be
16
+ # garbage collected or not.
17
+ attr_reader :last_used
18
+
19
19
  def initialize
20
20
  @active_connections = {}
21
+ @last_used = Util.monotonic_time
22
+
23
+ # A connection manager may be accessed across threads as one thread makes
24
+ # requests on it while another is trying to clear it (either because it's
25
+ # trying to garbage collect the manager or trying to clear it because a
26
+ # configuration setting has changed). That's probably thread-safe already
27
+ # because of Ruby's GIL, but just in case the library's running on JRuby
28
+ # or the like, use a mutex to synchronize access in this connection
29
+ # manager.
30
+ @mutex = Mutex.new
21
31
  end
22
32
 
23
33
  # Finishes any active connections by closing their TCP connection and
24
34
  # clears them from internal tracking.
25
35
  def clear
26
- @active_connections.each do |_, connection|
27
- connection.finish
36
+ @mutex.synchronize do
37
+ @active_connections.each do |_, connection|
38
+ connection.finish
39
+ end
40
+ @active_connections = {}
28
41
  end
29
- @active_connections = {}
30
42
  end
31
43
 
32
44
  # Gets a connection for a given URI. This is for internal use only as it's
@@ -35,17 +47,19 @@ module Stripe
35
47
  #
36
48
  # `uri` is expected to be a string.
37
49
  def connection_for(uri)
38
- u = URI.parse(uri)
39
- connection = @active_connections[[u.host, u.port]]
50
+ @mutex.synchronize do
51
+ u = URI.parse(uri)
52
+ connection = @active_connections[[u.host, u.port]]
40
53
 
41
- if connection.nil?
42
- connection = create_connection(u)
43
- connection.start
54
+ if connection.nil?
55
+ connection = create_connection(u)
56
+ connection.start
44
57
 
45
- @active_connections[[u.host, u.port]] = connection
46
- end
58
+ @active_connections[[u.host, u.port]] = connection
59
+ end
47
60
 
48
- connection
61
+ connection
62
+ end
49
63
  end
50
64
 
51
65
  # Executes an HTTP request to the given URI with the given method. Also
@@ -65,6 +79,8 @@ module Stripe
65
79
  raise ArgumentError, "query should be a string" \
66
80
  if query && !query.is_a?(String)
67
81
 
82
+ @last_used = Util.monotonic_time
83
+
68
84
  connection = connection_for(uri)
69
85
 
70
86
  u = URI.parse(uri)
@@ -74,7 +90,9 @@ module Stripe
74
90
  u.path
75
91
  end
76
92
 
77
- connection.send_request(method.to_s.upcase, path, body, headers)
93
+ @mutex.synchronize do
94
+ connection.send_request(method.to_s.upcase, path, body, headers)
95
+ end
78
96
  end
79
97
 
80
98
  #
@@ -5,18 +5,17 @@ module Stripe
5
5
  # recover both a resource a call returns as well as a response object that
6
6
  # contains information on the HTTP call.
7
7
  class StripeClient
8
- # A set of all known connection managers across all threads and a mutex to
8
+ # A set of all known thread contexts across all threads and a mutex to
9
9
  # synchronize global access to them.
10
- @all_connection_managers = []
11
- @all_connection_managers_mutex = Mutex.new
10
+ @thread_contexts_with_connection_managers = []
11
+ @thread_contexts_with_connection_managers_mutex = Mutex.new
12
+ @last_connection_manager_gc = Util.monotonic_time
12
13
 
13
- attr_accessor :connection_manager
14
-
15
- # Initializes a new `StripeClient`. Expects a `ConnectionManager` object,
16
- # and uses a default connection manager unless one is passed.
17
- def initialize(connection_manager = nil)
18
- self.connection_manager = connection_manager ||
19
- self.class.default_connection_manager
14
+ # Initializes a new `StripeClient`.
15
+ #
16
+ # Takes a connection manager object for backwards compatibility only, and
17
+ # that use is DEPRECATED.
18
+ def initialize(_connection_manager = nil)
20
19
  @system_profiler = SystemProfiler.new
21
20
  @last_request_metrics = nil
22
21
  end
@@ -42,17 +41,24 @@ module Stripe
42
41
  # Just a quick path for when configuration is being set for the first
43
42
  # time before any connections have been opened. There is technically some
44
43
  # potential for thread raciness here, but not in a practical sense.
45
- return if @all_connection_managers.empty?
46
-
47
- @all_connection_managers_mutex.synchronize do
48
- @all_connection_managers.each(&:clear)
44
+ return if @thread_contexts_with_connection_managers.empty?
45
+
46
+ @thread_contexts_with_connection_managers_mutex.synchronize do
47
+ @thread_contexts_with_connection_managers.each do |thread_context|
48
+ # Note that the thread context itself is not destroyed, but we clear
49
+ # its connection manager and remove our reference to it. If it ever
50
+ # makes a new request we'll give it a new connection manager and
51
+ # it'll go back into `@thread_contexts_with_connection_managers`.
52
+ thread_context.default_connection_manager.clear
53
+ thread_context.default_connection_manager = nil
54
+ end
55
+ @thread_contexts_with_connection_managers.clear
49
56
  end
50
57
  end
51
58
 
52
59
  # A default client for the current thread.
53
60
  def self.default_client
54
- current_thread_context.default_client ||=
55
- StripeClient.new(default_connection_manager)
61
+ current_thread_context.default_client ||= StripeClient.new
56
62
  end
57
63
 
58
64
  # A default connection manager for the current thread.
@@ -60,8 +66,9 @@ module Stripe
60
66
  current_thread_context.default_connection_manager ||= begin
61
67
  connection_manager = ConnectionManager.new
62
68
 
63
- @all_connection_managers_mutex.synchronize do
64
- @all_connection_managers << connection_manager
69
+ @thread_contexts_with_connection_managers_mutex.synchronize do
70
+ maybe_gc_connection_managers
71
+ @thread_contexts_with_connection_managers << current_thread_context
65
72
  end
66
73
 
67
74
  connection_manager
@@ -74,17 +81,23 @@ module Stripe
74
81
  def self.should_retry?(error, method:, num_retries:)
75
82
  return false if num_retries >= Stripe.max_network_retries
76
83
 
77
- # Retry on timeout-related problems (either on open or read).
78
- return true if error.is_a?(Net::OpenTimeout)
79
- return true if error.is_a?(Net::ReadTimeout)
84
+ case error
85
+ when Net::OpenTimeout, Net::ReadTimeout
86
+ # Retry on timeout-related problems (either on open or read).
87
+ true
88
+ when EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET,
89
+ Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError
90
+ # Destination refused the connection, the connection was reset, or a
91
+ # variety of other connection failures. This could occur from a single
92
+ # saturated server, so retry in case it's intermittent.
93
+ true
94
+ when Stripe::StripeError
95
+ # The API may ask us not to retry (e.g. if doing so would be a no-op),
96
+ # or advise us to retry (e.g. in cases of lock timeouts). Defer to
97
+ # those instructions if given.
98
+ return false if error.http_headers["stripe-should-retry"] == "false"
99
+ return true if error.http_headers["stripe-should-retry"] == "true"
80
100
 
81
- # Destination refused the connection, the connection was reset, or a
82
- # variety of other connection failures. This could occur from a single
83
- # saturated server, so retry in case it's intermittent.
84
- return true if error.is_a?(Errno::ECONNREFUSED)
85
- return true if error.is_a?(SocketError)
86
-
87
- if error.is_a?(Stripe::StripeError)
88
101
  # 409 Conflict
89
102
  return true if error.http_status == 409
90
103
 
@@ -106,10 +119,10 @@ module Stripe
106
119
  return true if error.http_status == 500 && method != :post
107
120
 
108
121
  # 503 Service Unavailable
109
- return true if error.http_status == 503
122
+ error.http_status == 503
123
+ else
124
+ false
110
125
  end
111
-
112
- false
113
126
  end
114
127
 
115
128
  def self.sleep_time(num_retries)
@@ -131,6 +144,15 @@ module Stripe
131
144
  sleep_seconds
132
145
  end
133
146
 
147
+ # Gets the connection manager in use for the current `StripeClient`.
148
+ #
149
+ # This method is DEPRECATED and for backwards compatibility only.
150
+ def connection_manager
151
+ self.class.default_connection_manager
152
+ end
153
+ extend Gem::Deprecate
154
+ deprecate :connection_manager, :none, 2020, 9
155
+
134
156
  # Executes the API call within the given block. Usage looks like:
135
157
  #
136
158
  # client = StripeClient.new
@@ -207,10 +229,10 @@ module Stripe
207
229
  context.query = query
208
230
 
209
231
  http_resp = execute_request_with_rescues(method, api_base, context) do
210
- connection_manager.execute_request(method, url,
211
- body: body,
212
- headers: headers,
213
- query: query)
232
+ self.class.default_connection_manager.execute_request(method, url,
233
+ body: body,
234
+ headers: headers,
235
+ query: query)
214
236
  end
215
237
 
216
238
  begin
@@ -233,6 +255,14 @@ module Stripe
233
255
  # private
234
256
  #
235
257
 
258
+ # Time (in seconds) that a connection manager has not been used before it's
259
+ # eligible for garbage collection.
260
+ CONNECTION_MANAGER_GC_LAST_USED_EXPIRY = 120
261
+
262
+ # How often to check (in seconds) for connection managers that haven't been
263
+ # used in a long time and which should be garbage collected.
264
+ CONNECTION_MANAGER_GC_PERIOD = 60
265
+
236
266
  ERROR_MESSAGE_CONNECTION =
237
267
  "Unexpected error communicating when trying to connect to " \
238
268
  "Stripe (%s). You may be seeing this message because your DNS is not " \
@@ -266,7 +296,11 @@ module Stripe
266
296
  # The original error message is also appended onto the final exception for
267
297
  # full transparency.
268
298
  NETWORK_ERROR_MESSAGES_MAP = {
299
+ EOFError => ERROR_MESSAGE_CONNECTION,
269
300
  Errno::ECONNREFUSED => ERROR_MESSAGE_CONNECTION,
301
+ Errno::ECONNRESET => ERROR_MESSAGE_CONNECTION,
302
+ Errno::EHOSTUNREACH => ERROR_MESSAGE_CONNECTION,
303
+ Errno::ETIMEDOUT => ERROR_MESSAGE_TIMEOUT_CONNECT,
270
304
  SocketError => ERROR_MESSAGE_CONNECTION,
271
305
 
272
306
  Net::OpenTimeout => ERROR_MESSAGE_TIMEOUT_CONNECT,
@@ -320,6 +354,46 @@ module Stripe
320
354
  Thread.current[:stripe_client__internal_use_only] ||= ThreadContext.new
321
355
  end
322
356
 
357
+ # Garbage collects connection managers that haven't been used in some time,
358
+ # with the idea being that we want to remove old connection managers that
359
+ # belong to dead threads and the like.
360
+ #
361
+ # Prefixed with `maybe_` because garbage collection will only run
362
+ # periodically so that we're not constantly engaged in busy work. If
363
+ # connection managers live a little passed their useful age it's not
364
+ # harmful, so it's not necessary to get them right away.
365
+ #
366
+ # For testability, returns `nil` if it didn't run and the number of
367
+ # connection managers that were garbage collected otherwise.
368
+ #
369
+ # IMPORTANT: This method is not thread-safe and expects to be called inside
370
+ # a lock on `@thread_contexts_with_connection_managers_mutex`.
371
+ #
372
+ # For internal use only. Does not provide a stable API and may be broken
373
+ # with future non-major changes.
374
+ def self.maybe_gc_connection_managers
375
+ next_gc_time = @last_connection_manager_gc + CONNECTION_MANAGER_GC_PERIOD
376
+ return nil if next_gc_time > Util.monotonic_time
377
+
378
+ last_used_threshold =
379
+ Util.monotonic_time - CONNECTION_MANAGER_GC_LAST_USED_EXPIRY
380
+
381
+ pruned_thread_contexts = []
382
+ @thread_contexts_with_connection_managers.each do |thread_context|
383
+ connection_manager = thread_context.default_connection_manager
384
+ next if connection_manager.last_used > last_used_threshold
385
+
386
+ connection_manager.clear
387
+ thread_context.default_connection_manager = nil
388
+ pruned_thread_contexts << thread_context
389
+ end
390
+
391
+ @thread_contexts_with_connection_managers -= pruned_thread_contexts
392
+ @last_connection_manager_gc = Util.monotonic_time
393
+
394
+ pruned_thread_contexts.count
395
+ end
396
+
323
397
  private def api_url(url = "", api_base = nil)
324
398
  (api_base || Stripe.api_base) + url
325
399
  end
@@ -375,7 +449,7 @@ module Stripe
375
449
  private def execute_request_with_rescues(method, api_base, context)
376
450
  num_retries = 0
377
451
  begin
378
- request_start = Time.now
452
+ request_start = Util.monotonic_time
379
453
  log_request(context, num_retries)
380
454
  resp = yield
381
455
  context = context.dup_from_response_headers(resp)
@@ -385,7 +459,8 @@ module Stripe
385
459
  log_response(context, request_start, resp.code.to_i, resp.body)
386
460
 
387
461
  if Stripe.enable_telemetry? && context.request_id
388
- request_duration_ms = ((Time.now - request_start) * 1000).to_int
462
+ request_duration_ms =
463
+ ((Util.monotonic_time - request_start) * 1000).to_int
389
464
  @last_request_metrics =
390
465
  StripeRequestMetrics.new(context.request_id, request_duration_ms)
391
466
  end
@@ -656,7 +731,7 @@ module Stripe
656
731
  Util.log_info("Response from Stripe API",
657
732
  account: context.account,
658
733
  api_version: context.api_version,
659
- elapsed: Time.now - request_start,
734
+ elapsed: Util.monotonic_time - request_start,
660
735
  idempotency_key: context.idempotency_key,
661
736
  method: context.method,
662
737
  path: context.path,
@@ -678,7 +753,7 @@ module Stripe
678
753
 
679
754
  private def log_response_error(context, request_start, error)
680
755
  Util.log_error("Request error",
681
- elapsed: Time.now - request_start,
756
+ elapsed: Util.monotonic_time - request_start,
682
757
  error_message: error.message,
683
758
  idempotency_key: context.idempotency_key,
684
759
  method: context.method,
data/lib/stripe/util.rb CHANGED
@@ -172,6 +172,18 @@ module Stripe
172
172
  result
173
173
  end
174
174
 
175
+ # `Time.now` can be unstable in cases like an administrator manually
176
+ # updating its value or a reconcilation via NTP. For this reason, prefer
177
+ # the use of the system's monotonic clock especially where comparing times
178
+ # to calculate an elapsed duration.
179
+ #
180
+ # Shortcut for getting monotonic time, mostly for purposes of line length
181
+ # and test stubbing. Returns time in seconds since the event used for
182
+ # monotonic reference purposes by the platform (e.g. system boot time).
183
+ def self.monotonic_time
184
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
185
+ end
186
+
175
187
  def self.normalize_id(id)
176
188
  if id.is_a?(Hash) # overloaded id
177
189
  params_hash = id.dup
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stripe
4
- VERSION = "5.1.1"
4
+ VERSION = "5.5.0"
5
5
  end
@@ -8,6 +8,14 @@ module Stripe
8
8
  @manager = Stripe::ConnectionManager.new
9
9
  end
10
10
 
11
+ context "#initialize" do
12
+ should "set #last_used to current time" do
13
+ t = 123.0
14
+ Util.stubs(:monotonic_time).returns(t)
15
+ assert_equal t, Stripe::ConnectionManager.new.last_used
16
+ end
17
+ end
18
+
11
19
  context "#clear" do
12
20
  should "clear any active connections" do
13
21
  stub_request(:post, "#{Stripe.api_base}/path")
@@ -133,6 +141,23 @@ module Stripe
133
141
  end
134
142
  assert_equal e.message, "query should be a string"
135
143
  end
144
+
145
+ should "set #last_used to current time" do
146
+ stub_request(:post, "#{Stripe.api_base}/path")
147
+ .to_return(body: JSON.generate(object: "account"))
148
+
149
+ t = 123.0
150
+ Util.stubs(:monotonic_time).returns(t)
151
+
152
+ manager = Stripe::ConnectionManager.new
153
+
154
+ # `#last_used` is also set by the constructor, so make sure we get a
155
+ # new value for it.
156
+ Util.stubs(:monotonic_time).returns(t + 1.0)
157
+
158
+ manager.execute_request(:post, "#{Stripe.api_base}/path")
159
+ assert_equal t + 1.0, manager.last_used
160
+ end
136
161
  end
137
162
  end
138
163
  end
@@ -17,6 +17,76 @@ module Stripe
17
17
  end
18
18
  end
19
19
 
20
+ context ".maybe_gc_connection_managers" do
21
+ should "garbage collect old connection managers when appropriate" do
22
+ stub_request(:post, "#{Stripe.api_base}/v1/path")
23
+ .to_return(body: JSON.generate(object: "account"))
24
+
25
+ # Make sure we start with a blank slate (state may have been left in
26
+ # place by other tests).
27
+ StripeClient.clear_all_connection_managers
28
+
29
+ # Establish a base time.
30
+ t = 0.0
31
+
32
+ # And pretend that `StripeClient` was just initialized for the first
33
+ # time. (Don't access instance variables like this, but it's tricky to
34
+ # test properly otherwise.)
35
+ StripeClient.instance_variable_set(:@last_connection_manager_gc, t)
36
+
37
+ #
38
+ # t
39
+ #
40
+ Util.stubs(:monotonic_time).returns(t)
41
+
42
+ # Execute an initial request to ensure that a connection manager was
43
+ # created.
44
+ client = StripeClient.new
45
+ client.execute_request(:post, "/v1/path")
46
+
47
+ # The GC shouldn't run yet (a `nil` return indicates that GC didn't run).
48
+ assert_equal nil, StripeClient.maybe_gc_connection_managers
49
+
50
+ #
51
+ # t + StripeClient::CONNECTION_MANAGER_GC_PERIOD - 1
52
+ #
53
+ # Move time to just *before* garbage collection is eligible to run.
54
+ # Nothing should happen.
55
+ #
56
+ Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_PERIOD - 1)
57
+
58
+ assert_equal nil, StripeClient.maybe_gc_connection_managers
59
+
60
+ #
61
+ # t + StripeClient::CONNECTION_MANAGER_GC_PERIOD + 1
62
+ #
63
+ # Move time to just *after* garbage collection is eligible to run.
64
+ # Garbage collection will run, but because the connection manager is
65
+ # not passed its expiry age, it will not be collected. Zero is returned
66
+ # to indicate so.
67
+ #
68
+ Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_PERIOD + 1)
69
+
70
+ assert_equal 0, StripeClient.maybe_gc_connection_managers
71
+
72
+ #
73
+ # t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1
74
+ #
75
+ # Move us far enough into the future that we're passed the horizons for
76
+ # both a GC run as well as well as the expiry age of a connection
77
+ # manager. That means the GC will run and collect the connection
78
+ # manager that we created above.
79
+ #
80
+ Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1)
81
+
82
+ assert_equal 1, StripeClient.maybe_gc_connection_managers
83
+
84
+ # And as an additional check, the connection manager of the current
85
+ # thread context should have been set to `nil` as it was GCed.
86
+ assert_nil StripeClient.current_thread_context.default_connection_manager
87
+ end
88
+ end
89
+
20
90
  context ".clear_all_connection_managers" do
21
91
  should "clear connection managers across all threads" do
22
92
  stub_request(:post, "#{Stripe.api_base}/path")
@@ -102,6 +172,26 @@ module Stripe
102
172
  method: :post, num_retries: 0)
103
173
  end
104
174
 
175
+ should "retry on EOFError" do
176
+ assert StripeClient.should_retry?(EOFError.new,
177
+ method: :post, num_retries: 0)
178
+ end
179
+
180
+ should "retry on Errno::ECONNRESET" do
181
+ assert StripeClient.should_retry?(Errno::ECONNRESET.new,
182
+ method: :post, num_retries: 0)
183
+ end
184
+
185
+ should "retry on Errno::ETIMEDOUT" do
186
+ assert StripeClient.should_retry?(Errno::ETIMEDOUT.new,
187
+ method: :post, num_retries: 0)
188
+ end
189
+
190
+ should "retry on Errno::EHOSTUNREACH" do
191
+ assert StripeClient.should_retry?(Errno::EHOSTUNREACH.new,
192
+ method: :post, num_retries: 0)
193
+ end
194
+
105
195
  should "retry on Net::OpenTimeout" do
106
196
  assert StripeClient.should_retry?(Net::OpenTimeout.new,
107
197
  method: :post, num_retries: 0)
@@ -117,6 +207,28 @@ module Stripe
117
207
  method: :post, num_retries: 0)
118
208
  end
119
209
 
210
+ should "retry when the `Stripe-Should-Retry` header is `true`" do
211
+ headers = StripeResponse::Headers.new(
212
+ "Stripe-Should-Retry" => ["true"]
213
+ )
214
+
215
+ # Note we send status 400 here, which would normally not be retried.
216
+ assert StripeClient.should_retry?(Stripe::StripeError.new(http_headers: headers,
217
+ http_status: 400),
218
+ method: :post, num_retries: 0)
219
+ end
220
+
221
+ should "not retry when the `Stripe-Should-Retry` header is `false`" do
222
+ headers = StripeResponse::Headers.new(
223
+ "Stripe-Should-Retry" => ["false"]
224
+ )
225
+
226
+ # Note we send status 409 here, which would normally be retried.
227
+ refute StripeClient.should_retry?(Stripe::StripeError.new(http_headers: headers,
228
+ http_status: 409),
229
+ method: :post, num_retries: 0)
230
+ end
231
+
120
232
  should "retry on a 409 Conflict" do
121
233
  assert StripeClient.should_retry?(Stripe::StripeError.new(http_status: 409),
122
234
  method: :post, num_retries: 0)
@@ -199,20 +311,6 @@ module Stripe
199
311
  end
200
312
  end
201
313
 
202
- context "#initialize" do
203
- should "set Stripe.default_connection_manager" do
204
- client = StripeClient.new
205
- assert_equal StripeClient.default_connection_manager,
206
- client.connection_manager
207
- end
208
-
209
- should "set a different connection if one was specified" do
210
- connection_manager = ConnectionManager.new
211
- client = StripeClient.new(connection_manager)
212
- assert_equal connection_manager, client.connection_manager
213
- end
214
- end
215
-
216
314
  context "#execute_request" do
217
315
  context "headers" do
218
316
  should "support literal headers" do
@@ -239,16 +337,11 @@ module Stripe
239
337
  context "logging" do
240
338
  setup do
241
339
  # Freeze time for the purposes of the `elapsed` parameter that we
242
- # emit for responses. I didn't want to bring in a new dependency for
243
- # this, but Mocha's `anything` parameter can't match inside of a hash
244
- # and is therefore not useful for this purpose. If we switch over to
245
- # rspec-mocks at some point, we can probably remove Timecop from the
246
- # project.
247
- Timecop.freeze(Time.local(1990))
248
- end
249
-
250
- teardown do
251
- Timecop.return
340
+ # emit for responses. Mocha's `anything` parameter can't match inside
341
+ # of a hash and is therefore not useful for this purpose. If we
342
+ # switch over to rspec-mocks at some point, we can probably remove
343
+ # this.
344
+ Util.stubs(:monotonic_time).returns(0.0)
252
345
  end
253
346
 
254
347
  should "produce appropriate logging" do
@@ -470,10 +563,10 @@ module Stripe
470
563
  .to_return(body: "", status: 500)
471
564
 
472
565
  client = StripeClient.new
566
+
473
567
  e = assert_raises Stripe::APIError do
474
568
  client.execute_request(:post, "/v1/charges")
475
569
  end
476
-
477
570
  assert_equal 'Invalid response object from API: "" (HTTP response code was 500)', e.message
478
571
  end
479
572
 
@@ -482,22 +575,36 @@ module Stripe
482
575
  .to_return(body: "", status: 200)
483
576
 
484
577
  client = StripeClient.new
578
+
485
579
  e = assert_raises Stripe::APIError do
486
580
  client.execute_request(:post, "/v1/charges")
487
581
  end
488
-
489
582
  assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
490
583
  end
491
584
 
585
+ should "feed a request ID through to the error object" do
586
+ stub_request(:post, "#{Stripe.api_base}/v1/charges")
587
+ .to_return(body: JSON.generate(make_missing_id_error),
588
+ headers: { "Request-ID": "req_123" },
589
+ status: 400)
590
+
591
+ client = StripeClient.new
592
+
593
+ e = assert_raises Stripe::InvalidRequestError do
594
+ client.execute_request(:post, "/v1/charges")
595
+ end
596
+ assert_equal("req_123", e.request_id)
597
+ end
598
+
492
599
  should "handle low level error" do
493
600
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
494
601
  .to_raise(Errno::ECONNREFUSED.new)
495
602
 
496
603
  client = StripeClient.new
604
+
497
605
  e = assert_raises Stripe::APIConnectionError do
498
606
  client.execute_request(:post, "/v1/charges")
499
607
  end
500
-
501
608
  assert_equal StripeClient::ERROR_MESSAGE_CONNECTION % Stripe.api_base +
502
609
  "\n\n(Network error: Connection refused)",
503
610
  e.message
@@ -508,10 +615,10 @@ module Stripe
508
615
  .to_return(body: JSON.generate(bar: "foo"), status: 500)
509
616
 
510
617
  client = StripeClient.new
618
+
511
619
  e = assert_raises Stripe::APIError do
512
620
  client.execute_request(:post, "/v1/charges")
513
621
  end
514
-
515
622
  assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message
516
623
  end
517
624
 
@@ -521,6 +628,7 @@ module Stripe
521
628
 
522
629
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
523
630
  .to_return(body: JSON.generate(data), status: 400)
631
+
524
632
  client = StripeClient.new
525
633
 
526
634
  e = assert_raises Stripe::IdempotencyError do
@@ -533,75 +641,81 @@ module Stripe
533
641
  should "raise InvalidRequestError on other 400s" do
534
642
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
535
643
  .to_return(body: JSON.generate(make_missing_id_error), status: 400)
644
+
536
645
  client = StripeClient.new
537
- begin
646
+
647
+ e = assert_raises Stripe::InvalidRequestError do
538
648
  client.execute_request(:post, "/v1/charges")
539
- rescue Stripe::InvalidRequestError => e
540
- assert_equal(400, e.http_status)
541
- assert_equal(true, e.json_body.is_a?(Hash))
542
649
  end
650
+ assert_equal(400, e.http_status)
651
+ assert_equal(true, e.json_body.is_a?(Hash))
543
652
  end
544
653
 
545
654
  should "raise AuthenticationError on 401" do
546
655
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
547
656
  .to_return(body: JSON.generate(make_missing_id_error), status: 401)
657
+
548
658
  client = StripeClient.new
549
- begin
659
+
660
+ e = assert_raises Stripe::AuthenticationError do
550
661
  client.execute_request(:post, "/v1/charges")
551
- rescue Stripe::AuthenticationError => e
552
- assert_equal(401, e.http_status)
553
- assert_equal(true, e.json_body.is_a?(Hash))
554
662
  end
663
+ assert_equal(401, e.http_status)
664
+ assert_equal(true, e.json_body.is_a?(Hash))
555
665
  end
556
666
 
557
667
  should "raise CardError on 402" do
558
668
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
559
669
  .to_return(body: JSON.generate(make_invalid_exp_year_error), status: 402)
670
+
560
671
  client = StripeClient.new
561
- begin
672
+
673
+ e = assert_raises Stripe::CardError do
562
674
  client.execute_request(:post, "/v1/charges")
563
- rescue Stripe::CardError => e
564
- assert_equal(402, e.http_status)
565
- assert_equal(true, e.json_body.is_a?(Hash))
566
- assert_equal("invalid_expiry_year", e.code)
567
- assert_equal("exp_year", e.param)
568
675
  end
676
+ assert_equal(402, e.http_status)
677
+ assert_equal(true, e.json_body.is_a?(Hash))
678
+ assert_equal("invalid_expiry_year", e.code)
679
+ assert_equal("exp_year", e.param)
569
680
  end
570
681
 
571
682
  should "raise PermissionError on 403" do
572
683
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
573
684
  .to_return(body: JSON.generate(make_missing_id_error), status: 403)
685
+
574
686
  client = StripeClient.new
575
- begin
687
+
688
+ e = assert_raises Stripe::PermissionError do
576
689
  client.execute_request(:post, "/v1/charges")
577
- rescue Stripe::PermissionError => e
578
- assert_equal(403, e.http_status)
579
- assert_equal(true, e.json_body.is_a?(Hash))
580
690
  end
691
+ assert_equal(403, e.http_status)
692
+ assert_equal(true, e.json_body.is_a?(Hash))
581
693
  end
582
694
 
583
695
  should "raise InvalidRequestError on 404" do
584
696
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
585
697
  .to_return(body: JSON.generate(make_missing_id_error), status: 404)
698
+
586
699
  client = StripeClient.new
587
- begin
700
+
701
+ e = assert_raises Stripe::InvalidRequestError do
588
702
  client.execute_request(:post, "/v1/charges")
589
- rescue Stripe::InvalidRequestError => e
590
- assert_equal(404, e.http_status)
591
- assert_equal(true, e.json_body.is_a?(Hash))
592
703
  end
704
+ assert_equal(404, e.http_status)
705
+ assert_equal(true, e.json_body.is_a?(Hash))
593
706
  end
594
707
 
595
708
  should "raise RateLimitError on 429" do
596
709
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
597
710
  .to_return(body: JSON.generate(make_rate_limit_error), status: 429)
711
+
598
712
  client = StripeClient.new
599
- begin
713
+
714
+ e = assert_raises Stripe::RateLimitError do
600
715
  client.execute_request(:post, "/v1/charges")
601
- rescue Stripe::RateLimitError => e
602
- assert_equal(429, e.http_status)
603
- assert_equal(true, e.json_body.is_a?(Hash))
604
716
  end
717
+ assert_equal(429, e.http_status)
718
+ assert_equal(true, e.json_body.is_a?(Hash))
605
719
  end
606
720
 
607
721
  should "raise OAuth::InvalidRequestError when error is a string with value 'invalid_request'" do
@@ -610,6 +724,7 @@ module Stripe
610
724
  error_description: "No grant type specified"), status: 400)
611
725
 
612
726
  client = StripeClient.new
727
+
613
728
  opts = { api_base: Stripe.connect_base }
614
729
  e = assert_raises Stripe::OAuth::InvalidRequestError do
615
730
  client.execute_request(:post, "/oauth/token", opts)
@@ -625,6 +740,7 @@ module Stripe
625
740
  error_description: "This authorization code has already been used. All tokens issued with this code have been revoked."), status: 400)
626
741
 
627
742
  client = StripeClient.new
743
+
628
744
  opts = { api_base: Stripe.connect_base }
629
745
  e = assert_raises Stripe::OAuth::InvalidGrantError do
630
746
  client.execute_request(:post, "/oauth/token", opts)
@@ -641,6 +757,7 @@ module Stripe
641
757
  error_description: "This application is not connected to stripe account acct_19tLK7DSlTMT26Mk, or that account does not exist."), status: 401)
642
758
 
643
759
  client = StripeClient.new
760
+
644
761
  opts = { api_base: Stripe.connect_base }
645
762
  e = assert_raises Stripe::OAuth::InvalidClientError do
646
763
  client.execute_request(:post, "/oauth/deauthorize", opts)
@@ -657,6 +774,7 @@ module Stripe
657
774
  error_description: "Something."), status: 401)
658
775
 
659
776
  client = StripeClient.new
777
+
660
778
  opts = { api_base: Stripe.connect_base }
661
779
  e = assert_raises Stripe::OAuth::OAuthError do
662
780
  client.execute_request(:post, "/oauth/deauthorize", opts)
@@ -817,6 +935,22 @@ module Stripe
817
935
  end
818
936
  end
819
937
 
938
+ context "#connection_manager" do
939
+ should "warn that #connection_manager is deprecated" do
940
+ old_stderr = $stderr
941
+ $stderr = StringIO.new
942
+ begin
943
+ client = StripeClient.new
944
+ client.connection_manager
945
+ message = "NOTE: Stripe::StripeClient#connection_manager is " \
946
+ "deprecated"
947
+ assert_match Regexp.new(message), $stderr.string
948
+ ensure
949
+ $stderr = old_stderr
950
+ end
951
+ end
952
+ end
953
+
820
954
  context "#request" do
821
955
  should "return a result and response object" do
822
956
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
data/test/test_helper.rb CHANGED
@@ -8,7 +8,6 @@ require "test/unit"
8
8
  require "mocha/setup"
9
9
  require "stringio"
10
10
  require "shoulda/context"
11
- require "timecop"
12
11
  require "webmock/test_unit"
13
12
 
14
13
  PROJECT_ROOT = ::File.expand_path("../", __dir__)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stripe
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.1
4
+ version: 5.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-04 00:00:00.000000000 Z
11
+ date: 2019-10-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Stripe is the easiest way to accept payments online. See https://stripe.com
14
14
  for details.