Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,375 @@
|
||||
module pg
|
||||
|
||||
import sync
|
||||
import time
|
||||
|
||||
// PoolStats reports the current state of a `DB`'s connection pool.
|
||||
pub struct PoolStats {
|
||||
pub:
|
||||
max_open_connections int // configured limit (0 = unlimited)
|
||||
open_connections int // total in-use + idle conns
|
||||
in_use int // conns currently checked out
|
||||
idle int // conns parked as idle
|
||||
wait_count int // number of callers currently blocked on acquire
|
||||
}
|
||||
|
||||
// PoolConfig configures pool behavior at construction time.
|
||||
@[params]
|
||||
pub struct PoolConfig {
|
||||
pub:
|
||||
max_open_conns int // 0 = unlimited
|
||||
max_idle_conns int = 2 // 0 = keep no idle conns
|
||||
conn_max_lifetime time.Duration // 0 = unlimited
|
||||
}
|
||||
|
||||
// IdleSlot is the pool's internal representation of a pooled libpq handle.
|
||||
// The pool tracks raw PGconn* metadata in idle/waiter channels; `acquire`
|
||||
// allocates a fresh `&Conn` wrapping the slot and `release` extracts the
|
||||
// metadata back. Keeping the wrapper separate from the slot means a stale
|
||||
// `&Conn` kept by user code after `close()` cannot be revived even when
|
||||
// the pool re-hands the same physical PGconn* to another caller.
|
||||
struct IdleSlot {
|
||||
handle voidptr
|
||||
created_at time.Time
|
||||
mut:
|
||||
bad bool
|
||||
}
|
||||
|
||||
@[heap]
|
||||
struct Pool {
|
||||
mut:
|
||||
mu &sync.Mutex = unsafe { nil }
|
||||
conninfo string
|
||||
max_open int
|
||||
max_idle int
|
||||
max_lifetime time.Duration
|
||||
idle []IdleSlot
|
||||
open_count int
|
||||
waiters []chan IdleSlot
|
||||
closed bool
|
||||
// last_ids stores the per-thread last-inserted id so DB.last_id() returns
|
||||
// the value captured on the same pooled conn that ran the INSERT, instead
|
||||
// of calling LASTVAL() on whatever conn the pool happens to hand out next
|
||||
// (which is session-scoped and would return 0 or a wrong value).
|
||||
last_ids_mu &sync.Mutex = unsafe { nil }
|
||||
last_ids map[u64]i64
|
||||
}
|
||||
|
||||
fn new_pool(conninfo string, cfg PoolConfig) &Pool {
|
||||
mut p := &Pool{
|
||||
mu: sync.new_mutex()
|
||||
last_ids_mu: sync.new_mutex()
|
||||
conninfo: conninfo
|
||||
max_open: cfg.max_open_conns
|
||||
max_idle: cfg.max_idle_conns
|
||||
max_lifetime: cfg.conn_max_lifetime
|
||||
}
|
||||
if p.max_open < 0 {
|
||||
p.max_open = 0
|
||||
}
|
||||
if p.max_idle < 0 {
|
||||
p.max_idle = 0
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// wrap_slot builds the fresh `&Conn` that gets handed to a caller. Each
|
||||
// acquire allocates a new wrapper so a previously-checked-out (and since
|
||||
// released) `&Conn` reference can never be reused to reach the underlying
|
||||
// PGconn*.
|
||||
fn (p &Pool) wrap_slot(slot IdleSlot) &Conn {
|
||||
return &Conn{
|
||||
conn: slot.handle
|
||||
pool: unsafe { p }
|
||||
created_at: slot.created_at
|
||||
bad: slot.bad
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut p Pool) acquire() !&Conn {
|
||||
for {
|
||||
p.mu.lock()
|
||||
if p.closed {
|
||||
p.mu.unlock()
|
||||
return error('pg: pool is closed')
|
||||
}
|
||||
// Pop newest idle slot (LIFO keeps the freshest connection warm)
|
||||
for p.idle.len > 0 {
|
||||
slot := p.idle.last()
|
||||
p.idle.delete_last()
|
||||
if slot_expired(slot, p.max_lifetime) || slot_bad(slot) {
|
||||
physical_close_handle(slot.handle)
|
||||
p.open_count--
|
||||
continue
|
||||
}
|
||||
p.mu.unlock()
|
||||
return p.wrap_slot(slot)
|
||||
}
|
||||
// Capacity available: open a new conn outside the lock
|
||||
if p.max_open == 0 || p.open_count < p.max_open {
|
||||
p.open_count++
|
||||
conninfo := p.conninfo
|
||||
p.mu.unlock()
|
||||
slot := connect_slot(conninfo) or {
|
||||
p.mu.lock()
|
||||
p.open_count--
|
||||
p.mu.unlock()
|
||||
return err
|
||||
}
|
||||
// close() may have run while we were dialing outside the lock.
|
||||
// Honor the close contract by tearing the fresh handle down
|
||||
// instead of returning a live Conn after shutdown.
|
||||
p.mu.lock()
|
||||
if p.closed {
|
||||
p.open_count--
|
||||
p.mu.unlock()
|
||||
physical_close_handle(slot.handle)
|
||||
return error('pg: pool is closed')
|
||||
}
|
||||
p.mu.unlock()
|
||||
return p.wrap_slot(slot)
|
||||
}
|
||||
// At capacity: wait for a release/close/cap-raise to signal us.
|
||||
// Senders transfer slot ownership via this channel; a sentinel slot
|
||||
// with a nil handle means "capacity changed, retry the acquire
|
||||
// loop" (used by set_max_open when raising the limit).
|
||||
waiter := chan IdleSlot{cap: 1}
|
||||
p.waiters << waiter
|
||||
p.mu.unlock()
|
||||
slot := <-waiter or { return error('pg: pool was closed while waiting for connection') }
|
||||
if isnil(slot.handle) {
|
||||
continue
|
||||
}
|
||||
return p.wrap_slot(slot)
|
||||
}
|
||||
return error('pg: unreachable')
|
||||
}
|
||||
|
||||
fn (mut p Pool) release(conn &Conn) {
|
||||
if isnil(conn) {
|
||||
return
|
||||
}
|
||||
mut c := unsafe { conn }
|
||||
// Detach the wrapper from the physical handle before doing anything else.
|
||||
// This is what makes a stale `&Conn` reference held by user code inert:
|
||||
// any subsequent method call on it will see `c.conn == nil` and error.
|
||||
// It is also the idempotency guard against double-close — a second
|
||||
// release() finds nothing to return to the pool and is a no-op.
|
||||
if isnil(c.conn) {
|
||||
return
|
||||
}
|
||||
slot := IdleSlot{
|
||||
handle: c.conn
|
||||
created_at: c.created_at
|
||||
bad: c.bad
|
||||
}
|
||||
c.conn = unsafe { nil }
|
||||
c.pool = unsafe { nil }
|
||||
p.mu.lock()
|
||||
if p.closed {
|
||||
// close() only decremented open_count for slots it found parked;
|
||||
// in-use conns are accounted for here as they trickle back.
|
||||
p.open_count--
|
||||
p.mu.unlock()
|
||||
physical_close_handle(slot.handle)
|
||||
return
|
||||
}
|
||||
// Discard broken or expired conns
|
||||
if slot_bad(slot) || slot_expired(slot, p.max_lifetime) {
|
||||
physical_close_handle(slot.handle)
|
||||
p.open_count--
|
||||
// A capacity slot just opened; hand it to a waiter as a fresh conn
|
||||
if p.waiters.len > 0 && (p.max_open == 0 || p.open_count < p.max_open) {
|
||||
waiter := p.waiters[0]
|
||||
p.waiters.delete(0)
|
||||
p.open_count++
|
||||
conninfo := p.conninfo
|
||||
p.mu.unlock()
|
||||
new_slot := connect_slot(conninfo) or {
|
||||
p.mu.lock()
|
||||
p.open_count--
|
||||
// Capacity is now open but the dial just failed. Wake every
|
||||
// other parked waiter too, otherwise they hang forever waiting
|
||||
// for a release that will never come (e.g. max_open=1).
|
||||
extras := p.waiters.clone()
|
||||
p.waiters = []chan IdleSlot{}
|
||||
p.mu.unlock()
|
||||
waiter.close()
|
||||
for w in extras {
|
||||
w.close()
|
||||
}
|
||||
return
|
||||
}
|
||||
p.mu.lock()
|
||||
if p.closed {
|
||||
// Pool closed during the dial: drop the new conn and signal the waiter.
|
||||
p.open_count--
|
||||
p.mu.unlock()
|
||||
physical_close_handle(new_slot.handle)
|
||||
waiter.close()
|
||||
return
|
||||
}
|
||||
waiter <- new_slot
|
||||
p.mu.unlock()
|
||||
return
|
||||
}
|
||||
p.mu.unlock()
|
||||
return
|
||||
}
|
||||
// If a recent set_max_open() shrank the cap below open_count, this
|
||||
// returning slot has to be retired even when a waiter is queued —
|
||||
// otherwise the fast hand-off keeps open_count pinned above the new
|
||||
// limit forever under steady traffic. Wake the waiter with a retry
|
||||
// sentinel so it re-evaluates capacity under the new cap.
|
||||
if p.max_open > 0 && p.open_count > p.max_open {
|
||||
physical_close_handle(slot.handle)
|
||||
p.open_count--
|
||||
if p.waiters.len > 0 {
|
||||
waiter := p.waiters[0]
|
||||
p.waiters.delete(0)
|
||||
waiter <- IdleSlot{
|
||||
handle: unsafe { nil }
|
||||
}
|
||||
}
|
||||
p.mu.unlock()
|
||||
return
|
||||
}
|
||||
// Healthy conn: prefer handing it directly to a waiter. Send under the
|
||||
// lock (cap:1 makes it non-blocking) so close() can't slip in and orphan
|
||||
// the popped waiter with a live conn.
|
||||
if p.waiters.len > 0 {
|
||||
waiter := p.waiters[0]
|
||||
p.waiters.delete(0)
|
||||
waiter <- slot
|
||||
p.mu.unlock()
|
||||
return
|
||||
}
|
||||
// Park as idle, unless we'd exceed max_idle (0 = keep no idle conns)
|
||||
// or we are over a recently-shrunk max_open and need to converge.
|
||||
if p.idle.len >= p.max_idle || (p.max_open > 0 && p.open_count > p.max_open) {
|
||||
physical_close_handle(slot.handle)
|
||||
p.open_count--
|
||||
p.mu.unlock()
|
||||
return
|
||||
}
|
||||
p.idle << slot
|
||||
p.mu.unlock()
|
||||
}
|
||||
|
||||
fn (mut p Pool) close() {
|
||||
p.mu.lock()
|
||||
if p.closed {
|
||||
p.mu.unlock()
|
||||
return
|
||||
}
|
||||
p.closed = true
|
||||
for slot in p.idle {
|
||||
physical_close_handle(slot.handle)
|
||||
}
|
||||
idle_len := p.idle.len
|
||||
p.idle = []IdleSlot{}
|
||||
p.open_count -= idle_len
|
||||
for waiter in p.waiters {
|
||||
waiter.close()
|
||||
}
|
||||
p.waiters = []chan IdleSlot{}
|
||||
p.mu.unlock()
|
||||
}
|
||||
|
||||
fn (mut p Pool) stats() PoolStats {
|
||||
p.mu.lock()
|
||||
stats := PoolStats{
|
||||
max_open_connections: p.max_open
|
||||
open_connections: p.open_count
|
||||
idle: p.idle.len
|
||||
in_use: p.open_count - p.idle.len
|
||||
wait_count: p.waiters.len
|
||||
}
|
||||
p.mu.unlock()
|
||||
return stats
|
||||
}
|
||||
|
||||
fn (mut p Pool) set_max_open(n int) {
|
||||
mut nn := n
|
||||
if nn < 0 {
|
||||
nn = 0
|
||||
}
|
||||
p.mu.lock()
|
||||
p.max_open = nn
|
||||
// Shrink: drop idle slots until we are back under the new cap. Without
|
||||
// this, acquire() pops idle slots before checking max_open, so two
|
||||
// callers could still both succeed after set_max_open_conns(1) when two
|
||||
// warm conns are parked. In-use conns can't be reclaimed here — they get
|
||||
// discarded on release once open_count is over the cap.
|
||||
if nn > 0 {
|
||||
for p.idle.len > 0 && p.open_count > nn {
|
||||
slot := p.idle.last()
|
||||
p.idle.delete_last()
|
||||
physical_close_handle(slot.handle)
|
||||
p.open_count--
|
||||
}
|
||||
}
|
||||
// Raise: wake parked waiters so they can retry the acquire loop and
|
||||
// dial against the new headroom. Without this nudge they stay blocked
|
||||
// until some other release happens, which may never come if all current
|
||||
// conns are long-lived.
|
||||
if p.waiters.len > 0 && (nn == 0 || p.open_count < nn) {
|
||||
spare := if nn == 0 { p.waiters.len } else { nn - p.open_count }
|
||||
mut n_wake := if spare < p.waiters.len { spare } else { p.waiters.len }
|
||||
for n_wake > 0 {
|
||||
waiter := p.waiters[0]
|
||||
p.waiters.delete(0)
|
||||
waiter <- IdleSlot{
|
||||
handle: unsafe { nil }
|
||||
}
|
||||
n_wake--
|
||||
}
|
||||
}
|
||||
p.mu.unlock()
|
||||
}
|
||||
|
||||
fn (mut p Pool) set_max_idle(n int) {
|
||||
mut nn := n
|
||||
if nn < 0 {
|
||||
nn = 0
|
||||
}
|
||||
p.mu.lock()
|
||||
p.max_idle = nn
|
||||
for p.idle.len > nn {
|
||||
slot := p.idle.last()
|
||||
p.idle.delete_last()
|
||||
physical_close_handle(slot.handle)
|
||||
p.open_count--
|
||||
}
|
||||
p.mu.unlock()
|
||||
}
|
||||
|
||||
fn (mut p Pool) set_conn_max_lifetime(d time.Duration) {
|
||||
p.mu.lock()
|
||||
p.max_lifetime = d
|
||||
p.mu.unlock()
|
||||
}
|
||||
|
||||
// stash_last_id records `id` as the last-inserted id for the current thread.
|
||||
// Called by `DB.insert` right after running INSERT on the pinned conn, so the
|
||||
// next `DB.last_id()` call on the same thread returns this exact value rather
|
||||
// than running LASTVAL() on a different pooled session.
|
||||
fn (mut p Pool) stash_last_id(id int) {
|
||||
tid := sync.thread_id()
|
||||
p.last_ids_mu.lock()
|
||||
p.last_ids[tid] = i64(id)
|
||||
p.last_ids_mu.unlock()
|
||||
}
|
||||
|
||||
// take_last_id returns the last-inserted id stashed by this thread's most
|
||||
// recent `DB.insert`, or 0 if there is none. Consuming on read keeps the
|
||||
// map bounded as threads come and go, and prevents a future thread that
|
||||
// reuses this tid from observing a stale id before doing its own insert.
|
||||
fn (mut p Pool) take_last_id() int {
|
||||
tid := sync.thread_id()
|
||||
p.last_ids_mu.lock()
|
||||
id := p.last_ids[tid] or { i64(0) }
|
||||
p.last_ids.delete(tid)
|
||||
p.last_ids_mu.unlock()
|
||||
return int(id)
|
||||
}
|
||||
Reference in new issue
Block a user