stripe 5.1.1 → 5.5.0

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: 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.