Added a connection pool timeout to the Configuration type in the Persistence package.
This commit is contained in:
@@ -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)
|
||||
)
|
||||
)
|
||||
}()
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user