Class: Pgtk::Impatient

Inherits:
Object
  • Object
show all
Defined in:
lib/pgtk/impatient.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 but enforces the timeout on the server side, by wrapping each query in a tiny transaction that issues SET LOCAL statement_timeout. PostgreSQL itself terminates the query at the deadline, which guarantees that the server-side connection slot is freed even when the client cannot deliver a cancellation request (for example, behind a transaction-pool PgBouncer that does not forward client disconnects to in-flight server queries). On timeout, TooSlow is raised.

Basic usage:

# Create and configure a regular pool
pool = Pgtk::Pool.new(wire, 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 © 2019-2026 Yegor Bugayenko

License

MIT

Defined Under Namespace

Classes: TooSlow

Instance Method Summary collapse

Constructor Details

#initialize(pool, timeout, *off) ⇒ Impatient

Constructor.

Parameters:

  • pool (Pgtk::Pool)

    The pool to decorate

  • timeout (Integer)

    Timeout in seconds for each SQL query

  • off (Array<Regex>)

    List of regex to exclude queries from checking



69
70
71
72
73
# File 'lib/pgtk/impatient.rb', line 69

def initialize(pool, timeout, *off)
  @pool = pool
  @timeout = timeout
  @off = off
end

Instance Method Details

#dumpObject

Convert internal state into text.



88
89
90
91
92
93
94
95
# File 'lib/pgtk/impatient.rb', line 88

def dump
  [
    @pool.dump,
    '',
    "Pgtk::Impatient (timeout=#{@timeout}s):",
    @off.map { |re| "  #{re}" }
  ].join("\n")
end

#exec(query, *args) ⇒ Array

Execute a SQL query with a server-side timeout.

The query is wrapped in a tiny transaction that issues SET LOCAL statement_timeout, so PostgreSQL itself terminates the query at the deadline. This guarantees the server-side connection slot is freed even when the client cannot deliver a cancellation request (for example, behind a transaction-pool PgBouncer). When the deadline fires, the underlying PG::QueryCanceled is translated to TooSlow.

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



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/pgtk/impatient.rb', line 110

def exec(query, *args)
  sql = query.is_a?(Array) ? query.join(' ') : query
  return @pool.exec(sql, *args) if @off.any? { |re| re.match?(sql) }
  start = Time.now
  ms = [Integer(@timeout * 1000), 1].max
  begin
    @pool.transaction do |t|
      t.exec("SET LOCAL statement_timeout = #{ms}")
      t.exec(sql, *args)
    end
  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.



76
77
78
# File 'lib/pgtk/impatient.rb', line 76

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



140
141
142
143
144
145
146
147
# File 'lib/pgtk/impatient.rb', line 140

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



83
84
85
# File 'lib/pgtk/impatient.rb', line 83

def version
  @pool.version
end