Server
The server SDK crate is a library, it doesn't contain an executable binary. That's why you need a server crate.
Note
As a convention, the server crate is named server
.
You can use {project_name}_server
if you need to disambiguate between
multiple Pavex applications in the same workspace.
The executable binary
The server crate contains the main
function that you'll be running to start your application.
In that main
function you'll be building an instance of ApplicationState
and passing it to run
.
You'll be doing a few other things too: initializing your tracing
subscriber, loading
configuration, etc.
The main
function in server
use anyhow::Context;
use pavex::config::ConfigLoader;
use pavex::server::{Server, ServerHandle, ShutdownMode};
use server::{
configuration::Profile,
telemetry::{get_subscriber, init_telemetry},
};
use server_sdk::{ApplicationConfig, ApplicationState, run};
use std::time::Duration;
use tracing_log_error::log_error;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load environment variables from a .env file, if it exists.
let _ = dotenvy::dotenv();
let subscriber = get_subscriber("demo".into(), "info".into(), std::io::stdout);
init_telemetry(subscriber)?;
// We isolate all the server setup and launch logic in a separate function
// to have a single point for logging fatal errors that cause the application to exit.
if let Err(e) = _main().await {
log_error!(*e, "The application is exiting due to an error");
}
Ok(())
}
async fn _main() -> anyhow::Result<()> {
let config: ApplicationConfig = ConfigLoader::<Profile>::new().load()?;
let tcp_listener = config
.server
.listener()
.await
.context("Failed to bind the server TCP listener")?;
let address = tcp_listener
.local_addr()
.context("The server TCP listener doesn't have a local socket address")?;
let server_builder = Server::new().listen(tcp_listener);
let shutdown_timeout = config.server.graceful_shutdown_timeout;
let application_state = ApplicationState::new(config)
.await
.context("Failed to build the application state")?;
tracing::info!("Starting to listen for incoming requests at {}", address);
let server_handle = run(server_builder, application_state);
graceful_shutdown(server_handle.clone(), shutdown_timeout).await;
server_handle.await;
Ok(())
}
async fn graceful_shutdown(server_handle: ServerHandle, timeout: Duration) {
tokio::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Failed to listen for the Ctrl+C signal");
server_handle
.shutdown(ShutdownMode::Graceful { timeout })
.await;
});
}
Most of this ceremony is taken care for you by the pavex new
command, but it's good to know
that it's happening (and where it's happening) in case you need to customize it.
Integration tests
The server crate is also where you'll be writing your API tests, also known as black-box tests.
These are scenarios that exercise your application as a customer would, by sending HTTP requests and asserting on the
responses.
The demo
project includes an example of such a test which you can use as a reference: