// Public configuration and error types for the PostgreSQL connection pool.
///|
/// Strategy used when choosing which idle physical connection to reuse next.
///
/// This setting affects only the order of the pool's idle list. It does not
/// reorder waiting tasks, and it has no effect while the pool must open a new
/// connection because no idle connection is available.
pub enum QueueMode {
Fifo
Lifo
} derive(Eq, Debug)
///|
/// Construct FIFO queue mode.
///
/// The oldest idle connection is reused first.
pub fn QueueMode::fifo() -> QueueMode {
Fifo
}
///|
/// Construct LIFO queue mode.
///
/// The most recently returned idle connection is reused first.
pub fn QueueMode::lifo() -> QueueMode {
Lifo
}
///|
/// Phase of checkout reported by `PoolError::Timeout`.
///
/// `Wait` means the caller could not obtain pool capacity in time, `Create`
/// means opening a new physical connection timed out, and `Recycle` means
/// checkout-time validation or cleanup of an idle connection timed out.
pub enum TimeoutKind {
Wait
Create
Recycle
} derive(Eq, Debug)
///|
/// Re-export the client isolation level for pooled transaction options.
pub using @client {type IsolationLevel}
///|
/// Options for the `BEGIN` command used when opening a pooled transaction.
pub struct TransactionOptions {
/// Optional transaction isolation level.
isolation_level : IsolationLevel?
/// `Some(true)` emits `READ ONLY`; `Some(false)` emits `READ WRITE`.
read_only : Bool?
/// `Some(true)` emits `DEFERRABLE`; `Some(false)` emits `NOT DEFERRABLE`.
deferrable : Bool?
} derive(Eq, Debug)
///|
/// Build transaction options for a future `BEGIN` command.
pub fn TransactionOptions::TransactionOptions(
isolation_level? : IsolationLevel,
read_only? : Bool,
deferrable? : Bool,
) -> TransactionOptions {
{ isolation_level, read_only, deferrable, }
}
///|
/// Optional checkout timeout overrides in milliseconds.
///
/// These values control how long the pool waits for capacity, creates a new
/// connection, or recycles an idle connection. `None` leaves the phase
/// unbounded. Negative values are rejected by `PoolConfig::PoolConfig` or checkout.
pub struct Timeouts {
/// Maximum wait for a checkout slot before raising `PoolError::Timeout(Wait)`.
wait_ms : Int32?
/// Maximum wait for opening a new physical connection.
create_ms : Int32?
/// Maximum wait for checkout-time recycling of an idle connection.
recycle_ms : Int32?
} derive(Eq, Debug)
///|
/// Construct one timeout record.
///
/// This constructor stores the values as-is and does not validate them yet.
/// `None` disables the corresponding timeout.
pub fn Timeouts::Timeouts(
wait_ms~ : Int32? = None,
create_ms~ : Int32? = None,
recycle_ms~ : Int32? = None,
) -> Timeouts {
{ wait_ms, create_ms, recycle_ms, }
}
///|
/// Checkout-time cleanup policy for already-open physical connections.
///
/// This policy runs only when the pool reuses an idle connection. New
/// connections skip it and instead run the optional `post_create` hook.
pub enum RecyclingMethod {
Fast
Verified
Clean
Custom(String)
} derive(Eq, Debug)
///|
/// Construct fast recycling mode.
///
/// The pool trusts the idle connection without sending any SQL.
pub fn RecyclingMethod::fast() -> RecyclingMethod {
Fast
}
///|
/// Construct verified recycling mode.
///
/// The pool runs a lightweight connection check before handing the connection
/// out again.
pub fn RecyclingMethod::verified() -> RecyclingMethod {
Verified
}
///|
/// Construct clean recycling mode.
///
/// The pool runs the same cleanup SQL sequence used by
/// `deadpool-postgres`-style "clean" recycling. In practice this closes
/// open portals/cursors, resets session settings, clears LISTEN state, releases
/// advisory locks, and discards temporary objects before reuse.
/// Prepared statements and their cached plans are preserved so the
/// connection-local statement cache remains effective across checkouts.
pub fn RecyclingMethod::clean() -> RecyclingMethod {
Clean
}
///|
/// Construct custom recycling mode.
///
/// `sql` is executed with `batch_execute` during checkout of an idle
/// connection. If it fails or times out, that physical connection is discarded.
pub fn RecyclingMethod::custom(sql : String) -> RecyclingMethod {
Custom(sql)
}
///|
/// Capacity and checkout policy for one pool instance.
pub struct PoolConfig {
/// Maximum number of live physical PostgreSQL sessions owned by the pool.
max_size : Int32
/// Default timeout policy used by automatic checkout.
timeouts : Timeouts
/// Strategy used when selecting from the idle connection list.
queue_mode : QueueMode
/// Cleanup strategy applied when reusing an idle connection.
recycling_method : RecyclingMethod
/// Maximum prepared statements cached on each physical connection.
///
/// `0` disables caching.
statement_cache_capacity : Int32
/// Maximum asynchronous messages retained across physical connections.
async_message_capacity : Int32
} derive(Eq, Debug)
///|
/// Construct and validate one pool configuration.
///
/// `max_size` and `async_message_capacity` must be positive. Each configured
/// timeout must be non-negative when present. On success this has no side effects.
pub fn PoolConfig::PoolConfig(
max_size : Int32,
timeouts~ : Timeouts = Timeouts(),
queue_mode~ : QueueMode = Fifo,
recycling_method~ : RecyclingMethod = Fast,
statement_cache_capacity~ : Int32 = 100,
async_message_capacity~ : Int32 = 256,
) -> PoolConfig raise {
validate_pool_config({
max_size,
timeouts,
queue_mode,
recycling_method,
statement_cache_capacity,
async_message_capacity,
})
{
max_size,
timeouts,
queue_mode,
recycling_method,
statement_cache_capacity,
async_message_capacity,
}
}
///|
/// Failures raised while validating declarative pool input.
///
/// These errors are reported before the pool opens any physical connection.
pub suberror ConfigError {
UsernameEmpty
DbnameEmpty
InvalidConfig(String)
} derive(Eq, Debug)
///|
/// PostgreSQL target-session policy checked right after a new connection opens.
///
/// `Any` accepts the first successfully opened target.
/// `ReadWrite` immediately runs `show transaction_read_only` and rejects a
/// target whose value is `on`, which is useful when the target list may contain
/// replicas.
pub enum TargetSessionAttrs {
Any
ReadWrite
} derive(Eq, Debug)
///|
/// Construct `Any`.
pub fn TargetSessionAttrs::any() -> TargetSessionAttrs {
Any
}
///|
/// Construct `ReadWrite`.
pub fn TargetSessionAttrs::read_write() -> TargetSessionAttrs {
ReadWrite
}
///|
/// Host-ordering strategy used before opening a new physical connection.
///
/// This affects only the order in which candidate targets are tried for a new
/// connection. It does not reshuffle already-open idle connections, and it does
/// not move a checked-out client from one target to another.
pub(all) enum LoadBalanceHosts {
Disable
Random
} derive(Eq, Debug)
///|
/// One PostgreSQL endpoint. When both addresses are set, the client connects
/// to `hostaddr` and uses `host` for TLS hostname verification.
pub struct ConnectionTarget {
/// Hostname used for TLS verification and DNS connection when no address is set.
host : String?
/// Concrete address used for the TCP connection when present.
hostaddr : String?
/// TCP port for this endpoint.
port : Int32
} derive(Eq, Debug)
///|
/// Construct one endpoint. `Config::Config` validates its address and port.
pub fn ConnectionTarget::ConnectionTarget(
host? : String,
hostaddr? : String,
port~ : Int32 = 5432,
) -> ConnectionTarget {
{ host, hostaddr, port, }
}
///|
/// Declarative input used to derive one pool and its connection targets.
///
/// The constructor validates all endpoints and common connection settings.
/// Its `Debug` representation replaces a configured password with ``.
pub struct Config {
user : String
/// Optional in the declarative config, but required at connection startup
/// when PostgreSQL selects password or SCRAM authentication.
password : String?
dbname : String
options : String?
application_name : String
ssl_mode : @client.SslMode?
ssl_root_cert : String?
channel_binding : @client.ChannelBinding?
priv targets : Array[ConnectionTarget]
connect_timeout_ms : Int32?
keepalives : Bool?
keepalives_idle_s : Int32?
target_session_attrs : TargetSessionAttrs?
load_balance_hosts : LoadBalanceHosts?
pool : PoolConfig
} derive(Eq)
///|
/// Keep pool diagnostics useful without rendering the password.
pub impl @debug.Debug for Config with fn to_repr(self) {
let password : String? = if self.password is Some(_) {
Some("")
} else {
None
}
@debug.Repr::record({
"user": @debug.Repr(self.user),
"password": @debug.Repr(password),
"dbname": @debug.Repr(self.dbname),
"options": @debug.Repr(self.options),
"application_name": @debug.Repr(self.application_name),
"ssl_mode": @debug.Repr(self.ssl_mode),
"ssl_root_cert": @debug.Repr(self.ssl_root_cert),
"channel_binding": @debug.Repr(self.channel_binding),
"targets": @debug.Repr(self.targets),
"connect_timeout_ms": @debug.Repr(self.connect_timeout_ms),
"keepalives": @debug.Repr(self.keepalives),
"keepalives_idle_s": @debug.Repr(self.keepalives_idle_s),
"target_session_attrs": @debug.Repr(self.target_session_attrs),
"load_balance_hosts": @debug.Repr(self.load_balance_hosts),
"pool": @debug.Repr(self.pool),
})
}
///|
/// Return a read-only view of the configured endpoints in connection order.
pub fn Config::targets(self : Config) -> ArrayView[ConnectionTarget] {
self.targets[:]
}
///|
/// Build a pool config from an ordered, non-empty list of endpoints.
///
/// The target array is copied. Defaults keep the secure client path:
/// `ssl_mode = VerifyFull` and `dbname = user`.
pub fn Config::Config(
targets : Array[ConnectionTarget],
user~ : String,
dbname~ : String = user,
password? : String,
ssl_mode~ : @client.SslMode = VerifyFull,
ssl_root_cert? : String,
channel_binding~ : @client.ChannelBinding = Disable,
application_name~ : String,
options? : String,
connect_timeout_ms? : Int32,
keepalives? : Bool,
keepalives_idle_s? : Int32,
target_session_attrs~ : TargetSessionAttrs = Any,
load_balance_hosts~ : LoadBalanceHosts = Disable,
pool~ : PoolConfig,
) -> Config raise {
validate_connection_targets(targets)
if user == "" {
raise ConfigError::UsernameEmpty
}
if dbname == "" {
raise ConfigError::DbnameEmpty
}
validate_tls_config(ssl_mode, ssl_root_cert)
{
user,
password,
dbname,
options,
application_name,
ssl_mode: Some(ssl_mode),
ssl_root_cert,
channel_binding: Some(channel_binding),
targets: targets.copy(),
connect_timeout_ms,
keepalives,
keepalives_idle_s,
target_session_attrs: Some(target_session_attrs),
load_balance_hosts: Some(load_balance_hosts),
pool,
}
}
///|
/// Point-in-time snapshot of one pool's runtime state.
pub struct Status {
/// Number of currently live physical connections, both idle and checked out.
size : Int32
/// Number of idle connections immediately available for reuse.
available : Int32
/// Number of tasks currently waiting for pool capacity.
waiting : Int32
/// Current configured connection limit for the pool.
max_size : Int32
/// Whether `Pool::close()` has started and new checkouts are rejected.
closed : Bool
} derive(Eq, Debug)
///|
/// Failures raised by pool lifecycle, checkout, and scoped-handle operations.
///
/// `ScopeExpired` reports use-after-scope on pooled handles, `Closed` rejects
/// new checkouts after `Pool::close()`, and `Timeout` identifies the checkout
/// phase that expired.
pub suberror PoolError {
Closed
ExecutorAlreadyStarted
Timeout(TimeoutKind)
ScopeExpired
/// A lifecycle hook returned with an open transaction; the connection was discarded.
HookLeftOpenTransaction
InvalidConfig(String)
} derive(Eq, @debug.Debug)
///|
/// Validate pool config values before the pool is built.
fn validate_pool_config(config : PoolConfig) -> Unit raise {
if config.max_size < 1 {
raise PoolError::InvalidConfig("pool max_size must be at least 1")
}
if config.statement_cache_capacity < 0 {
raise PoolError::InvalidConfig(
"statement_cache_capacity must be non-negative",
)
}
if config.async_message_capacity < 1 {
raise PoolError::InvalidConfig("async_message_capacity must be positive")
}
validate_timeout("wait_ms", config.timeouts.wait_ms)
validate_timeout("create_ms", config.timeouts.create_ms)
validate_timeout("recycle_ms", config.timeouts.recycle_ms)
}
///|
/// Validate that an optional timeout is non-negative.
fn validate_timeout(name : String, timeout_ms : Int32?) -> Unit raise {
if timeout_ms is Some(timeout_ms) {
if timeout_ms < 0 {
raise PoolError::InvalidConfig("pool \{name} must be non-negative")
}
}
}
///|
/// Return the validated pool-capacity config for this declarative input.
pub fn Config::get_pool_config(self : Config) -> PoolConfig raise {
validate_pool_config(self.pool)
self.pool
}
///|
/// Build one client config per endpoint, preserving the configured order.
///
/// Each target connects to its own address and port. When both addresses are
/// present, the client connects to `hostaddr` and verifies TLS against `host`.
pub fn Config::get_connection_targets(self : Config) -> Array[@client.Config] {
let targets : Array[@client.Config] = []
for target in self.targets {
targets.push(
@client.Config::new_internal(
target.host.unwrap_or(""),
target.hostaddr,
target.port,
self.user,
self.dbname,
self.password,
self.ssl_mode.unwrap_or(VerifyFull),
self.ssl_root_cert,
self.channel_binding.unwrap_or(Disable),
self.application_name,
self.options,
self.connect_timeout_ms,
self.keepalives,
self.keepalives_idle_s,
),
)
}
targets
}
///|
/// Validate every address and port before a config can be constructed.
fn validate_connection_targets(targets : Array[ConnectionTarget]) -> Unit raise {
if targets.length() == 0 {
raise ConfigError::InvalidConfig("targets must not be empty")
}
for index in 0.. 65535 {
raise ConfigError::InvalidConfig(
"target \{index}: port must be between 1 and 65535",
)
}
}
}
///|
fn is_non_empty_string(value : String?) -> Bool {
if value is Some(value) {
value != ""
} else {
false
}
}
///|
fn validate_tls_config(
ssl_mode : @client.SslMode,
ssl_root_cert : String?,
) -> Unit raise {
if ssl_root_cert is Some(root_cert) {
if root_cert == "system" && ssl_mode != VerifyFull {
raise ConfigError::InvalidConfig(
"sslrootcert=system requires sslmode=verify-full",
)
}
}
}