Updated the Probe method in the Persistence package with a query deadline.
This commit is contained in:
@@ -12,14 +12,21 @@ public struct Probe: Sendable {
|
||||
/// The `Fluent` service whose default database is probed.
|
||||
private let fluent: Fluent
|
||||
|
||||
/// The longest the probe waits for the database's answer before reporting it as not reachable.
|
||||
private let timeout: Duration
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a probe for the default database of the given `Fluent` service.
|
||||
/// - Parameter fluent: the `Fluent` service whose default database is probed.
|
||||
/// - 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.
|
||||
public init(
|
||||
fluent: Fluent
|
||||
fluent: Fluent,
|
||||
timeout: Duration = .seconds(2)
|
||||
) {
|
||||
self.fluent = fluent
|
||||
self.timeout = timeout
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
@@ -28,21 +35,43 @@ public struct Probe: Sendable {
|
||||
///
|
||||
/// Runs a trivial `SELECT 1` against the default database — the cheapest statement both the PostgreSQL and SQLite backends understand — so
|
||||
/// a readiness check does not depend on any particular schema or model. Any failure (connection refused, authentication error, pool exhausted) is
|
||||
/// reported as not reachable rather than thrown, so callers can map it straight onto a readiness response. A default database that is not an SQL
|
||||
/// database is likewise reported as not reachable.
|
||||
/// - Returns: `true` when the database answers the probe, `false` otherwise.
|
||||
/// 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.
|
||||
public func callAsFunction() async -> Bool {
|
||||
guard let database = fluent.db() as? any SQLDatabase else {
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
try await database.raw("SELECT 1").run()
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
var answer = answers.makeAsyncIterator()
|
||||
let isReachable = await answer.next() ?? false
|
||||
|
||||
query.cancel()
|
||||
deadline.cancel()
|
||||
|
||||
return isReachable
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user