Custom integrations
Strut’s core mechanisms used by its components are also exposed to client code:
AppConfigsupports arbitrary configuration.AppContextprovides a central reference for the application’s “liveness” state (whether shutdown is imminent).AppSpindownissues “wait-for-me-at-shutdown” tokens, which enable pre-shutdown logic (e.g., cleanup).
Let’s exemplify how to integrate with Strut.
Example: Heartbeat
We’ll implement a heartbeat component that produces “tick” events at configurable intervals. When shutdown is imminent, it echoes the total uptime before exiting.
$ cargo run 1969-10-29T22:30:48.239628Z INFO ThreadId(01) strut::launchpad::wiring::preflight: Starting app with profile 'dev' (default replica, lifetime ID 'oqq-mnss-yfw') 1969-10-29T22:30:51.241940Z INFO ThreadId(05) heartbeat-demo: Tick 0 1969-10-29T22:30:54.243332Z INFO ThreadId(05) heartbeat-demo: Tick 1 1969-10-29T22:30:57.245996Z INFO ThreadId(05) heartbeat-demo: Tick 2 1969-10-29T22:30:58.242312Z INFO ThreadId(01) strut_core::context: Terminating application context 1969-10-29T22:30:58.242397Z INFO ThreadId(01) strut_core::spindown::registry: Spindown initiated 1969-10-29T22:30:58.242439Z INFO ThreadId(05) heartbeat-demo: Total uptime: 10 seconds 1969-10-29T22:30:58.242492Z INFO ThreadId(01) strut_core::spindown::registry: Waiting for 1 registered workload(s) to complete 1969-10-29T22:30:58.242550Z INFO ThreadId(01) strut_core::spindown::registry: Completed gracefully workload="heartbeat" 1969-10-29T22:30:58.242571Z INFO ThreadId(01) strut_core::spindown::registry: All workloads completed gracefully 1969-10-29T22:30:58.242588Z INFO ThreadId(01) strut_core::spindown::registry: Spindown completed
Configuration
Write a configuration struct that customizes the heartbeat period:
struct HeartbeatConfig {
period_secs: u64,
}
To make it work, we need to implement Default and serde::Deserialize on it.
The struct also cannot have any reference-type fields (to satisfy serde::de::DeserializeOwned).
use serde::Deserialize;
/// Configuration struct that implements [`Default`] and [`Deserialize`].
#[derive(Debug, Deserialize)]
#[serde(default)]
struct HeartbeatConfig {
period_secs: u64,
}
/// Implementing by hand to have non-trivial defaults.
impl Default for HeartbeatConfig {
fn default() -> Self {
Self { period_secs: 3 }
}
}
Startup
The startup logic begins (but not necessarily ends) at startup.
Write a startup function that emits an incrementing tick event every period_secs seconds.
use std::time::Duration;
use strut::{AppConfig, AppContext};
/// Implements main logic of the `heartbeat` component. In this case, it emits
/// an incrementing tick number at regular intervals.
async fn heartbeat_startup() {
// Create counter
let mut counter = 0;
// Retrieve config
let heartbeat_config: HeartbeatConfig = AppConfig::section("heartbeat");
let period = Duration::from_secs(heartbeat_config.period_secs);
// Write heartbeat events repeatedly
while AppContext::is_alive() {
tokio::time::sleep(period).await;
tracing::info!("Tick {}", counter);
counter += 1;
}
}
The function has to be async because we want to suspend execution without blocking the thread while we wait for the next tick.
Things to note here:
AppConfigsupports arbitrary config structures, which you can request by name.AppContextallows us to check whether the application is alive, so we can stop ticking when the application is headed for shutdown.
Spindown
The spindown logic gets executed right before the application shuts down. Write a spindown function that emits the total application uptime in seconds right before exiting.
use std::time::Instant;
use strut::{AppContext, AppSpindown};
/// Implements pre-shutdown logic of the `heartbeat` component. In this case, it
/// reports the total application uptime.
async fn heartbeat_spindown() {
// Mark startup time
let startup_time = Instant::now();
// Register for spindown (dropping the token is sufficient to signal completion)
let _token = AppSpindown::register("heartbeat");
// Wait for global application context to terminate
AppContext::terminated().await;
// Report total uptime right before shutdown
tracing::info!("Total uptime: {} seconds", startup_time.elapsed().as_secs());
}
This function is also async because it will spend most of the time waiting for the application to go into spindown. Things to note here:
- You should register with
AppSpindownearly on and keep the returned token. AppContextallows us to suspend execution until the application context is terminated.
Main function
The #[strut::main] function ties it all up, producing the output similar to the above.
use std::time::Duration;
#[strut::main]
async fn main() {
// Schedule startup and spindown logic
tokio::spawn(heartbeat_startup());
tokio::spawn(heartbeat_spindown());
// Simulate 10 seconds of work
tokio::time::sleep(Duration::from_secs(10)).await;
}
Full code
use serde::Deserialize;
use std::time::{Duration, Instant};
use strut::{AppConfig, AppContext, AppSpindown};
#[strut::main]
async fn main() {
// Schedule startup and spindown logic
tokio::spawn(heartbeat_startup());
tokio::spawn(heartbeat_spindown());
// Simulate 10 seconds of work
tokio::time::sleep(Duration::from_secs(10)).await;
}
/// Configuration struct that implements [`Default`] and [`Deserialize`].
#[derive(Debug, Deserialize)]
#[serde(default)]
struct HeartbeatConfig {
period_secs: u64,
}
/// Implementing by hand to have non-trivial defaults.
impl Default for HeartbeatConfig {
fn default() -> Self {
Self { period_secs: 3 }
}
}
/// Implements main logic of the `heartbeat` component. In this case, it emits
/// an incrementing tick number at regular intervals.
async fn heartbeat_startup() {
// Create counter
let mut counter = 0;
// Retrieve config
let heartbeat_config: HeartbeatConfig = AppConfig::section("heartbeat");
let period = Duration::from_secs(heartbeat_config.period_secs);
// Write heartbeat events repeatedly
while AppContext::is_alive() {
tokio::time::sleep(period).await;
tracing::info!("Tick {}", counter);
counter += 1;
}
}
/// Implements pre-shutdown logic of the `heartbeat` component. In this case, it
/// reports the total application uptime.
async fn heartbeat_spindown() {
// Mark startup time
let startup_time = Instant::now();
// Register for spindown (dropping the token is sufficient to signal completion)
let _token = AppSpindown::register("heartbeat");
// Wait for global application context to terminate
AppContext::terminated().await;
// Report total uptime right before shutdown
tracing::info!("Total uptime: {} seconds", startup_time.elapsed().as_secs());
}
We didn’t provide any runtime configuration, forcing AppConfig to use the default.
What will happen if we run this example with APP_HEARTBEAT_PERIODSECS=1 cargo run?