Class: Stripe::StripeClient

Inherits:
Object
  • Object
show all
Extended by:
Gem::Deprecate
Defined in:
lib/stripe/stripe_client.rb

Overview

StripeClient executes requests against the Stripe API and allows a user to recover both a resource a call returns as well as a response object that contains information on the HTTP call.

Defined Under Namespace

Classes: RequestLogContext, StripeRequestMetrics, SystemProfiler, ThreadContext

Constant Summary collapse

CONNECTION_MANAGER_GC_LAST_USED_EXPIRY =

Time (in seconds) that a connection manager has not been used before it's eligible for garbage collection.

120
CONNECTION_MANAGER_GC_PERIOD =

How often to check (in seconds) for connection managers that haven't been used in a long time and which should be garbage collected.

60
ERROR_MESSAGE_CONNECTION =
"Unexpected error communicating when trying to connect to " \
"Stripe (%s). You may be seeing this message because your DNS is not " \
"working or you don't have an internet connection.  To check, try " \
"running `host stripe.com` from the command line."
ERROR_MESSAGE_SSL =
"Could not establish a secure connection to Stripe (%s), you " \
"may need to upgrade your OpenSSL version. To check, try running " \
"`openssl s_client -connect api.stripe.com:443` from the command " \
"line."
ERROR_MESSAGE_TIMEOUT_SUFFIX =

Common error suffix sared by both connect and read timeout messages.

"Please check your internet connection and try again. " \
"If this problem persists, you should check Stripe's service " \
"status at https://status.stripe.com, or let us know at " \
"[email protected]"
ERROR_MESSAGE_TIMEOUT_CONNECT =
(
  "Timed out connecting to Stripe (%s). " +
  ERROR_MESSAGE_TIMEOUT_SUFFIX
).freeze
ERROR_MESSAGE_TIMEOUT_READ =
(
  "Timed out communicating with Stripe (%s). " +
  ERROR_MESSAGE_TIMEOUT_SUFFIX
).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_arg = {}) ⇒ StripeClient

Initializes a new StripeClient


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/stripe/stripe_client.rb', line 17

def initialize(config_arg = {})
  @system_profiler = SystemProfiler.new
  @last_request_metrics = nil

  @config = case config_arg
            when Hash
              Stripe.config.reverse_duplicate_merge(config_arg)
            when Stripe::ConnectionManager
              # Supports accepting a connection manager object for backwards
              # compatibility only, and that use is DEPRECATED.
              Stripe.config.dup
            when Stripe::StripeConfiguration
              config_arg
            when String
              Stripe.config.reverse_duplicate_merge(
                { api_key: config_arg }
              )
            else
              raise ArgumentError, "Can't handle argument: #{config_arg}"
            end
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.


39
40
41
# File 'lib/stripe/stripe_client.rb', line 39

def config
  @config
end

#optionsObject (readonly)

Returns the value of attribute options.


40
41
42
# File 'lib/stripe/stripe_client.rb', line 40

def options
  @options
end

Class Method Details

.active_clientObject

Gets a currently active `StripeClient`. Set for the current thread when `StripeClient#request` is being run so that API operations being executed inside of that block can find the currently active client. It's reset to the original value (hopefully `nil`) after the block ends.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.


49
50
51
# File 'lib/stripe/stripe_client.rb', line 49

def self.active_client
  current_thread_context.active_client || default_client
end

.clear_all_connection_managers(config: nil) ⇒ Object

Finishes any active connections by closing their TCP connection and clears them from internal tracking in all connection managers across all threads.

If passed a `config` object, only clear connection managers for that particular configuration.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/stripe/stripe_client.rb', line 62

def self.clear_all_connection_managers(config: nil)
  # Just a quick path for when configuration is being set for the first
  # time before any connections have been opened. There is technically some
  # potential for thread raciness here, but not in a practical sense.
  return if @thread_contexts_with_connection_managers.empty?

  @thread_contexts_with_connection_managers_mutex.synchronize do
    pruned_contexts = Set.new

    @thread_contexts_with_connection_managers.each do |thread_context|
      # Note that the thread context itself is not destroyed, but we clear
      # its connection manager and remove our reference to it. If it ever
      # makes a new request we'll give it a new connection manager and
      # it'll go back into `@thread_contexts_with_connection_managers`.
      thread_context.default_connection_managers.reject! do |cm_config, cm|
        if config.nil? || config.key == cm_config
          cm.clear
          true
        end
      end

      if thread_context.default_connection_managers.empty?
        pruned_contexts << thread_context
      end
    end

    @thread_contexts_with_connection_managers.subtract(pruned_contexts)
  end
