Skip to main content

Custom integrations

Strut’s core mechanisms used by its components are also exposed to client code:

  • AppConfig supports arbitrary configuration.
  • AppContext provides a central reference for the application’s “liveness” state (whether shutdown is imminent).
  • AppSpindown issues “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:

  • AppConfig supports arbitrary config structures, which you can request by name.
  • AppContext allows 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 AppSpindown early on and keep the returned token.
  • AppContext allows 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
main.rs
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());
}
No config?

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?