Lifecycle events
A Corvis application goes through three phases: startup (the container is created and providers are instantiated), running (it serves requests), and shutdown. At the boundaries between phases Corvis calls hooks that your providers, controllers, and modules can implement to prepare or release resources.
The hooks
Each hook is an async trait:
| Trait | Method | When it is called |
|---|---|---|
OnModuleInit | on_module_init(&self) | On initialization, with all dependencies already resolved |
OnApplicationBootstrap | on_application_bootstrap(&self) | After all the on_module_init, before accepting connections |
OnModuleDestroy | on_module_destroy(&self) | When the application closes (app.close() or a signal) |
BeforeApplicationShutdown | before_application_shutdown(&self, signal: Option<String>) | After all the on_module_destroy |
OnApplicationShutdown | on_application_shutdown(&self, signal: Option<String>) | At the end of shutdown |
All of them return corvis::Result<()>. Implementing the trait is enough: #[injectable], #[controller], and #[module] detect the implementation at compile time, with no additional registration.
use corvis::prelude::*;
#[injectable]
pub struct RoomsService {
db: Arc<OrmService>,
events: Arc<EventEmitter>,
}
#[async_trait]
impl OnApplicationBootstrap for RoomsService {
async fn on_application_bootstrap(&self) -> corvis::Result<()> {
for (name, topic) in DEFAULT_ROOMS {
if self.find(name).await?.is_none() {
self.db
.room()
.create(json!({ "data": { "name": name, "topic": topic, "createdBy": "tertulia" } }))
.await?;
}
}
Ok(())
}
}(examples/tertulia/src/rooms/rooms_service.rs: creates the default rooms at startup.)
In examples/tertulia/src/firehose.rs, FirehoseService uses on_application_bootstrap to register an upgrade listener on the HTTP server: at that point the container is already complete and the application is not accepting connections yet.
The full sequence
Startup
CorvisFactory::create::<AppModule>()walks the module graph and creates all providers and controllers (except those withTransientscope, which are created when injected). If something fails here,createreturns the error.app.init()(whichlistencalls for you if you didn't) registers the routes, middleware, gateways, and event handlers, and then:- calls
on_module_initon all modules; - mounts the scheduled tasks (
#[cron],#[interval],#[timeout]); - calls
on_application_bootstrapon all modules.
- calls
listenopens the socket and starts serving requests.
init() is idempotent: calling it twice does not repeat the hooks.
Shutdown
app.close().await (or a signal, see below) runs:
on_module_destroyon all modules;- stops the scheduled tasks;
before_application_shutdown(signal);on_application_shutdown(signal).
close() is also idempotent.
Order between modules
Hooks are walked module by module, starting with those farthest from the root module (the most deeply imported) and ending at the root. Within each module the order is: controllers, providers, and finally the module's own class. Shutdown follows the same order as startup.
If a hook returns Err, the walk stops and the error comes out of init(), listen(), or close().
Which instances receive hooks
- Providers with the default scope (singleton) and controllers.
- The module class (
#[module]is also a provider): a module can implementOnModuleInitfor configuration tasks. Transientproviders do not receive them (there is no single instance to notify), nor do guards, pipes, interceptors, or filters that Corvis builds on demand without being inproviders.
System signals
By default the shutdown hooks only run if you call app.close(). For the application to react to system signals, enable it before listening:
#[corvis::main]
async fn main() -> corvis::Result<()> {
let mut app = tertulia::create_app().await?;
app.enable_shutdown_hooks();
let port: u16 = app.get::<ConfigService>()?.get_or("PORT", 5150);
app.listen(format!("127.0.0.1:{port}")).await
}With enable_shutdown_hooks(), on receiving the signal:
- the server stops accepting connections and waits for the ones in progress to finish;
- the shutdown sequence runs, with
signalequal toSome("SIGINT")orSome("SIGTERM"); listenreturns.
On Unix, SIGINT (Ctrl+C) and SIGTERM are listened for. On Windows only Ctrl+C, which is reported as "SIGINT". When you close with app.close(), signal is None.
#[async_trait]
impl BeforeApplicationShutdown for JobsService {
async fn before_application_shutdown(&self, signal: Option<String>) -> corvis::Result<()> {
self.logger.log(format!("Shutting down ({})", signal.as_deref().unwrap_or("manual")));
self.flush().await
}
}Connections during shutdown
When shutdown is started by a signal, the HTTP connections have already finished before on_module_destroy is called. Hooks are for closing your own resources (queues, database connections, files), not for continuing to serve requests.
Contexts without a server
CorvisFactory::create_application_context::<M>() creates the container and runs on_module_init and on_application_bootstrap without registering routes or opening sockets. It is the basis of scripts and workers (see Standalone applications). Close with app.close().await so the shutdown hooks run.
WARNING
In the current version, create_application_context does not register the #[handlers] handlers (#[on_event], #[cron], #[interval], #[timeout]): that registration only happens in app.init(). If your worker depends on them, create the application with CorvisFactory::create and call app.init().await instead of using the standalone context.
In tests, TestingModule::init() and TestingModule::close() trigger the same phases (see Testing).