2016-09-12 17:35:21 +00:00
|
|
|
require "weak_ref"
|
|
|
|
|
2020-09-14 13:49:00 +00:00
|
|
|
require "./error"
|
|
|
|
|
2016-07-05 18:21:39 +00:00
|
|
|
module DB
|
|
|
|
class Pool(T)
|
2023-06-23 01:03:08 +00:00
|
|
|
record Options,
|
|
|
|
# initial number of connections in the pool
|
|
|
|
initial_pool_size : Int32 = 1,
|
|
|
|
# maximum amount of connections in the pool (Idle + InUse). 0 means no maximum.
|
|
|
|
max_pool_size : Int32 = 0,
|
|
|
|
# maximum amount of idle connections in the pool
|
|
|
|
max_idle_pool_size : Int32 = 1,
|
|
|
|
# seconds to wait before timeout while doing a checkout
|
|
|
|
checkout_timeout : Float64 = 5.0,
|
|
|
|
# maximum amount of retry attempts to reconnect to the db. See `Pool#retry`
|
|
|
|
retry_attempts : Int32 = 1,
|
|
|
|
# seconds to wait before a retry attempt
|
|
|
|
retry_delay : Float64 = 0.2 do
|
|
|
|
def self.from_http_params(params : HTTP::Params, default = Options.new)
|
|
|
|
Options.new(
|
|
|
|
initial_pool_size: params.fetch("initial_pool_size", default.initial_pool_size).to_i,
|
|
|
|
max_pool_size: params.fetch("max_pool_size", default.max_pool_size).to_i,
|
|
|
|
max_idle_pool_size: params.fetch("max_idle_pool_size", default.max_idle_pool_size).to_i,
|
|
|
|
checkout_timeout: params.fetch("checkout_timeout", default.checkout_timeout).to_f,
|
|
|
|
retry_attempts: params.fetch("retry_attempts", default.retry_attempts).to_i,
|
|
|
|
retry_delay: params.fetch("retry_delay", default.retry_delay).to_f,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-08-20 19:20:58 +00:00
|
|
|
# Pool configuration
|
|
|
|
|
|
|
|
# initial number of connections in the pool
|
2016-07-05 18:21:39 +00:00
|
|
|
@initial_pool_size : Int32
|
2019-08-20 19:20:58 +00:00
|
|
|
# maximum amount of connections in the pool (Idle + InUse)
|
2016-07-05 18:21:39 +00:00
|
|
|
@max_pool_size : Int32
|
2019-08-20 19:20:58 +00:00
|
|
|
# maximum amount of idle connections in the pool
|
|
|
|
@max_idle_pool_size : Int32
|
|
|
|
# seconds to wait before timeout while doing a checkout
|
2016-07-05 18:21:39 +00:00
|
|
|
@checkout_timeout : Float64
|
2019-08-20 19:20:58 +00:00
|
|
|
# maximum amount of retry attempts to reconnect to the db. See `Pool#retry`
|
2016-08-31 20:32:01 +00:00
|
|
|
@retry_attempts : Int32
|
2019-08-20 19:20:58 +00:00
|
|
|
# seconds to wait before a retry attempt
|
2016-08-31 20:32:01 +00:00
|
|
|
@retry_delay : Float64
|
2016-07-05 18:21:39 +00:00
|
|
|
|
2019-08-20 19:20:58 +00:00
|
|
|
# Pool state
|
|
|
|
|
|
|
|
# total of open connections managed by this pool
|
|
|
|
@total = [] of T
|
|
|
|
# connections available for checkout
|
|
|
|
@idle = Set(T).new
|
|
|
|
# connections waiting to be stablished (they are not in *@idle* nor in *@total*)
|
|
|
|
@inflight : Int32
|
|
|
|
|
|
|
|
# Sync state
|
|
|
|
|
|
|
|
# communicate that a connection is available for checkout
|
|
|
|
@availability_channel : Channel(Nil)
|
|
|
|
# global pool mutex
|
|
|
|
@mutex : Mutex
|
|
|
|
|
2023-06-23 01:03:08 +00:00
|
|
|
@[Deprecated("Use `#new` with DB::Pool::Options instead")]
|
|
|
|
def initialize(initial_pool_size = 1, max_pool_size = 0, max_idle_pool_size = 1, checkout_timeout = 5.0,
|
|
|
|
retry_attempts = 1, retry_delay = 0.2, &factory : -> T)
|
|
|
|
initialize(
|
|
|
|
Options.new(
|
|
|
|
initial_pool_size: initial_pool_size, max_pool_size: max_pool_size,
|
|
|
|
max_idle_pool_size: max_idle_pool_size, checkout_timeout: checkout_timeout,
|
|
|
|
retry_attempts: retry_attempts, retry_delay: retry_delay),
|
|
|
|
&factory)
|
|
|
|
end
|
|
|
|
|
|
|
|
def initialize(pool_options : Options = Options.new, &@factory : -> T)
|
|
|
|
@initial_pool_size = pool_options.initial_pool_size
|
|
|
|
@max_pool_size = pool_options.max_pool_size
|
|
|
|
@max_idle_pool_size = pool_options.max_idle_pool_size
|
|
|
|
@checkout_timeout = pool_options.checkout_timeout
|
|
|
|
@retry_attempts = pool_options.retry_attempts
|
|
|
|
@retry_delay = pool_options.retry_delay
|
|
|
|
|
2016-07-05 18:21:39 +00:00
|
|
|
@availability_channel = Channel(Nil).new
|
2019-08-20 19:20:58 +00:00
|
|
|
@inflight = 0
|
2016-07-07 17:48:58 +00:00
|
|
|
@mutex = Mutex.new
|
2019-08-20 19:20:58 +00:00
|
|
|
|
|
|
|
@initial_pool_size.times { build_resource }
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# close all resources in the pool
|
|
|
|
def close : Nil
|
|
|
|
@total.each &.close
|
2016-08-31 17:45:32 +00:00
|
|
|
@total.clear
|
2019-08-20 19:20:58 +00:00
|
|
|
@idle.clear
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
|
2020-09-14 13:49:00 +00:00
|
|
|
record Stats,
|
|
|
|
open_connections : Int32,
|
|
|
|
idle_connections : Int32,
|
|
|
|
in_flight_connections : Int32,
|
|
|
|
max_connections : Int32
|
2019-08-20 19:23:15 +00:00
|
|
|
|
|
|
|
# Returns stats of the pool
|
|
|
|
def stats
|
|
|
|
Stats.new(
|
2020-09-14 13:49:00 +00:00
|
|
|
open_connections: @total.size,
|
|
|
|
idle_connections: @idle.size,
|
|
|
|
in_flight_connections: @inflight,
|
|
|
|
max_connections: @max_pool_size,
|
2019-08-20 19:23:15 +00:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2016-07-05 18:21:39 +00:00
|
|
|
def checkout : T
|
2019-08-20 19:20:58 +00:00
|
|
|
res = sync do
|
2019-09-02 17:14:46 +00:00
|
|
|
resource = nil
|
|
|
|
|
|
|
|
until resource
|
|
|
|
resource = if @idle.empty?
|
|
|
|
if can_increase_pool?
|
|
|
|
@inflight += 1
|
2023-07-10 13:55:35 +00:00
|
|
|
begin
|
|
|
|
r = unsync { build_resource }
|
|
|
|
ensure
|
|
|
|
@inflight -= 1
|
|
|
|
end
|
2019-09-02 17:14:46 +00:00
|
|
|
r
|
|
|
|
else
|
|
|
|
unsync { wait_for_available }
|
|
|
|
# The wait for available can unlock
|
|
|
|
# multiple fibers waiting for a resource.
|
|
|
|
# Although only one will pick it due to the lock
|
|
|
|
# in the end of the unsync, the pick_available
|
|
|
|
# will return nil
|
|
|
|
pick_available
|
|
|
|
end
|
2019-08-20 19:20:58 +00:00
|
|
|
else
|
|
|
|
pick_available
|
|
|
|
end
|
2019-09-02 17:14:46 +00:00
|
|
|
end
|
2016-07-05 18:21:39 +00:00
|
|
|
|
2019-08-20 19:20:58 +00:00
|
|
|
@idle.delete resource
|
|
|
|
|
|
|
|
resource
|
|
|
|
end
|
|
|
|
|
2020-09-14 13:49:00 +00:00
|
|
|
if res.responds_to?(:before_checkout)
|
|
|
|
res.before_checkout
|
|
|
|
end
|
2019-08-20 19:20:58 +00:00
|
|
|
res
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
|
2020-09-14 13:49:00 +00:00
|
|
|
def checkout(&block : T ->)
|
|
|
|
connection = checkout
|
|
|
|
|
|
|
|
begin
|
|
|
|
yield connection
|
|
|
|
ensure
|
|
|
|
release connection
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-05 18:21:39 +00:00
|
|
|
def release(resource : T) : Nil
|
2019-08-20 19:20:58 +00:00
|
|
|
idle_pushed = false
|
|
|
|
|
|
|
|
sync do
|
2021-09-06 22:02:43 +00:00
|
|
|
if resource.responds_to?(:closed?) && resource.closed?
|
|
|
|
@total.delete(resource)
|
|
|
|
elsif can_increase_idle_pool
|
2019-08-20 19:20:58 +00:00
|
|
|
@idle << resource
|
2020-09-14 13:49:00 +00:00
|
|
|
if resource.responds_to?(:after_release)
|
|
|
|
resource.after_release
|
|
|
|
end
|
2019-08-20 19:20:58 +00:00
|
|
|
idle_pushed = true
|
|
|
|
else
|
|
|
|
resource.close
|
|
|
|
@total.delete(resource)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-07-31 14:04:18 +00:00
|
|
|
if idle_pushed
|
|
|
|
select
|
|
|
|
when @availability_channel.send(nil)
|
|
|
|
else
|
|
|
|
end
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-08-31 20:32:01 +00:00
|
|
|
# :nodoc:
|
|
|
|
# Will retry the block if a `ConnectionLost` exception is thrown.
|
|
|
|
# It will try to reuse all of the available connection right away,
|
|
|
|
# but if a new connection is needed there is a `retry_delay` seconds delay.
|
|
|
|
def retry
|
2019-08-20 19:20:58 +00:00
|
|
|
current_available = 0
|
|
|
|
|
|
|
|
sync do
|
|
|
|
current_available = @idle.size
|
|
|
|
# if the pool hasn't reach the max size, allow 1 attempt
|
|
|
|
# to make a new connection if needed without sleeping
|
|
|
|
current_available += 1 if can_increase_pool?
|
|
|
|
end
|
2016-08-31 20:32:01 +00:00
|
|
|
|
|
|
|
(current_available + @retry_attempts).times do |i|
|
|
|
|
begin
|
|
|
|
sleep @retry_delay if i >= current_available
|
|
|
|
return yield
|
2020-09-14 13:49:00 +00:00
|
|
|
rescue e : PoolResourceLost(T)
|
2021-09-10 11:36:01 +00:00
|
|
|
# if the connection is lost it will be closed by
|
|
|
|
# the exception to release resources
|
|
|
|
# we still need to remove it from the known pool.
|
2020-09-14 13:49:00 +00:00
|
|
|
sync { delete(e.resource) }
|
|
|
|
rescue e : PoolResourceRefused
|
2016-12-15 17:40:56 +00:00
|
|
|
# a ConnectionRefused means a new connection
|
2017-11-08 02:23:06 +00:00
|
|
|
# was intended to be created
|
2016-12-15 17:40:56 +00:00
|
|
|
# nothing to due but to retry soon
|
2016-08-31 20:32:01 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
raise PoolRetryAttemptsExceeded.new
|
|
|
|
end
|
|
|
|
|
2016-08-18 03:55:43 +00:00
|
|
|
# :nodoc:
|
|
|
|
def each_resource
|
2019-08-20 19:20:58 +00:00
|
|
|
sync do
|
|
|
|
@idle.each do |resource|
|
|
|
|
yield resource
|
|
|
|
end
|
2016-08-18 03:55:43 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-09-13 04:36:49 +00:00
|
|
|
# :nodoc:
|
2016-08-29 16:14:47 +00:00
|
|
|
def is_available?(resource : T)
|
2019-08-20 19:20:58 +00:00
|
|
|
@idle.includes?(resource)
|
2016-08-29 16:14:47 +00:00
|
|
|
end
|
|
|
|
|
2016-09-13 04:36:49 +00:00
|
|
|
# :nodoc:
|
|
|
|
def delete(resource : T)
|
|
|
|
@total.delete(resource)
|
2019-08-20 19:20:58 +00:00
|
|
|
@idle.delete(resource)
|
2016-09-13 04:36:49 +00:00
|
|
|
end
|
|
|
|
|
2016-07-05 18:21:39 +00:00
|
|
|
private def build_resource : T
|
|
|
|
resource = @factory.call
|
2023-04-24 10:26:25 +00:00
|
|
|
sync do
|
|
|
|
@total << resource
|
|
|
|
@idle << resource
|
|
|
|
end
|
2016-07-05 18:21:39 +00:00
|
|
|
resource
|
|
|
|
end
|
|
|
|
|
2019-08-20 19:20:58 +00:00
|
|
|
private def can_increase_pool?
|
|
|
|
@max_pool_size == 0 || @total.size + @inflight < @max_pool_size
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private def can_increase_idle_pool
|
2019-08-20 19:20:58 +00:00
|
|
|
@idle.size < @max_idle_pool_size
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private def pick_available
|
2019-09-02 17:14:46 +00:00
|
|
|
@idle.first?
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
|
2020-09-29 13:35:14 +00:00
|
|
|
private def wait_for_available
|
|
|
|
select
|
|
|
|
when @availability_channel.receive
|
|
|
|
when timeout(@checkout_timeout.seconds)
|
|
|
|
raise DB::PoolTimeout.new("Could not check out a connection in #{@checkout_timeout} seconds")
|
2020-03-25 13:56:03 +00:00
|
|
|
end
|
2020-09-29 13:35:14 +00:00
|
|
|
end
|
2016-07-07 17:48:58 +00:00
|
|
|
|
2019-08-20 19:20:58 +00:00
|
|
|
private def sync
|
|
|
|
@mutex.lock
|
|
|
|
begin
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
@mutex.unlock
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private def unsync
|
|
|
|
@mutex.unlock
|
|
|
|
begin
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
@mutex.lock
|
2016-07-07 17:48:58 +00:00
|
|
|
end
|
2016-07-05 18:21:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|