класс ActiveRecord::ConnectionAdapters::ConnectionPool
Пул соединений Active Record
Базовый класс пула соединений для управления подключениями Active Record к базе данных.
Введение
Пул соединений синхронизирует доступ потоков к ограниченному числу соединений с базой данных. Основная идея заключается в том, что каждый поток получает соединение с базой данных из пула, использует его, а затем возвращает в пул. ConnectionPool полностью потокобезопасен и гарантирует, что одно соединение не смогут одновременно использовать два потока, если соблюдается контракт ConnectionPool. Он также обрабатывает ситуации, когда потоков больше, чем соединений: если все соединения получены из пула, а поток всё равно пытается получить соединение, ConnectionPool будет ждать, пока какой-либо другой поток не вернёт соединение или пока не истечёт checkout_timeout.
Получение соединения из пула
Получить соединения из пула и использовать их можно несколькими способами:
-
Просто используйте ActiveRecord::Base.lease_connection. Когда работа с соединением (соединениями) завершена и его нужно вернуть в пул, вызовите ActiveRecord::Base.connection_handler.clear_active_connections!. Это стандартное поведение Active Record при использовании цикла обработки запросов Action Pack.
-
Вручную получите соединение из пула с помощью ActiveRecord::Base.connection_pool.checkout. Вы отвечаете за возврат этого соединения в пул после завершения работы: для этого вызовите ActiveRecord::Base.connection_pool.checkin(connection).
-
Используйте ActiveRecord::Base.connection_pool.with_connection(&block): этот метод получает соединение, передаёт его блоку в качестве единственного аргумента и возвращает в пул после завершения блока.
Соединения в пуле на самом деле являются объектами AbstractAdapter (или объектами, совместимыми с интерфейсом AbstractAdapter).
Пока поток получил соединение из пула одним из трёх описанных выше способов, именно это соединение будет автоматически использоваться для запросов ActiveRecord, выполняемых в этом потоке. Например, нет необходимости явно передавать полученное соединение моделям или запросам Rails.
Параметры
В конфигурацию подключения к базе данных можно добавить несколько параметров, связанных с пулом соединений:
-
checkout_timeout: количество секунд ожидания доступности соединения, прежде чем прекратить ожидание и выдать ошибку тайм-аута (по умолчанию — 5 секунд). -
idle_timeout: количество секунд, в течение которых неиспользуемое соединение будет оставаться в пуле, прежде чем автоматически отключиться (по умолчанию — 300 секунд). Установите значение 0, чтобы соединения сохранялись бесконечно. -
keepalive: интервал в секундах между проверками поддержания активности простаивающего соединения (по умолчанию — 600 секунд). -
max_age: время в секундах, в течение которого пул будет разрешать существование соединения, прежде чем пометить его для удаления при следующем возврате в пул (по умолчанию — Float::INFINITY). -
max_connections: максимальное количество соединений, которыми может управлять пул (по умолчанию — 5). Установите значениеnilили -1, чтобы разрешить неограниченное количество соединений. -
min_connections: минимальное количество соединений, которые пул будет открывать и поддерживать (по умолчанию — 0). -
pool_jitter: максимальный коэффициент уменьшения для интерваловmax_ageиkeepalive(по умолчанию — 0.2; диапазон — 0.0–1.0).
Константы
- WeakThreadKeyMap
Атрибуты
Открытые методы класса
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 231 def install_executor_hooks(executor = ActiveSupport::Executor) executor.register_hook(ExecutorHooks) end
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 251 def initialize(pool_config) super() @pool_config = pool_config @db_config = pool_config.db_config @role = pool_config.role @shard = pool_config.shard @checkout_timeout = db_config.checkout_timeout @idle_timeout = db_config.idle_timeout @max_connections = db_config.max_connections @min_connections = db_config.min_connections @max_age = db_config.max_age @keepalive = db_config.keepalive # This variable tracks the cache of threads mapped to reserved connections, with the # sole purpose of speeding up the +connection+ method. It is not the authoritative # registry of which thread owns which connection. Connection ownership is tracked by # the +connection.owner+ attr on each +connection+ instance. # The invariant works like this: if there is mapping of <tt>thread => conn</tt>, # then that +thread+ does indeed own that +conn+. However, an absence of such # mapping does not mean that the +thread+ doesn't own the said connection. In # that case +conn.owner+ attr should be consulted. # Access and modification of <tt>@leases</tt> does not require # synchronization. @leases = LeaseRegistry.new @connections = [] @automatic_reconnect = true # Connection pool allows for concurrent (outside the main +synchronize+ section) # establishment of new connections. This variable tracks the number of threads # currently in the process of independently establishing connections to the DB. @now_connecting = 0 # Sometimes otherwise-idle connections are temporarily held by the Reaper for # maintenance. This variable tracks the number of connections currently in that # state -- if a thread requests a connection and there are none available, it # will await any in-maintenance connections in preference to creating a new one. @maintaining = 0 @threads_blocking_new_connections = 0 @available = ConnectionLeasingQueue.new self @pinned_connection = nil @pinned_connections_depth = 0 @async_executor = build_async_executor @schema_cache = nil @activated = false @original_context = ActiveSupport::IsolatedExecutionState.context @reaper_lock = Monitor.new @reaper = Reaper.new(self, db_config.reaping_frequency) @reaper.run end
Создаёт новый объект ConnectionPool. pool_config — это объект PoolConfig, содержащий сведения о подключении к базе данных (например, адаптер, имя хоста, имя пользователя, пароль и т. д.), а также максимальный размер этого ConnectionPool.
Максимальный размер ConnectionPool по умолчанию равен 5.
Открытые методы экземпляра
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 342 def activate @activated = true end
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 346 def activated? @activated end
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 419 def active_connection? connection_lease.connection end
Возвращает true, если для текущего потока используется открытое соединение.
Этот метод работает только с соединениями, полученными с помощью методов lease_connection или with_connection. Соединения, полученные с помощью checkout, не будут обнаружены методом active_connection?
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 658
def checkin(conn)
return if @pinned_connection.equal?(conn)
conn.lock.synchronize do
synchronize do
connection_lease.clear(conn)
conn.expire
@available.add conn
end
end
end Возвращает соединение с базой данных в пул, сообщая, что оно больше не требуется.
conn: объект AbstractAdapter, ранее полученный вызовом checkout для этого пула.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 630
def checkout(checkout_timeout = @checkout_timeout)
return checkout_and_verify(acquire_connection(checkout_timeout)) unless @pinned_connection
@pinned_connection.lock.synchronize do
synchronize do
# The pinned connection may have been cleaned up before we synchronized, so check if it is still present
if @pinned_connection
@pinned_connection.verify!
# Any leased connection must be in @connections otherwise
# some methods like #connected? won't behave correctly
unless @connections.include?(@pinned_connection)
@connections << @pinned_connection
end
@pinned_connection
else
checkout_and_verify(acquire_connection(checkout_timeout))
end
end
end
end Получает соединение с базой данных из пула, сообщая, что вы хотите его использовать. Когда соединение больше не требуется, вызовите checkin.
Метод либо возвращает и выделяет существующее соединение, либо создаёт новое и выделяет его.
Если все соединения выделены и пул достиг предельного размера (то есть количество выделенных в данный момент соединений больше или равно установленному лимиту), будет вызвано исключение ActiveRecord::ConnectionTimeoutError.
Возвращает: объект AbstractAdapter.
Вызывает исключение:
-
ActiveRecord::ConnectionTimeoutError, если из пула нельзя получить соединение.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 588
def clear_reloadable_connections(raise_on_acquisition_timeout = true)
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@connections.each do |conn|
if conn.in_use?
conn.steal!
checkin conn
end
conn.disconnect! if conn.requires_reloading?
end
@connections.delete_if(&:requires_reloading?)
@available.clear
end
end
end Удаляет из пула перезагружаемые соединения и заново подключает соединения, требующие перезагрузки.
Вызывает исключение:
-
ActiveRecord::ExclusiveConnectionTimeoutError, если не удалось получить владение всеми соединениями в пуле в течение интервала ожидания (по умолчанию —spec.db_config.checkout_timeout * 2секунд).
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 612 def clear_reloadable_connections! clear_reloadable_connections(false) end
Удаляет из пула перезагружаемые соединения и заново подключает соединения, требующие перезагрузки.
Сначала пул пытается получить владение всеми соединениями. Если это не удаётся сделать в течение интервала ожидания (по умолчанию — spec.db_config.checkout_timeout * 2 секунд), пул принудительно очищает кэш и перезагружает соединения, не учитывая потоки, владеющие другими соединениями.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 490
def connected?
synchronize { @connections.any?(&:connected?) }
end Возвращает true, если соединение уже открыто.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 505
def connections
synchronize { @connections.dup }
end Возвращает массив соединений, находящихся в данный момент в пуле. Для доступа к массиву не требуется синхронизация с пулом, поскольку массив создаётся заново и не сохраняется пулом.
Однако этот метод обходит потокобезопасный механизм доступа к соединениям ConnectionPool. Возвращённым соединением может владеть другой поток, оно может не принадлежать ни одному потоку или случайно принадлежать вызывающему потоку.
Вызов методов соединения без владения им зависит от гарантий потокобезопасности базового метода. Многие методы классов адаптеров соединений по своей природе небезопасны для многопоточного использования.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 515
def disconnect(raise_on_acquisition_timeout = true)
@reaper_lock.synchronize do
return if self.discarded?
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
return if self.discarded?
@connections.each do |conn|
if conn.in_use?
conn.steal!
checkin conn
end
conn.disconnect!
end
@connections = []
@leases.clear
@available.clear
# Stop maintaining the minimum size until reactivated
@activated = false
end
end
end
end Отключает все соединения в пуле и очищает пул.
Вызывает исключение:
-
ActiveRecord::ExclusiveConnectionTimeoutError, если не удалось получить владение всеми соединениями в пуле в течение интервала ожидания (по умолчанию —spec.db_config.checkout_timeout * 2секунд).
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 546 def disconnect! disconnect(false) end
Отключает все соединения в пуле и очищает пул.
Сначала пул пытается получить владение всеми соединениями. Если это не удаётся сделать в течение интервала ожидания (по умолчанию — spec.db_config.checkout_timeout * 2 секунд), пул принудительно отключает соединения, не учитывая потоки, владеющие другими соединениями.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 727
def flush(minimum_idle = @idle_timeout)
return if minimum_idle.nil?
removed_connections = synchronize do
return if self.discarded?
idle_connections = @connections.select do |conn|
!conn.in_use? && conn.seconds_idle >= minimum_idle
end.sort_by { |conn| -conn.seconds_idle } # sort longest idle first
# Don't go below our configured pool minimum unless we're flushing
# everything
idles_to_retain =
if minimum_idle > 0
@min_connections - (@connections.size - idle_connections.size)
else
0
end
if idles_to_retain > 0
idle_connections.pop idles_to_retain
end
idle_connections.each do |conn|
conn.lease
@available.delete conn
@connections.delete conn
end
end
removed_connections.each do |conn|
conn.disconnect!
end
end Отключает все соединения, простаивающие не менее minimum_idle секунд. Это не затрагивает соединения, полученные из пула в данный момент или возвращённые в пул менее minimum_idle секунд назад.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 766 def flush! reap flush(-1) # Stop maintaining the minimum size until reactivated @activated = false end
Отключает все простаивающие соединения. Соединения, полученные из пула в данный момент, не затрагиваются. Пул перестанет поддерживать минимальный размер, пока не будет активирован повторно (например, при последующем получении соединения).
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 825
def keep_alive(threshold = @keepalive)
return if threshold.nil?
sequential_maintenance -> c { (c.seconds_since_last_activity || 0) > c.pool_jitter(threshold) } do |conn|
# conn.active? will cause some amount of network activity, which is all
# we need to provide a keepalive signal.
#
# If it returns false, the connection is already broken; disconnect,
# so it can be found and repaired.
conn.disconnect! unless conn.active?
end
end Проверяет соединения, простаивающие дольше настроенного интервала поддержания активности. Заодно это подтверждает, что соединение всё ещё работает, но главная цель — сообщить серверу (и всем промежуточным узлам сети), что соединение по-прежнему используется.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 355 def lease_connection lease = connection_lease lease.connection ||= checkout lease.sticky = true lease.connection end
Получает соединение, связанное с текущим потоком, или при необходимости вызывает checkout, чтобы получить соединение.
lease_connection можно вызывать сколько угодно раз; соединение хранится в кэше, ключом которого служит поток.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 891
def pool_transaction_isolation_level
isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}"
ActiveSupport::IsolatedExecutionState[isolation_level_key]
end # File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 896
def pool_transaction_isolation_level=(isolation_level)
isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}"
ActiveSupport::IsolatedExecutionState[isolation_level_key] = isolation_level
end # File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 810
def preconnect
sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
conn.connect!
rescue
# Wholesale rescue: there's nothing we can do but move on. The
# connection will go back to the pool, and the next consumer will
# presumably try to connect again -- which will either work, or
# fail and they'll be able to report the exception.
end
end Заранее устанавливает соединения для всего пула. Это избавляет пользователей пула от ожидания установки соединения при его первом использовании после получения из пула.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 776
def prepopulate
need_new_connections = nil
synchronize do
return if self.discarded?
# We don't want to start prepopulating until we know the pool is wanted,
# so we can avoid maintaining full pools in one-off scripts etc.
return unless @activated
need_new_connections = @connections.size < @min_connections
end
if need_new_connections
while new_conn = try_to_checkout_new_connection { @connections.size < @min_connections }
new_conn.allow_preconnect = true
checkin(new_conn)
end
end
end Гарантирует, что пул содержит как минимум настроенное минимальное количество соединений.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 704
def reap
stale_connections = synchronize do
return if self.discarded?
@connections.select do |conn|
conn.in_use? && !conn.owner.alive?
end.each do |conn|
conn.steal!
end
end
stale_connections.each do |conn|
if conn.active?
conn.reset!
checkin conn
else
remove conn
end
end
end Восстанавливает потерянные соединения пула. Соединение может быть потеряно, если программист забыл вернуть его в пул по завершении потока или если поток неожиданно завершился.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 841
def recycle!
synchronize do
return if self.discarded?
@connections.each do |conn|
conn.force_retirement
end
end
retire_old_connections
end Немедленно помечает все текущие соединения как подлежащие замене, как если бы они достигли значения max_age, даже если max_age не настроен.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 431
def release_connection(existing_lease = nil)
return if self.discarded?
if conn = connection_lease.release
checkin conn
return true
end
false
end Сообщает, что поток завершил работу с текущим соединением. release_connection снимает связь между соединением и потоком и возвращает соединение в пул.
Этот метод работает только с соединениями, полученными с помощью методов lease_connection или with_connection. Соединения, полученные с помощью checkout, автоматически возвращены не будут.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 672
def remove(conn)
needs_new_connection = false
synchronize do
remove_connection_from_thread_cache conn
@connections.delete conn
@available.delete conn
# @available.any_waiting? => true means that prior to removing this
# conn, the pool was at its max size (@connections.size == @max_connections).
# This would mean that any threads stuck waiting in the queue wouldn't
# know they could checkout_new_connection, so let's do it for them.
# Because condition-wait loop is encapsulated in the Queue class
# (that in turn is oblivious to ConnectionPool implementation), threads
# that are "stuck" there are helpless. They have no way of creating
# new connections and are completely reliant on us feeding available
# connections into the Queue.
needs_new_connection = @available.num_waiting > @maintaining
end
# This is intentionally done outside of the synchronized section as we
# would like not to hold the main mutex while checking out new connections.
# Thus there is some chance that needs_new_connection information is now
# stale, we can live with that (bulk_make_new_connections will make
# sure not to exceed the pool's @max_connections limit).
bulk_make_new_connections(1) if needs_new_connection
end Удаляет соединение из пула. Соединение останется открытым и активным, но пул больше не будет им управлять.
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 797
def retire_old_connections(max_age = @max_age)
max_age ||= Float::INFINITY
sequential_maintenance -> c { c.connection_age&.>= c.pool_jitter(max_age) } do |conn|
# Disconnect, then return the adapter to the pool. Preconnect will
# handle the rest.
conn.disconnect!
end
end # File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 317 def schema_cache @schema_cache ||= BoundSchemaReflection.new(schema_reflection, self) end
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 321 def schema_reflection=(schema_reflection) pool_config.schema_reflection = schema_reflection @schema_cache = nil end
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 864
def stat
synchronize do
{
size: size,
connections: @connections.size,
busy: @connections.count { |c| c.in_use? && c.owner.alive? },
dead: @connections.count { |c| c.in_use? && !c.owner.alive? },
idle: @connections.count { |c| !c.in_use? },
waiting: num_waiting_in_queue,
checkout_timeout: checkout_timeout
}
end
end Возвращает статистику использования пула соединений.
ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }
# File activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb, line 450
def with_connection(prevent_permanent_checkout: false)
lease = connection_lease
sticky_was = lease.sticky
lease.sticky = false if prevent_permanent_checkout
if lease.connection
begin
yield lease.connection
ensure
lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
end
else
begin
yield lease.connection = checkout
ensure
lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
release_connection(lease) unless lease.sticky
end
end
end Передаёт блоку соединение из пула. Если текущий поток ещё не получил соединение из пула, метод получает соединение, передаёт его блоку, а затем возвращает в пул по завершении блока. Если соединение уже было получено в текущем потоке, например с помощью lease_connection или with_connection, блоку будет передано существующее соединение, и по завершении блока оно не будет автоматически возвращено в пул; предполагается, что код, получивший это соединение, корректно вернёт его в пул.
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.