Class: Sqlx::Database::PgPool

Inherits:
Object
  • Object
show all
Defined in:
runtime/database/postgresql.rb

Overview

A lazy pool of libpq connections. Connections open on first checkout, not in configure: a prefork child must not inherit the parent's sockets, and the boot probe already proved the conninfo is reachable. The pool is the concurrency boundary — one connection is leased to one thread, so libpq's single-statement-at-a-time rule holds without a process-wide lock.

Pool state: @conns[i] is a live connection, or nil while position i waits for a replacement; @free holds positions whose connection is live and idle; @vacant holds positions whose connection is gone; @opening counts positions reserved for an open in progress. A dead lease token and the replacement at its position can be outstanding together — release identifies the connection, not only the position.

A checkout validates the leased connection (idle and lifetime bounds plus a SELECT 1 ping) before handing it to the caller; a failed check discards that connection and leases again.

Constant Summary collapse

CHECKOUT_TIMEOUT_SECONDS =

How long a checkout may wait for a free connection. A request that cannot get one fails loudly instead of parking behind a long transaction; the knob is checkout_timeout.

5.0
DEFAULT_IDLE_TIMEOUT_MS =

A connection idle longer than this is replaced at checkout rather than reused. idle_timeout disables it at 0. The bound is applied at checkout, not by a background reaper.

300000
DEFAULT_MAX_LIFETIME_MS =

A connection older than this is replaced at checkout. HikariCP calls the knob maxLifetime; 0 disables it.

1800000

Instance Method Summary collapse

Constructor Details

#initialize(conninfo, size) ⇒ PgPool

Returns a new instance of PgPool.



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'runtime/database/postgresql.rb', line 245

def initialize(conninfo, size)
  if size < 1
    raise ArgumentError, "Sqlx::Database: pool_size must be at least 1, got " + size.to_s
  end
  @conninfo = conninfo
  @size = size
  @timeout_seconds = CHECKOUT_TIMEOUT_SECONDS
  ev = ENV["DATABASE_POOL_TIMEOUT_MS"]
  if !ev.nil? && ev != ""
    @timeout_seconds = ev.to_f / 1000.0
  end
  @idle_timeout_seconds = env_seconds("DATABASE_IDLE_TIMEOUT_MS", DEFAULT_IDLE_TIMEOUT_MS)
  @max_lifetime_seconds = env_seconds("DATABASE_MAX_LIFETIME_MS", DEFAULT_MAX_LIFETIME_MS)
  @conns = []
  @free = []
  @vacant = []
  @opening = 0
  @lock = Mutex.new
  @cv = ConditionVariable.new
end

Instance Method Details

#checkout ⇒ LeaseToken

lease a connection, prove it usable, and answer the lease token. A failed check discards the connection and retries the checkout once; the retry is a checkout retry, not a statement retry.

Returns:

  • (LeaseToken) —

    the checked-out connection's token

Raises:



318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'runtime/database/postgresql.rb', line 318

def checkout
  attempts = 0
  while attempts < 2
    idx = lease
    conn = @conns[idx]
    now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    if !conn.nil? && conn.usable?(now, @idle_timeout_seconds, @max_lifetime_seconds)
      return LeaseToken.new(self, idx)
    end
    discard(conn) if !conn.nil?
    attempts += 1
  end
  raise Sqlx::ConnectionFailed, "Sqlx::Database: no healthy pooled PostgreSQL connection after " + attempts.to_s + " attempts"
end

#close_all ⇒ Object



367
368
369
370
371
372
373
374
375
376
# File 'runtime/database/postgresql.rb', line 367

def close_all
  i = 0
  while i < @conns.length
    conn = @conns[i]
    if !conn.nil?
      conn.close
    end
    i += 1
  end
end

#conn(idx) ⇒ Object



266
267
268
# File 'runtime/database/postgresql.rb', line 266

def conn(idx)
  @conns[idx]
end

#discard(conn) ⇒ Object

remove a failed connection. The position stays vacant and is refilled by a later checkout that finds no free connection; the current lease's release is a no-op because its connection no longer occupies the position. Closing happens outside the lock.



352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'runtime/database/postgresql.rb', line 352

def discard(conn)
  discarded = false
  @lock.synchronize do
    idx = conn.index
    if idx >= 0 && idx < @conns.length && @conns[idx] == conn
      @conns[idx] = nil
      @free.delete(idx)
      @vacant.push(idx)
      @cv.signal
      discarded = true
    end
  end
  conn.close if discarded
end

#first ⇒ Object

The fallback connection for single-fiber contexts before require_leases!: answer position 0, opening it on first use. The position stays in the free list, so a later lease can take it once the server requires leases (the fallback path is gone by then). A failed connection at position 0 is repaired here too — the fallback has no lease to ask for a replacement. The open runs outside @lock, like lease: no caller may wait behind DNS/TCP. Two fallback callers are outside the single-fiber contract, but the race is closed anyway — the loser closes its extra connection and answers the repaired one.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'runtime/database/postgresql.rb', line 279

def first
  existing = nil
  @lock.synchronize do
    if @conns.length > 0 && !@conns[0].nil?
      existing = @conns[0]
    end
  end
  return existing if !existing.nil?

  conn = PgConnection.new(@conninfo)
  extra = nil
  @lock.synchronize do
    if @conns.length == 0
      conn.attach_to_position(0)
      @conns.push(conn)
      @free.push(0)
      @cv.signal
    elsif @conns[0].nil?
      conn.attach_to_position(0)
      @conns[0] = conn
      @vacant.delete(0)
      @free.push(0) unless @free.include?(0)
      @cv.signal
    else
      extra = conn
      conn = @conns[0]
    end
  end
  extra.close if !extra.nil?
  conn
end

#release(idx, conn, finished) ⇒ Object

the shared lease token's release hook. finished is unused here: this adapter has no statement cache to trim. The position is only returned when it still holds the same connection; a connection discarded mid-statement has already left the pool, and its position waits for a later checkout that finds no free connection.



338
339
340
341
342
343
344
345
346
# File 'runtime/database/postgresql.rb', line 338

def release(idx, conn, finished)
  @lock.synchronize do
    if idx >= 0 && idx < @conns.length && @conns[idx] == conn
      conn.mark_used(Process.clock_gettime(Process::CLOCK_MONOTONIC))
      @free.push(idx)
      @cv.signal
    end
  end
end