PostgreSQL database programming with Ruby and pgx
a2cucGd4bW9ja2JpdGVzLmNvbQ==Abstract
If you use Ruby with PostgreSQL you probably use the excellent
pg
gem.
This article documents
pgx
, a pure ruby “thin” extension of
pg
that provides logging, transaction retrying, and database reconnecting
functionality.
Summary
pgx
injects additional higher level functionality into the
pg
API without changing its method signatures, except for new()
.
Specifically it adds the following:
- Logging
- Retrying
- Reconnecting
These features are only available for the synchronous pg methods.
Installation
Install pgx
with:
gem install pgx
Logging
Upon connecting you can pass a ruby logger object, or a lambda returning the ruby logger object. You can also pass an array of logger objects in case you want to log to multiple logs (eg: stdout and a log file.)
To log everything to a file you can do:
log = Logger.new('/tmp/debug.log') log.level = Logger::DEBUG con = PGx::Connection.new(logger: log, dbname: 'test')
You can also use a lambda to return the logger object, which makes the logging of the log object itself easier on the eyes:
log_l = lambda { log = Logger.new('/tmp/debug.log') log.level = Logger::DEBUG return log } con = PGx::Connection.new(logger: log_l, dbname: 'test')
To log everything to a file, and warnings plus errors to stdout you can do:
log_l = lambda { log_d = Logger.new('/tmp/debug.log') log_d.level = Logger::DEBUG log_w = Logger.new($stdout) log_w.level = Logger::WARN return [log_d, log_w] } con = PGx::Connection.new(logger: log_l, dbname: 'test')
Retrying
Any transaction that fails because of a “retriable” error is
automatically retried. You can define which errors are “retriable”
upon connecting by
passing method_retriable_error_states
with an array of retriable
error states.
The maximum number of retries is controlled by the method_try_count_max
parameter.
Example:
con = PGx::Connection.new( method_try_count_max: 3, -- retry deadlocks and serialization failures method_retriable_error_states: ['40P01', '40001'], host: 'somehost', dbname: 'test' )
Connecting and Re-connecting
If a transaction fails because of a bad database connection,
pgx
goes into a loop trying to re-establish the connection, and
when it does successfully reconnect, it retries the failed transaction.
There are two arguments related to establishing and re-establishing database connections:
connect_init
and
connect_retry_sleep
.
connect_init
can be passed a lambda that contains code that should
be executed immediately after establishing (or re-establishing) a connection
to the database. The lambda is called with one argument, the database connection object.
This can be useful for setting the search_path
.
connect_retry_sleep
can be passed a lambda, an integer, or nil.
If it's passed a lambda, the lambda is called with one argument (try counter)
which starts at 1 and increments by one with every unccesseful connection attempt.
The lambda should implement an algorithm for sleeping between connection
attempts. If it's passed an integer, it indicates how long to sleep
between connection attempts, in seconds. If a nil is passed, no attempt
to reconnect is made and an exception is raised.
Here is a complete program that illustrates all of the above:
#! /usr/bin/ruby require 'pgx' con = PGx::Connection.new( logger: lambda { log = Logger.new($stdout); log.level = Logger::DEBUG; return log; }, method_try_count_max: 3, method_retriable_error_states: ['40P01', '40001'], connect_init: lambda { |con| con.exec %q{ select set_search_path('mydb', 'development') } }, connect_retry_sleep: lambda { |try_count| sleep([try_count ** 2, 120].min) }, host: 'somehost', port: 5432, dbname: 'test', user: 'wizkid' ) def pid(con) return con.exec( 'select pg_backend_pid()' ).first['pg_backend_pid'] end pid = pid(con) con.prepare 'sql1', 'select ($1)::int + ($1)::int as x' con.prepare 'sql2', 'select ($1)::int * ($1)::int as x' con.transaction do # simulate losing the database connection in the middle # of a transaction con.reset if pid == pid(con) # # Once the connection is reset, the prepared statements # from above are lost! Thus, after each exec_prepared # below, there is a failure which causes the prepared # statement to be automatically reprepared before retrying # the transaction; this causes the transaction to be # retried twice before it finally succeeds. # # should output 6 con.exec_prepared 'sql1', [3] { |rs| puts rs.first['x'] } # should output 9 con.exec_prepared 'sql2', [3] { |rs| puts rs.first['x'] } end
In the above example,
the first five arguments to PGx::Connection.new()
are pgx
specific,
the rest are
PG::Connection.new()
arguments. Note the connect_retry_sleep
lambda implements a function that sleeps
1, 2, 4, 8, ... seconds, with each retry sleeping double the amount until it reaches 120 seconds; from that
point on it tries to connect to the database every two minutes.
Examples
Below are examples that illustrate a few of the ways you can
iterate over result sets.
The example snippets assume a database connection has been established in
the variable con
.
Refer to
pg
(mostly the
PG::Connection
and
PG::Result classes)
for documentation on the methods that are used.
Display the contents of the products
table:
con.exec 'select * from products' do |rs| # iterate rows rs.values.each do |row| # iterate row columns row.each_with_index do |col, i| # print column name print rs.fname(i) + ': ' # print column value puts col.nil? ? 'NULL' : col end end end
Same as above, but uses the each_row()
method which is
slightly more efficient for large result sets:
con.exec 'select * from products' do |rs| # iterate result set rows rs.each_row do |row| # iterate row columns row.each_with_index do |col, i| # print column name print rs.fname(i) + ': ' # print column value puts col.nil? ? 'NULL' : col end end end
Same as above, but uses the []
operator on the result set
which returns a hash containing the row.
con.exec 'select * from products' do |rs| # iterate result set rows rs.cmd_tuples().times do |n| # iterate row columns rs[n].each do |key, value| # print column name print key + ': ' # print column value puts value.nil? ? 'NULL' : value end end end