1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
# frozen-string-literal: true
#
# The connection_validator extension modifies a database's
# connection pool to validate that connections checked out
# from the pool are still valid, before yielding them for
# use. If it detects an invalid connection, it removes it
# from the pool and tries the next available connection,
# creating a new connection if no available connection is
# valid. Example of use:
#
# DB.extension(:connection_validator)
#
# As checking connections for validity involves issuing a
# query, which is potentially an expensive operation,
# the validation checks are only run if the connection has
# been idle for longer than a certain threshold. By default,
# that threshold is 3600 seconds (1 hour), but it can be
# modified by the user, set to -1 to always validate
# connections on checkout:
#
# DB.pool.connection_validation_timeout = -1
#
# Note that if you set the timeout to validate connections
# on every checkout, you should probably manually control
# connection checkouts on a coarse basis, using
# Database#synchronize. In a web application, the optimal
# place for that would be a rack middleware. Validating
# connections on every checkout without setting up coarse
# connection checkouts will hurt performance, in some cases
# significantly. Note that setting up coarse connection
# checkouts reduces the concurrency level achievable. For
# example, in a web application, using Database#synchronize
# in a rack middleware will limit the number of concurrent
# web requests to the number to connections in the database
# connection pool.
#
# Note that this extension does not work with the single
# threaded and sharded single threaded connection pools.
# As the only reason to use the single threaded
# pools is for speed, and this extension makes the connection
# pool slower, there's not much point in modifying this
# extension to work with the single threaded pools. The
# non-single threaded pools work fine even in single threaded
# code, so if you are currently using a single threaded pool
# and want to use this extension, switch to using another
# pool.
#
# Related module: Sequel::ConnectionValidator
#
module Sequel
module ConnectionValidator
class Retry < Error; end
Sequel::Deprecation.deprecate_constant(self, :Retry)
# The number of seconds that need to pass since
# connection checkin before attempting to validate
# the connection when checking it out from the pool.
# Defaults to 3600 seconds (1 hour).
attr_accessor :connection_validation_timeout
# Initialize the data structures used by this extension.
def self.extended(pool)
case pool.pool_type
when :single, :sharded_single
raise Error, "cannot load connection_validator extension if using single or sharded_single connection pool"
end
pool.instance_exec do
sync do
@connection_timestamps ||= {}
@connection_validation_timeout ||= 3600
end
end
# Make sure the valid connection SQL query is precached,
# otherwise it's possible it will happen at runtime. While
# it should work correctly at runtime, it's better to avoid
# the possibility of failure altogether.
pool.db.send(:valid_connection_sql)
end
private
# Record the time the connection was checked back into the pool.
def checkin_connection(*)
conn = super
@connection_timestamps[conn] = Sequel.start_timer
conn
end
# Clean up timestamps during disconnect.
def disconnect_connection(conn)
sync{@connection_timestamps.delete(conn)}
super
end
# When acquiring a connection, if it has been
# idle for longer than the connection validation timeout,
# test the connection for validity. If it is not valid,
# disconnect the connection, and retry with a new connection.
def acquire(*a)
conn = nil
1.times do
if (conn = super) &&
(timer = sync{@connection_timestamps.delete(conn)}) &&
Sequel.elapsed_seconds_since(timer) > @connection_validation_timeout
begin
valid = db.valid_connection?(conn)
ensure
unless valid
case pool_type
when :sharded_threaded, :sharded_timed_queue
sync{@allocated[a.last].delete(Sequel.current)}
else
sync{@allocated.delete(Sequel.current)}
end
disconnect_connection(conn)
redo if valid == false
end
end
end
end
conn
end
end
Database.register_extension(:connection_validator){|db| db.pool.extend(ConnectionValidator)}
end
|