Class: PGI::DB

Inherits:
Object
  • Object
show all
Defined in:
lib/pgi/db.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pool, logger, max_retries: 30, retry_wait: 2) ⇒ DB

Create instance

Parameters:

  • pool (ConnectionPool)
  • logger (Logger)
  • max_retries (Integer, Float) (defaults to: 30)

    shared retry budget for lost connections and pool checkout timeouts - use Float::INFINITY to ride out arbitrarily long outages

  • retry_wait (Numeric) (defaults to: 2)

    seconds to sleep between reconnection attempts



16
17
18
19
20
21
# File 'lib/pgi/db.rb', line 16

def initialize(pool, logger, max_retries: 30, retry_wait: 2)
  @pool        = pool
  @logger      = logger
  @max_retries = max_retries
  @retry_wait  = retry_wait
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name) ⇒ Object

Pass the remainder of methods on to a PGI::Connection



95
96
97
98
99
# File 'lib/pgi/db.rb', line 95

def method_missing(name, ...)
  with do |conn|
    conn.__send__(name, ...)
  end
end

Instance Attribute Details

#poolObject (readonly)

Returns the value of attribute pool.



7
8
9
# File 'lib/pgi/db.rb', line 7

def pool
  @pool
end

Class Method Details

.configure {|@options| ... } ⇒ Object

Yields:

  • (@options)


23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/pgi/db.rb', line 23

def self.configure
  @options = Struct.new(
    :pool_size, :pool_timeout, :pg_conn_uri, :logger, :max_retries, :retry_wait
  ).new

  yield @options

  pool = ConnectionPool.new(size: @options.pool_size, timeout: @options.pool_timeout) do
    Connection.new(conn_uri: @options.pg_conn_uri, logger: @options.logger)
  end

  retry_options = { max_retries: @options.max_retries, retry_wait: @options.retry_wait }.compact
  new(pool, @options.logger, **retry_options)
end

Instance Method Details

#exec(sql) ⇒ Object



86
87
88
89
90
# File 'lib/pgi/db.rb', line 86

def exec(sql)
  with do |conn|
    conn.exec(sql)
  end
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
# File 'lib/pgi/db.rb', line 101

def respond_to_missing?(name, include_private = false)
  PG::Connection.method_defined?(name) || super
end

#transaction { ... } ⇒ Object

wrapper around ConnectionPool#with with auto-healing capabilities

Yields:

  • PGI:Connection



41
42
43
44
45
46
47
48
49
# File 'lib/pgi/db.rb', line 41

def transaction
  raise "Missing block" unless block_given?

  with do |conn|
    conn.transaction do |trans_conn|
      yield Connection.new(conn: trans_conn, logger: @logger)
    end
  end
end

#with { ... } ⇒ Object

wrapper around ConnectionPool#with with auto-healing capabilities

Lost connections and pool checkout timeouts share the max_retries budget, so a genuinely stuck pool fails loudly instead of waiting forever.

Yields:

  • PGI:Connection



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
# File 'lib/pgi/db.rb', line 57

def with
  raise "Missing block" unless block_given?

  retries = 0
  begin
    @pool.with do |conn| # rubocop:disable Style/ExplicitBlockArgument
      yield conn
    end
  rescue PG::ConnectionBad, PG::UnableToSend => e
    retries += 1
    if retries > @max_retries
      @logger&.error("DB connection was lost - unable to reconnect (#{e.class}: #{e.message})")
      raise
    end
    @logger&.warn("DB connection was lost - reconnecting(#{retries}/#{@max_retries}) and retrying (#{e.class}: #{e.message})")
    @pool.reload(&:close)
    sleep @retry_wait
    retry
  rescue ConnectionPool::TimeoutError => e
    retries += 1
    if retries > @max_retries
      @logger&.error("Timeout in checking out DB connection from pool - giving up (#{e.message})")
      raise
    end
    @logger&.warn("Timeout in checking out DB connection from pool - retrying(#{retries}/#{@max_retries}) (#{e.message})")
    retry
  end
end