Class: Pgtk::Impatient
- Inherits:
-
Object
- Object
- Pgtk::Impatient
- Defined in:
- lib/pgtk/impatient.rb,
lib/pgtk/impatient/too_slow.rb
Overview
Impatient is a decorator for Pool that enforces timeouts on all database operations. It ensures that SQL queries don't run indefinitely, which helps prevent application hangs and resource exhaustion when database operations are slow or stalled.
This class implements the same interface as Pool. The timeout itself is
enforced by PostgreSQL, through the statement_timeout that the wire puts
into the startup packet of every connection. The limit is in force from the
first byte of the first statement, which no in-band SET can achieve: the
SET itself, and the START TRANSACTION ahead of it, would travel with no
limit at all. Because the setting arrives in the startup packet, it also
survives a transaction-pooling PgBouncer, and it frees the server-side
connection slot even when the client cannot deliver a cancellation request.
When PostgreSQL terminates a query at the deadline, TooSlow is raised here.
Queries that match one of the off regular expressions are excluded from
this checking. They run on a single connection, guarded by a session-level
SET statement_timeout with the default fallback timeout, which is reset
afterwards. Pass default: 0 to run excluded queries with no timeout at all.
This is how statements that take much longer than a query, such as VACUUM
or REINDEX, stay possible.
Basic usage:
# Create a pool of connections that carry a two-second limit
pool = Pgtk::Pool.new(
Pgtk::Wire::Env.new('DATABASE_URL', options: '-c statement_timeout=2000'),
max: 4
)
pool.start!
# Wrap the pool in an impatient decorator with a 2-second timeout
impatient = Pgtk::Impatient.new(pool, 2)
# Execute queries with automatic timeout enforcement
begin
impatient.exec('SELECT * FROM large_table WHERE complex_condition')
rescue Pgtk::Impatient::TooSlow
puts "Query timed out after 2 seconds"
end
# Transactions also enforce timeouts on each query
begin
impatient.transaction do |t|
t.exec('UPDATE large_table SET processed = true')
t.exec('DELETE FROM queue WHERE processed = true')
end
rescue PG::QueryCanceled
puts "Transaction timed out"
end
# Combining with Spy for timeout monitoring
spy = Pgtk::Spy.new(impatient) do |sql, duration|
puts "Query completed in #{duration} seconds: #{sql}"
end
# Now queries are both timed and monitored
spy.exec('SELECT * FROM users')
- Author
Yegor Bugayenko (yegor256@gmail.com)
- Copyright
Copyright (c) 2019-2026 Yegor Bugayenko
- License
MIT
Defined Under Namespace
Classes: TooSlow
Instance Method Summary collapse
-
#dump ⇒ Object
Convert internal state into text.
-
#exec(query, *args) ⇒ Array
Execute a SQL query with a server-side timeout.
-
#initialize(pool, timeout, *off, default: 300) ⇒ Impatient
constructor
Constructor.
-
#start! ⇒ Object
Start a new connection pool with the given arguments.
-
#transaction {|Object| ... } ⇒ Object
Run a transaction with a timeout for each query and for idle time inside the transaction.
-
#version ⇒ String
Get the version of PostgreSQL server.
Constructor Details
#initialize(pool, timeout, *off, default: 300) ⇒ Impatient
Constructor.
80 81 82 83 84 85 |
# File 'lib/pgtk/impatient.rb', line 80 def initialize(pool, timeout, *off, default: 300) @pool = pool @timeout = timeout @off = off @default = default end |
Instance Method Details
#dump ⇒ Object
Convert internal state into text.
100 101 102 103 104 105 106 107 |
# File 'lib/pgtk/impatient.rb', line 100 def dump [ @pool.dump, '', "Pgtk::Impatient (timeout=#{@timeout}s, default=#{@default}s):", @off.map { |re| " #{re}" } ].join("\n") end |
#exec(query, *args) ⇒ Array
Execute a SQL query with a server-side timeout.
The query travels alone, in a single round trip, and the limit is the
statement_timeout that the connection already carries. When the deadline
fires, the underlying PG::QueryCanceled is translated to TooSlow.
Queries matching one of the off regular expressions are guarded instead
by a session-level SET statement_timeout (the default fallback, reset
afterwards), or by no timeout at all when default is zero. This keeps
statements that need much longer than a query, such as VACUUM or
REINDEX, working as expected.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/pgtk/impatient.rb', line 125 def exec(query, *args) sql = query.is_a?(Array) ? query.join(' ') : query if @off.any? { |re| re.match?(sql) } return @pool.session do |t| t.exec("SET statement_timeout = #{Integer(@default * 1000)}") t.exec(sql, *args).tap { t.exec('RESET statement_timeout') } end end start = Time.now begin @pool.exec(sql, *args) rescue PG::QueryCanceled raise( TooSlow, [ 'SQL query', ("with #{args.count} argument#{'s' if args.count > 1}" unless args.empty?), 'was terminated after', start.ago, 'of waiting:', sql.ellipsized(50).inspect ].compact.join(' ') ) end end |
#start! ⇒ Object
Start a new connection pool with the given arguments.
88 89 90 |
# File 'lib/pgtk/impatient.rb', line 88 def start! @pool.start! end |
#transaction {|Object| ... } ⇒ Object
Run a transaction with a timeout for each query and for idle time inside
the transaction. If the transaction stays in the INTRANS state (idle
inside transaction) for longer than the configured timeout, PostgreSQL
terminates the session, which frees locks and releases the connection
slot back to the pool.
158 159 160 161 162 163 164 165 |
# File 'lib/pgtk/impatient.rb', line 158 def transaction @pool.transaction do |t| ms = [Integer(@timeout * 1000), 1].max t.exec("SET LOCAL statement_timeout = #{ms}") t.exec("SET LOCAL idle_in_transaction_session_timeout = #{ms}") yield(t) end end |
#version ⇒ String
Get the version of PostgreSQL server.
95 96 97 |
# File 'lib/pgtk/impatient.rb', line 95 def version @pool.version end |