Class: Pgtk::Impatient

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(pool, timeout, *off, default: 300) ⇒ Impatient

Constructor.

Parameters:

  • pool (Pgtk::Pool)

    The pool to decorate

  • timeout (Integer)

    Timeout in seconds for each SQL query, the same one that the connections of the pool carry as statement_timeout

  • off (Array<Regex>)

    List of regex to exclude queries from checking

  • default (Integer) (defaults to: 300)

    Fallback timeout in seconds for excluded queries (0 = no timeout)



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

#dumpObject

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.

Parameters:

  • query (String, Array)

    The SQL query with params inside (possibly)

  • args (Array)

    List of arguments

Returns:

  • (Array)

    Result rows

Raises:

  • (TooSlow)

    If the query takes too long



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.

Yields:

  • (Object)

    Yields a transaction object that responds to exec

Returns:

  • (Object)

    Result of the block



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

#versionString

Get the version of PostgreSQL server.

Returns:

  • (String)

    Version of PostgreSQL server



95
96
97
# File 'lib/pgtk/impatient.rb', line 95

def version
  @pool.version
end