end

.current_thread_contextObject

Access data stored for `StripeClient` within the thread's current context. Returns `ThreadContext`.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.


381
382
383
# File 'lib/stripe/stripe_client.rb', line 381

def self.current_thread_context
  Thread.current[:stripe_client__internal_use_only] ||= ThreadContext.new
end

.default_clientObject

A default client for the current thread.


93
94
95
# File 'lib/stripe/stripe_client.rb', line 93

def self.default_client
  current_thread_context.default_client ||= StripeClient.new(Stripe.config)
end

.default_connection_manager(config = Stripe.config) ⇒ Object

A default connection manager for the current thread scoped to the configuration object that may be provided.


99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/stripe/stripe_client.rb', line 99

def self.default_connection_manager(config = Stripe.config)
  current_thread_context.default_connection_managers[config.key] ||= begin
    connection_manager = ConnectionManager.new(config)

    @thread_contexts_with_connection_managers_mutex.synchronize do
      maybe_gc_connection_managers
      @thread_contexts_with_connection_managers << current_thread_context
    end

    connection_manager
  end
end

.maybe_gc_connection_managersObject

Garbage collects connection managers that haven't been used in some time, with the idea being that we want to remove old connection managers that belong to dead threads and the like.

Prefixed with `maybe_` because garbage collection will only run periodically so that we're not constantly engaged in busy work. If connection managers live a little passed their useful age it's not harmful, so it's not necessary to get them right away.

For testability, returns `nil` if it didn't run and the number of connection managers that were garbage collected otherwise.

IMPORTANT: This method is not thread-safe and expects to be called inside a lock on `@thread_contexts_with_connection_managers_mutex`.

For internal use only. Does not provide a stable API and may be broken with future non-major changes.


402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/stripe/stripe_client.rb', line 402

def self.maybe_gc_connection_managers
  next_gc_time = @last_connection_manager_gc + CONNECTION_MANAGER_GC_PERIOD
  return nil if next_gc_time > Util.monotonic_time

  last_used_threshold =
    Util.monotonic_time - CONNECTION_MANAGER_GC_LAST_USED_EXPIRY

  pruned_contexts = []
  @thread_contexts_with_connection_managers.each do |thread_context|
    thread_context
      .default_connection_managers
      .each do |config_key, connection_manager|
        next if connection_manager.last_used > last_used_threshold

        connection_manager.clear
        thread_context.default_connection_managers.delete(config_key)
      end
  end

  @thread_contexts_with_connection_managers.each do |thread_context|
    next unless thread_context.default_connection_managers.empty?

    pruned_contexts << thread_context
  end

  @thread_contexts_with_connection_managers -= pruned_contexts
  @last_connection_manager_gc = Util.monotonic_time

  pruned_contexts.count
end

.should_retry?(error, method:, num_retries:, config: Stripe.config) ⇒ Boolean

Checks if an error is a problem that we should retry on. This includes both socket errors that may represent an intermittent problem and some special HTTP statuses.

Returns:

  • (Boolean)

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/stripe/stripe_client.rb', line 115

def self.should_retry?(error,
                       method:, num_retries:, config: Stripe.config)
  return false if num_retries >= config.max_network_retries

  case error
  when Net::OpenTimeout, Net::ReadTimeout
    # Retry on timeout-related problems (either on open or read).
    true
  when EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET,
        Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError
    # Destination refused the connection, the connection was reset, or a
    # variety of other connection failures. This could occur from a single
    # saturated server, so retry in case it's intermittent.
    true
  when Stripe::StripeError
    # The API may ask us not to retry (e.g. if doing so would be a no-op),
    # or advise us to retry (e.g. in cases of lock timeouts). Defer to
    # those instructions if given.
    return false if error.http_headers["stripe-should-retry"] == "false"
    return true if error.http_headers["stripe-should-retry"] == "true"

    # 409 Conflict
    return true if error.http_status == 409

    # 429 Too Many Requests
    #
    # There are a few different problems that can lead to a 429. The most
    # common is rate limiting, on which we *don't* want to retry because
    # that'd likely contribute to more contention problems. However, some
    # 429s are lock timeouts, which is when a request conflicted with
    # another request or an internal process on some particular object.
    # These 429s are safe to retry.
    return true if error.http_status == 429 && error.code == "lock_timeout"

    # 500 Internal Server Error
    #
    # We only bother retrying these for non-POST requests. POSTs end up
    # being cached by the idempotency layer so there's no purpose in
    # retrying them.
    return true if error.http_status == 500 && method != :post

    # 503 Service Unavailable
    error.http_status == 503
  else
    false
  end
