2026-07-11 09:15:58 +00:00
import HummingbirdFluent
import SQLKit
/// A readiness probe reporting whether the database behind a `Fluent` service is reachable.
///
2026-07-30 06:33:57 +00:00
/// Built once around the application's `Fluent` service and called as a function whenever a fresh answer is needed — typically from a readiness endpoint:
/// `let ready = await probe()`.
2026-07-11 09:15:58 +00:00
public struct Probe : Sendable {
// MARK: Properties
/// The `Fluent` service whose default database is probed.
private let fluent : Fluent
2026-08-12 23:57:08 +02:00
/// The longest the probe waits for the database's answer before reporting it as not reachable.
private let timeout : Duration
2026-07-11 09:15:58 +00:00
// MARK: Initializers
/// Creates a probe for the default database of the given `Fluent` service.
2026-08-12 23:57:08 +02:00
/// - Parameters:
/// - fluent: the `Fluent` service whose default database is probed.
/// - timeout: the longest the probe waits for the database's answer before reporting it as not reachable.
2026-07-11 09:15:58 +00:00
public init (
2026-08-12 23:57:08 +02:00
fluent : Fluent ,
timeout : Duration = . seconds ( 2 )
2026-07-11 09:15:58 +00:00
) {
self . fluent = fluent
2026-08-12 23:57:08 +02:00
self . timeout = timeout
2026-07-11 09:15:58 +00:00
}
// MARK: Methods
/// Reports whether the database behind the `Fluent` service is reachable.
///
2026-08-04 15:48:13 +02:00
/// Runs a trivial `SELECT 1` against the default database — the cheapest statement both the PostgreSQL and SQLite backends understand — so
2026-07-30 06:33:57 +00:00
/// a readiness check does not depend on any particular schema or model. Any failure (connection refused, authentication error, pool exhausted) is
2026-08-12 23:57:08 +02:00
/// reported as not reachable rather than thrown, and an answer that does not arrive within the timeout is likewise reported as not reachable — so
/// a database that hangs yields a prompt "not ready" instead of a hanging readiness endpoint. A default database that is not an SQL database is
/// also reported as not reachable.
/// - Returns: `true` when the database answers the probe in time, `false` otherwise.
2026-07-11 09:15:58 +00:00
public func callAsFunction () async -> Bool {
guard let database = fluent . db () as ? any SQLDatabase else {
return false
}
2026-08-12 23:57:08 +02:00
// The query is raced against the deadline from unstructured tasks whose first answer wins: a structured group
// would await the query child before returning, so a database that hangs mid-query — the very failure the
// deadline exists for — would hang the probe with it. The loser is cancelled and abandoned; a late answer lands
// in a finished stream and is dropped.
let ( answers , continuation ) = AsyncStream . makeStream ( of : Bool . self )
let query = Task {
do {
try await database
. raw ( "SELECT 1" )
. run ()
continuation . yield ( true )
} catch {
continuation . yield ( false )
}
}
let deadline = Task {
try ? await Task . sleep ( for : timeout )
continuation . yield ( false )
2026-07-11 09:15:58 +00:00
}
2026-08-12 23:57:08 +02:00
var answer = answers . makeAsyncIterator ()
let isReachable = await answer . next () ?? false
query . cancel ()
deadline . cancel ()
return isReachable
2026-07-11 09:15:58 +00:00
}
}