This PR contains the work done to introduce a _Fluent_-based persistence layer for the Website service, selectable at runtime alongside the existing in-memory default, plus the local dev tooling and docs to support it. To provide further details about the work: * Persistence package * The `Driver` and `TLS` enumerations * The `Configuration` type * The `Service` factory that builds the service * `PrepareDB` for migrations registration * The `Probe` for readiness checks. * App integration * Builds the driver, registers migrations, and attaches `Fluent` to the service lifecycle so it starts/stops with the HTTP server. * Migrate-on-boot is gated to the in-memory backend; MySQL/MariaDB is migrated out of band via --database-migrate so shared databases never race on startup. * The `ConfigReader+Properties` extension maps database.* config keys onto the driver. * Library * Added database configuration constants. * The `HealthController` controller gains a readiness probe: `GET /health/ready` checks whether the database is reachable, separate from the existing liveness check. * Others * Updated the `docker-compose` files to support a database service behind a database profile, and hardened for local development * New database targets on the `Makefile` file and overall documentation updated * Updated the `.env.local`, `Dockerfile`, and `README` files to document the persistence workflow, config keys, and local DB commands Reviewed-on: rock-n-code/loud-amsterdam#13 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
156 lines
4.9 KiB
Swift
156 lines
4.9 KiB
Swift
import Hummingbird
|
|
import NIOCore
|
|
import Persistence
|
|
|
|
/// Serves the website's health-check routes.
|
|
///
|
|
/// The controller exposes its routes as a `RouteCollection` so they can be added to a router (or a sub-group) by the application that composes it:
|
|
///
|
|
/// ```swift
|
|
/// router.addRoutes(HealthController<AppRequestContext>(probe: probe).routes)
|
|
/// ```
|
|
///
|
|
/// It always serves a liveness check at `/health`; when a `Probe` is supplied it also serves a readiness check at `/health/ready` that reports
|
|
/// whether the service's database is reachable. The two are kept distinct so an orchestrator can restart on liveness failure but only withhold traffic on
|
|
/// readiness failure.
|
|
///
|
|
/// - Note: `Context` is the request context the routes are resolved against, and must match the context of the router the routes are added to.
|
|
public struct HealthController<Context: RequestContext>: Sendable {
|
|
|
|
// MARK: Properties
|
|
|
|
/// The probe consulted for the readiness check, or `nil` when only liveness is served.
|
|
private let probe: Probe?
|
|
|
|
// MARK: Initializers
|
|
|
|
/// Creates a health controller.
|
|
/// - Parameter probe: the probe consulted by the readiness route; when `nil`, only the liveness
|
|
/// route is served.
|
|
public init(
|
|
probe: Probe? = nil
|
|
) {
|
|
self.probe = probe
|
|
}
|
|
|
|
// MARK: Computed
|
|
|
|
/// The routes served by the controller.
|
|
///
|
|
/// Serves a `GET` request for the liveness path (`/health`) with a static JSON status payload, and —
|
|
/// when a `Probe` was supplied — a `GET` request for the readiness path (`/health/ready`)
|
|
/// that consults the probe.
|
|
public var routes: RouteCollection<Context> {
|
|
let routes = RouteCollection(context: Context.self)
|
|
|
|
routes.get(
|
|
.Health.check,
|
|
use: check
|
|
)
|
|
|
|
if probe != nil {
|
|
routes.get(
|
|
.Health.ready,
|
|
use: ready
|
|
)
|
|
}
|
|
|
|
return routes
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private extension HealthController {
|
|
|
|
// MARK: Methods
|
|
|
|
/// Handles a request for the liveness check.
|
|
///
|
|
/// Returns a constant JSON body built directly per request — the payload is a tiny literal with no
|
|
/// rendering step, so there is nothing to pre-render or cache. It reports only that the process is up,
|
|
/// with no dependency check, so an orchestrator restarts the process only when the process itself is
|
|
/// unresponsive.
|
|
/// - Parameters:
|
|
/// - request: the incoming request.
|
|
/// - context: the context the request is resolved against.
|
|
/// - Returns: a `200 OK` response carrying the static JSON status payload.
|
|
@Sendable
|
|
func check(
|
|
request: Request,
|
|
context: some RequestContext
|
|
) -> Response {
|
|
json(
|
|
status: .ok,
|
|
payload: .Payload.live
|
|
)
|
|
}
|
|
|
|
/// Handles a request for the readiness check.
|
|
///
|
|
/// Consults the `Probe` supplied at initialization and reports `200 OK` when the service's
|
|
/// database is reachable, or `503 Service Unavailable` otherwise, so a load balancer withholds
|
|
/// traffic from an instance that cannot yet serve it without restarting the process.
|
|
/// - Parameters:
|
|
/// - request: the incoming request.
|
|
/// - context: the context the request is resolved against.
|
|
/// - Returns: a `200 OK` response when ready, or `503 Service Unavailable` when not.
|
|
@Sendable
|
|
func ready(
|
|
request: Request,
|
|
context: some RequestContext
|
|
) async -> Response {
|
|
guard await probe?() == true else {
|
|
return json(
|
|
status: .serviceUnavailable,
|
|
payload: .Payload.unavailable
|
|
)
|
|
}
|
|
|
|
return json(
|
|
status: .ok,
|
|
payload: .Payload.ready
|
|
)
|
|
}
|
|
|
|
/// Builds a JSON response carrying the given status and payload.
|
|
/// - Parameters:
|
|
/// - status: the HTTP status of the response.
|
|
/// - payload: the JSON body of the response.
|
|
/// - Returns: the configured JSON response.
|
|
func json(
|
|
status: HTTPResponse.Status,
|
|
payload: String
|
|
) -> Response {
|
|
Response(
|
|
status: status,
|
|
headers: [.contentType: "application/json"],
|
|
body: .init(byteBuffer: .init(string: payload))
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - RouterPath+Constants
|
|
|
|
private extension RouterPath {
|
|
/// A namespace for the ``HealthController`` route paths.
|
|
enum Health {
|
|
/// The path of the liveness endpoint.
|
|
static let check: RouterPath = "/health"
|
|
/// The path of the readiness endpoint.
|
|
static let ready: RouterPath = "/health/ready"
|
|
}
|
|
}
|
|
|
|
// MARK: - String+Constants
|
|
|
|
private extension String {
|
|
enum Payload {
|
|
static let live = #"{"status":"ok"}"#
|
|
static let ready = #"{"status":"ready"}"#
|
|
static let unavailable = #"{"status":"unavailable"}"#
|
|
}
|
|
}
|