A small async Rust crate for coordinating soft restarts and graceful shutdowns around a shared SoftCycleController, designed for async runtimes where many tasks need to observe lifecycle transitions without blocking:
try_notify(payload)publishes a lifecycle notification and wakes current listeners, with an optional payload.try_clear()transitions the controller back to the non-notified state.listener()returns a future that resolves when being notified with the observed payload.
Use [SoftCycleController::new] to create a new controller instance. Call try_notify to publish a notification with a payload and get Ok(sequence_number) on success (or Err(payload) if already notified), where sequence_number is a monotonically increasing number starting from 0. Call try_clear to clear the notified state and get Ok(sequence_number) for the cleared notification (or Err(()) if not currently notified). Call listener to create a [SoftCycleListener] future that resolves with Ok(payload) when a notification is observed.
- Linearizable order: All
try_notifyandtry_clearoperations are linearizable with respect to a single global order. - Non-blocking:
try_notifyandtry_clearare synchronous and never block. - Listener completion: A listener created after a notification and before the next clearance completes immediately with the current payload. A listener created after a clear and before the next notification completes in a finite number of polls (usually one) after the next
try_notify. If multiple notify/clear cycles occur after a listener is created, it returns one of those payloads (no guarantee of returning the earliest or latest); this is a rare scenario and is not likely to happen unlesstry_notifyandtry_clearare called multiple times in a very short time frame.
global_instance(default): Enables a process-wide default controller and the async free functions [get_lifetime_controller], [try_restart], [try_shutdown], [listener], and [clear] at the crate root. The global controller usesSoftCycleMessageas the payload type, which containsShutdownandRestartvariants.
use soft_cycle::{SoftCycleController, SoftCycleMessage};
use std::sync::Arc;
use tokio::time::{sleep, Duration, timeout};
#[tokio::main]
async fn main() {
let controller = Arc::new(SoftCycleController::<SoftCycleMessage>::new());
// Worker that reacts to notifications.
let worker_controller = controller.clone();
let worker = tokio::spawn(async move {
loop {
let payload = worker_controller.listener().await.unwrap();
match payload {
SoftCycleMessage::Shutdown => {
println!("worker: shutdown");
break;
}
SoftCycleMessage::Restart => {
println!("worker: restart");
}
}
}
});
// Producer notifies restart.
assert_eq!(controller.try_notify(SoftCycleMessage::Restart), Ok(0));
// Already notified, returns Err.
assert_eq!(controller.try_notify(SoftCycleMessage::Restart), Err(SoftCycleMessage::Restart));
// Clear when restart handling phase is done.
assert_eq!(controller.try_clear(), Ok(0));
// Already cleared, returns Err.
assert_eq!(controller.try_clear(), Err(()));
sleep(Duration::from_millis(100)).await;
// Producer notifies restart again.
assert_eq!(controller.try_notify(SoftCycleMessage::Restart), Ok(1));
// Clear when restart handling phase is done.
assert_eq!(controller.try_clear(), Ok(1));
sleep(Duration::from_millis(100)).await;
// Producer notifies shutdown.
assert_eq!(controller.try_notify(SoftCycleMessage::Shutdown), Ok(2));
// Optional: wait for worker to observe shutdown.
timeout(Duration::from_secs(2), worker).await.unwrap().unwrap();
}