stripe-ruby/test/stripe/connection_manager_test.rb
Brandur cbf44035b8 Introduce system for garbage collecting connection managers (#851)
Introduces a system for garbage collecting connection managers in an
attempt to solve #850.

Previously, the number of connection managers (and by extension the
number of connections that they were holding) would stay stable if a
program used a stable number of threads. However, if threads were used
disposably, the number of active connection managers out there could
continue to grow unchecked, and each of those could be holding one or
more dead connections which are no longer open, but still holding a file
descriptor waiting to be unlinked in disposed of by Ruby's GC.

This PR introduces a connection manager garbage collector that runs
periodically whenever a new connection manager is created. Connection
managers get a timestamp to indicate when they were last used, and the
GC runs through each one and prunes any that haven't seen use within a
certain threshold (currently, 120 seconds). This should have the effect
of removing connection managers as they're not needed anymore, and thus
resolving the socket leakage seen in #850.

I had to make a couple implementation tweaks to get this working
correctly. Namely:

* The `StripeClient` class now tracks thread contexts instead of
  connection managers. This is so that when we're disposing of a
  connection manager, we can set `default_connection_manager` on its
  parent thread context to `nil` so that it's not still tracking a
  connection manager that we're trying to get rid of.

* `StripeClient` instances can still be instantiated as before, but no
  longer internalize a reference to their own connection manager,
  instead falling back to the one in the current thread context. The
  rationale is that when trying to dispose of a connection manager, we'd
  also have to dispose of its reference in any outstanding
  `StripeClient` instances that might still be tracking it, and that
  starts to get a little unwieldy. I've left `#connection_manager` in
  place for backwards compatibility, but marked it as deprecated.
2019-09-19 23:43:49 -07:00

167 lines
5.3 KiB
Ruby

# frozen_string_literal: true
require ::File.expand_path("../test_helper", __dir__)
module Stripe
class ConnectionManagerTest < Test::Unit::TestCase
setup do
@manager = Stripe::ConnectionManager.new
end
context "#initialize" do
should "set #last_used to current time" do
t = Time.new(2019)
Timecop.freeze(t) do
assert_equal t, Stripe::ConnectionManager.new.last_used
end
end
end
context "#clear" do
should "clear any active connections" do
stub_request(:post, "#{Stripe.api_base}/path")
.to_return(body: JSON.generate(object: "account"))
# Making a request lets us know that at least one connection is open.
@manager.execute_request(:post, "#{Stripe.api_base}/path")
# Now clear the manager.
@manager.clear
# This check isn't great, but it's otherwise difficult to tell that
# anything happened with just the public-facing API.
assert_equal({}, @manager.instance_variable_get(:@active_connections))
end
end
context "#connection_for" do
should "correctly initialize a connection" do
old_proxy = Stripe.proxy
old_open_timeout = Stripe.open_timeout
old_read_timeout = Stripe.read_timeout
begin
# Make sure any global initialization here is undone in the `ensure`
# block below.
Stripe.proxy = "http://user:pass@localhost:8080"
Stripe.open_timeout = 123
Stripe.read_timeout = 456
conn = @manager.connection_for("https://stripe.com")
# Host/port
assert_equal "stripe.com", conn.address
assert_equal 443, conn.port
# Proxy
assert_equal "localhost", conn.proxy_address
assert_equal 8080, conn.proxy_port
assert_equal "user", conn.proxy_user
assert_equal "pass", conn.proxy_pass
# Timeouts
assert_equal 123, conn.open_timeout
assert_equal 456, conn.read_timeout
assert_equal true, conn.use_ssl?
assert_equal OpenSSL::SSL::VERIFY_PEER, conn.verify_mode
assert_equal Stripe.ca_store, conn.cert_store
ensure
Stripe.proxy = old_proxy
Stripe.open_timeout = old_open_timeout
Stripe.read_timeout = old_read_timeout
end
end
should "produce the same connection multiple times" do
conn1 = @manager.connection_for("https://stripe.com")
conn2 = @manager.connection_for("https://stripe.com")
assert_equal conn1, conn2
end
should "produce different connections for different hosts" do
conn1 = @manager.connection_for("https://example.com")
conn2 = @manager.connection_for("https://stripe.com")
refute_equal conn1, conn2
end
should "produce different connections for different ports" do
conn1 = @manager.connection_for("https://stripe.com:80")
conn2 = @manager.connection_for("https://stripe.com:443")
refute_equal conn1, conn2
end
end
context "#execute_request" do
should "make a request" do
stub_request(:post, "#{Stripe.api_base}/path?query=bar")
.with(
body: "body=foo",
headers: { "Stripe-Account" => "bar" }
)
.to_return(body: JSON.generate(object: "account"))
@manager.execute_request(:post, "#{Stripe.api_base}/path",
body: "body=foo",
headers: { "Stripe-Account" => "bar" },
query: "query=bar")
end
should "perform basic argument validation" do
e = assert_raises ArgumentError do
@manager.execute_request("POST", "#{Stripe.api_base}/path")
end
assert_equal e.message, "method should be a symbol"
e = assert_raises ArgumentError do
@manager.execute_request(:post, :uri)
end
assert_equal e.message, "uri should be a string"
e = assert_raises ArgumentError do
@manager.execute_request(:post, "#{Stripe.api_base}/path",
body: {})
end
assert_equal e.message, "body should be a string"
e = assert_raises ArgumentError do
@manager.execute_request(:post, "#{Stripe.api_base}/path",
headers: "foo")
end
assert_equal e.message, "headers should be a hash"
e = assert_raises ArgumentError do
@manager.execute_request(:post, "#{Stripe.api_base}/path",
query: {})
end
assert_equal e.message, "query should be a string"
end
should "set #last_used to current time" do
stub_request(:post, "#{Stripe.api_base}/path")
.to_return(body: JSON.generate(object: "account"))
t = Time.new(2019)
# Make sure the connection manager is initialized at a different time
# than the one we're going to measure at because `#last_used` is also
# set by the constructor.
manager = Timecop.freeze(t) do
Stripe::ConnectionManager.new
end
Timecop.freeze(t + 1) do
manager.execute_request(:post, "#{Stripe.api_base}/path")
assert_equal t + 1, manager.last_used
end
end
end
end
end