Added a connection pool timeout to the Configuration type in the Persistence package.

This commit is contained in:
2026-08-13 00:14:51 +02:00
parent d7465d03cc
commit 70c0cc5303
10 changed files with 39 additions and 16 deletions
@@ -77,7 +77,8 @@ public struct Service: Sendable {
database: configuration.name,
tls: tls
),
maxConnectionsPerEventLoop: configuration.maxConnectionsPerEventLoop
maxConnectionsPerEventLoop: configuration.maxConnectionsPerEventLoop,
connectionPoolTimeout: .init(configuration.poolTimeout)
),
as: .psql,
isDefault: true
@@ -17,6 +17,9 @@ public struct Configuration: Sendable {
/// The password the connection authenticates with.
let password: String
/// The longest a query waits for a pooled connection to become available before failing.
let poolTimeout: Duration
/// The port the database server listens on.
let port: Int
@@ -37,6 +40,7 @@ public struct Configuration: Sendable {
/// - password: the password the connection authenticates with.
/// - tls: the TLS posture used when connecting.
/// - maxConnectionsPerEventLoop: the maximum number of pooled connections opened per event loop.
/// - poolTimeout: the longest a query waits for a pooled connection to become available before failing.
public init(
host: String,
port: Int,
@@ -44,15 +48,17 @@ public struct Configuration: Sendable {
username: String,
password: String,
tls: TLS,
maxConnectionsPerEventLoop: Int
maxConnectionsPerEventLoop: Int,
poolTimeout: Duration
) {
self.host = host
self.port = port
self.name = name
self.username = username
self.password = password
self.tls = tls
self.maxConnectionsPerEventLoop = maxConnectionsPerEventLoop
self.name = name
self.password = password
self.poolTimeout = poolTimeout
self.port = port
self.tls = tls
self.username = username
}
}
@@ -42,7 +42,8 @@ struct ProbeTests {
username: "nobody",
password: "nothing",
tls: .off,
maxConnectionsPerEventLoop: 1
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")
@@ -72,7 +73,8 @@ struct ProbeTests {
username: "nobody",
password: "nothing",
tls: .off,
maxConnectionsPerEventLoop: 1
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")
@@ -43,7 +43,8 @@ struct ServiceTests {
username: "loud",
password: "loud",
tls: .off,
maxConnectionsPerEventLoop: 1
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")
@@ -165,7 +166,8 @@ private let postgresDriver: Persistence.Driver? = {
username: environment["POSTGRES_TEST_USERNAME"] ?? "loud",
password: environment["POSTGRES_TEST_PASSWORD"] ?? "loud",
tls: .off,
maxConnectionsPerEventLoop: 2
maxConnectionsPerEventLoop: 2,
poolTimeout: .seconds(10)
)
)
}()
+2 -1
View File
@@ -111,8 +111,9 @@ A dotted config key maps to an environment variable by upper-casing, splitting c
| `database.password` | `DATABASE_PASSWORD` | _(empty)_ | Database password. Provide via the environment/a secret — never commit it. |
| `database.tls` | `DATABASE_TLS` | `prefer` | TLS posture when connecting: `off`, `prefer`, or `require`. Ignored for `inMemory`. |
| `database.pool.maxPerEventLoop` | `DATABASE_POOL_MAX_PER_EVENT_LOOP` | `4` | Maximum pooled connections per event loop. Ignored for `inMemory`. |
| `database.pool.timeout` | `DATABASE_POOL_TIMEOUT` | `10` | Longest wait, in seconds, for a pooled connection to become available before the query fails. Ignored for `inMemory`. |
> **Connection budget:** the pool holds `database.pool.maxPerEventLoop` connections *per event loop*, and the event loop group runs one loop per core. An 8-core instance can therefore open 32, and each replica that many again — three replicas exhaust PostgreSQL's default `max_connections` of 100. Size this against the server's limit, not against the number alone.
> **Connection budget:** the pool holds `database.pool.maxPerEventLoop` connections *per event loop*, and the event loop group runs one loop per core. An 8-core instance can therefore open 32, and each replica that many again — three replicas exhaust PostgreSQL's default `max_connections` of 100. Size this against the server's limit, not against the number alone. When every connection is busy, a query waits up to `database.pool.timeout` for one to free up and then fails — this bounds how long requests stall on an exhausted pool.
See [Persistence](#persistence-1) below for the workflow.
@@ -60,8 +60,8 @@ package extension ConfigReader {
/// The persistence backend the service runs against, derived from the `database.*` keys.
///
/// When `database.driver` selects PostgreSQL, the connection parameters are assembled from the `database.host`, `database.port`,
/// `database.name`, `database.username`, `database.password` (empty when unset), `database.tls`, and
/// `database.pool.maxPerEventLoop` keys. Any other driver value falls back to the in-memory database.
/// `database.name`, `database.username`, `database.password` (empty when unset), `database.tls`,
/// `database.pool.maxPerEventLoop`, and `database.pool.timeout` keys. Any other driver value falls back to the in-memory database.
var driver: Driver {
switch string(
forKey: .Database.driver,
@@ -94,7 +94,11 @@ package extension ConfigReader {
maxConnectionsPerEventLoop: int(
forKey: .Database.poolMaxPerEventLoop,
default: .Database.poolMaxPerEventLoop
)
),
poolTimeout: .seconds(int(
forKey: .Database.poolTimeout,
default: .Database.poolTimeout
))
)
)
default:
@@ -37,6 +37,8 @@ extension AbsoluteConfigKey {
public static let tls: AbsoluteConfigKey = .init(.Database.tls)
/// The absolute configuration key for the maximum pooled connections per event loop.
public static let poolMaxPerEventLoop: AbsoluteConfigKey = .init(.Database.poolMaxPerEventLoop)
/// The absolute configuration key for the longest wait, in seconds, for a pooled connection to become available.
public static let poolTimeout: AbsoluteConfigKey = .init(.Database.poolTimeout)
}
/// A namespace for the HTTP server configuration keys, as absolute keys.
public enum HTTP {
@@ -37,6 +37,8 @@ extension ConfigKey {
public static let tls: ConfigKey = "database.tls"
/// The configuration key for the maximum pooled connections per event loop.
public static let poolMaxPerEventLoop: ConfigKey = "database.pool.maxPerEventLoop"
/// The configuration key for the longest wait, in seconds, for a pooled connection to become available.
public static let poolTimeout: ConfigKey = "database.pool.timeout"
}
/// A namespace for the HTTP server configuration keys.
public enum HTTP {
@@ -21,5 +21,7 @@ extension Int {
public static let port = 5_432
/// The default maximum pooled connections per event loop.
public static let poolMaxPerEventLoop = 4
/// The default longest wait, in seconds, for a pooled connection to become available (the driver's own default).
public static let poolTimeout = 10
}
}
@@ -78,7 +78,8 @@ struct HealthControllerTests {
username: "nobody",
password: "nothing",
tls: .off,
maxConnectionsPerEventLoop: 1
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")