///|
/// Raised when the lifespan protocol stops the boot or reports a failed shutdown: the application
/// sent `lifespan.startup.failed` or `lifespan.shutdown.failed`, or it raised under
/// `Lifespan::On`. Carries the application's message.
pub suberror LifespanError {
LifespanError(String)
}
///|
/// What the lifespan invocation of the application said, or how it ended.
priv enum Reply {
Said(@moonasgi.Event)
Returned
Raised(Error)
}
///|
/// Drives an application's lifespan protocol (← uvicorn's `LifespanOn`).
///
/// One invocation of the application runs under a `Lifespan` scope for the server's whole life:
/// `startup` sends `lifespan.startup` and waits for the answer, `shutdown` sends
/// `lifespan.shutdown`. `state` is the scope's namespace; the server gives every request scope a
/// shallow copy, so what startup stores there reaches each request.
///
/// `mode` is uvicorn's `--lifespan`: under `Auto` an application that fails on the lifespan scope,
/// as one serving only HTTP does, is taken not to speak the protocol and the server carries on;
/// under `On` that failure stops the boot; under `Off` nothing runs.
pub struct Driver {
priv app : @moonasgi.AsgiApp
priv mode : @config.Lifespan
priv logger : @config.Logger
priv inbox : @aqueue.Queue[@moonasgi.Event]
priv replies : @aqueue.Queue[Reply]
priv mut running : Bool
state : Map[String, Json]
}
///|
pub fn Driver::new(
app : @moonasgi.AsgiApp,
mode~ : @config.Lifespan = Auto,
logger~ : @config.Logger = @config.Logger::silent(),
state~ : Map[String, Json] = {},
) -> Driver {
{
app,
mode,
logger,
inbox: @aqueue.Queue::Queue(kind=Unbounded),
replies: @aqueue.Queue::Queue(kind=Unbounded),
running: false,
state,
}
}
///|
/// Start the lifespan invocation as a task in `g`. It waits on `receive()` until `startup`
/// and `shutdown` send it something; its failure is contained, and reported through them.
pub fn Driver::spawn(self : Driver, g : @async.TaskGroup[Unit]) -> Unit {
if self.mode is Off {
return
}
let receive : @moonasgi.Receive = () => self.inbox.get()
let send : @moonasgi.Send = event => {
match event {
LifespanStartupComplete
| LifespanStartupFailed(_)
| LifespanShutdownComplete
| LifespanShutdownFailed(_) => self.replies.put(Said(event))
_ => ()
}
}
let scope = @moonasgi.Scope::Lifespan({
asgi: @moonasgi.AsgiVersion::lifespan(),
state: self.state,
})
self.running = true
g.spawn_bg(allow_failure=true, () => {
let ended = try (self.app)(scope, receive, send) catch {
e => Raised(e)
} noraise {
_ => Returned
}
self.running = false
self.replies.put(ended)
})
}
///|
/// Run lifespan **startup**: send `lifespan.startup` and wait for the answer. Returns on
/// `lifespan.startup.complete`, and when the application returns without answering. Raises
/// `LifespanError` on `lifespan.startup.failed`, and on a failure of the application under
/// `On`; under `Auto` such a failure means the application does not speak the protocol.
pub async fn Driver::startup(self : Driver) -> Unit {
guard self.running else { return }
self.logger.log(Info, "Waiting for application startup.")
self.inbox.put(LifespanStartup)
match self.replies.get() {
Said(LifespanStartupFailed(message~)) => {
self.logger.log(Error, message)
self.logger.log(Error, "Application startup failed. Exiting.")
raise LifespanError(message)
}
Raised(e) if self.mode is On => {
self.logger.log(
Error,
"Exception in 'lifespan' protocol: " + e.to_string(),
)
self.logger.log(Error, "Application startup failed. Exiting.")
raise LifespanError(e.to_string())
}
Raised(_) =>
self.logger.log(Info, "ASGI 'lifespan' protocol appears unsupported.")
_ => ()
}
self.logger.log(Info, "Application startup complete.")
}
///|
/// Run lifespan **shutdown**: send `lifespan.shutdown` and wait for the answer, unless the
/// invocation has already ended. Raises `LifespanError` on `lifespan.shutdown.failed`, and on a
/// failure of the application under `On`.
pub async fn Driver::shutdown(self : Driver) -> Unit {
guard self.running else { return }
self.logger.log(Info, "Waiting for application shutdown.")
self.inbox.put(LifespanShutdown)
match self.replies.get() {
Said(LifespanShutdownFailed(message~)) => {
self.logger.log(Error, message)
raise LifespanError(message)
}
Raised(e) if self.mode is On => {
self.logger.log(
Error,
"Exception in 'lifespan' protocol: " + e.to_string(),
)
raise LifespanError(e.to_string())
}
_ => ()
}
self.logger.log(Info, "Application shutdown complete.")
}