Allow DB::Pool to be used a generic connection pool (#131)

* Allow DB::Pool to be a generic connection pool

* Use fully qualified class name for consistency

Co-authored-by: Brian J. Cardiff <bcardiff@gmail.com>

* Wrap only the necessary code in an `ensure`

* Add spec for http client pool

* Fix ICE in crystal-sqlite3

Co-authored-by: Brian J. Cardiff <bcardiff@gmail.com>
This commit is contained in:
Jamie Gaskins 2020-09-14 09:49:00 -04:00 committed by GitHub
parent ed686ad301
commit 291b65b853
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 15 deletions

View file

@ -0,0 +1,52 @@
require "./spec_helper"
require "./support/http"
describe DB::Pool do
it "distributes evenly the requests" do
mutex = Mutex.new
requests_per_connection = Hash(Socket::Address, Int32).new
server = HTTP::Server.new do |context|
remote_address = context.request.remote_address.not_nil!
mutex.synchronize do
requests_per_connection[remote_address] ||= 0
requests_per_connection[remote_address] += 1
end
sleep context.request.query_params["delay"].to_f
context.response.print "ok"
end
address = server.bind_unused_port "127.0.0.1"
run_server(server) do
fixed_pool_size = 5
expected_per_connection = 5
requests = fixed_pool_size * expected_per_connection
pool = DB::Pool.new(
initial_pool_size: fixed_pool_size,
max_pool_size: fixed_pool_size,
max_idle_pool_size: fixed_pool_size) {
HTTP::Client.new(URI.parse("http://127.0.0.1:#{address.port}/"))
}
done = Channel(Nil).new
requests.times do
spawn do
pool.checkout do |http|
http.get("/?delay=0.1")
end
done.send(nil)
end
end
spawn do
requests.times { done.receive }
done.close
end
wait_for { done.closed? }
requests_per_connection.values.should eq([expected_per_connection] * fixed_pool_size)
end
end
end

16
spec/support/fibers.cr Normal file
View file

@ -0,0 +1,16 @@
def wait_until_blocked(f : Fiber, timeout = 5.seconds)
now = Time.monotonic
until f.resumable?
Fiber.yield
raise "fiber failed to block within #{timeout}" if (Time.monotonic - now) > timeout
end
end
def wait_until_finished(f : Fiber, timeout = 5.seconds)
now = Time.monotonic
until f.dead?
Fiber.yield
raise "fiber failed to finish within #{timeout}" if (Time.monotonic - now) > timeout
end
end

48
spec/support/http.cr Normal file
View file

@ -0,0 +1,48 @@
require "http"
require "./fibers"
def wait_for(timeout = 5.seconds)
now = Time.monotonic
until yield
Fiber.yield
if (Time.monotonic - now) > timeout
raise "block failed to evaluate to true within #{timeout}"
end
end
end
# Helper method which runs *server*
# 1. Spawns `server.listen` in a new fiber.
# 2. Waits until `server.listening?`.
# 3. Yields to the given block.
# 4. Ensures the server is closed.
# 5. After returning from the block, it waits for the server to gracefully
# shut down before continuing execution in the current fiber.
# 6. If the listening fiber raises an exception, it is rescued and re-raised
# in the current fiber.
def run_server(server)
server_done = Channel(Exception?).new
f = spawn do
server.listen
rescue exc
server_done.send exc
else
server_done.send nil
end
begin
wait_for { server.listening? }
wait_until_blocked f
yield server_done
ensure
server.close unless server.closed?
if exc = server_done.receive
raise exc
end
end
end