end

.sleep_time(num_retries, config: Stripe.config) ⇒ Object


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/stripe/stripe_client.rb', line 163

def self.sleep_time(num_retries, config: Stripe.config)
  # Apply exponential backoff with initial_network_retry_delay on the
  # number of num_retries so far as inputs. Do not allow the number to
  # exceed max_network_retry_delay.
  sleep_seconds = [
    config.initial_network_retry_delay * (2**(num_retries - 1)),
    config.max_network_retry_delay,
  ].min

  # Apply some jitter by randomizing the value in the range of
  # (sleep_seconds / 2) to (sleep_seconds).
  sleep_seconds *= (0.5 * (1 + rand))

  # But never sleep less than the base sleep seconds.
  [config.initial_network_retry_delay, sleep_seconds].max
end

Instance Method Details

#connection_managerObject

Gets the connection manager in use for the current `StripeClient`.

This method is DEPRECATED and for backwards compatibility only.


183
184
185
# File 'lib/stripe/stripe_client.rb', line 183

def connection_manager
  self.class.default_connection_manager
end

#execute_request(method, path, api_base: nil, api_key: nil, headers: {}, params: {}) ⇒ Object


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/stripe/stripe_client.rb', line 214

def execute_request(method, path,
                    api_base: nil, api_key: nil, headers: {}, params: {})
  http_resp, api_key = execute_request_internal(
    method, path, api_base, api_key, headers, params
  )

  begin
    resp = StripeResponse.from_net_http(http_resp)
  rescue JSON::ParserError
    raise general_api_error(http_resp.code.to_i, http_resp.body)
  end

  # If being called from `StripeClient#request`, put the last response in
  # thread-local memory so that it can be returned to the user. Don't store
  # anything otherwise so that we don't leak memory.
  store_last_response(object_id, resp)

  [resp, api_key]
end

#execute_request_stream(method, path, api_base: nil, api_key: nil, headers: {}, params: {}, &read_body_chunk_block) ⇒ Object

Executes a request and returns the body as a stream instead of converting it to a StripeObject. This should be used for any request where we expect an arbitrary binary response.

A `read_body_chunk` block can be passed, which will be called repeatedly with the body chunks read from the socket.

If a block is passed, a StripeHeadersOnlyResponse is returned as the block is expected to do all the necessary body processing. If no block is passed, then a StripeStreamResponse is returned containing an IO stream with the response body.


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/stripe/stripe_client.rb', line 245

def execute_request_stream(method, path,
                           api_base: nil, api_key: nil,
                           headers: {}, params: {},
                           &read_body_chunk_block)
  unless block_given?
    raise ArgumentError,
          "execute_request_stream requires a read_body_chunk_block"
  end

  http_resp, api_key = execute_request_internal(
    method, path, api_base, api_key, headers, params, &read_body_chunk_block
  )

  # When the read_body_chunk_block is given, we no longer have access to the
  # response body at this point and so return a response object containing
  # only the headers. This is because the body was consumed by the block.
  resp = StripeHeadersOnlyResponse.from_net_http(http_resp)

  [resp, api_key]
end

#last_response_has_key?(object_id) ⇒ Boolean

Returns:

  • (Boolean)

272
273
274
# File 'lib/stripe/stripe_client.rb', line 272

def last_response_has_key?(object_id)
  self.class.current_thread_context.last_responses&.key?(object_id)
end

#requestObject

Executes the API call within the given block. Usage looks like:

client = StripeClient.new
charge, resp = client.request { Charge.create }

194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/stripe/stripe_client.rb', line 194

def request
  old_stripe_client = self.class.current_thread_context.active_client
  self.class.current_thread_context.active_client = self

  if self.class.current_thread_context.last_responses&.key?(object_id)
    raise "calls to StripeClient#request cannot be nested within a thread"
  end

  self.class.current_thread_context.last_responses ||= {}
  self.class.current_thread_context.last_responses[object_id] = nil

  begin
    res = yield
    [res, self.class.current_thread_context.last_responses[object_id]]
  ensure
    self.class.current_thread_context.active_client = old_stripe_client
    self.class.current_thread_context.last_responses.delete(object_id)
  end
end

#store_last_response(object_id, resp) ⇒ Object


266
267
268
269
270
# File 'lib/stripe/stripe_client.rb', line 266

def store_last_response(object_id, resp)
  return unless last_response_has_key?(object_id)

  self.class.current_thread_context.last_responses[object_id] = resp
end