# soft-cycle
[](https://crates.io/crates/soft-cycle)
[](https://docs.rs/soft-cycle)
[](LICENSE)
[](https://github.com/GeminiLab/soft-cycle/actions/workflows/ci.yml)
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.
## Usage
Use [`SoftCycleController::new`] to create a new controller instance. Call [`try_notify`](SoftCycleController::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`](SoftCycleController::try_clear) to clear the notified state and get `Ok(sequence_number)` for the cleared notification (or `Err(())` if not currently notified). Call [`listener`](SoftCycleController::listener) to create a [`SoftCycleListener`] future that resolves with `Ok(payload)` when a notification is observed.
### Guarantees
- **Linearizable order**: All `try_notify` and `try_clear` operations are linearizable with respect to a single global order.
- **Non-blocking**: `try_notify` and `try_clear` are 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 unless `try_notify` and `try_clear` are called multiple times in a very short time frame.
## Features
- **`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 uses `SoftCycleMessage` as the payload type, which contains `Shutdown` and `Restart` variants.
## Example
```rust
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();
}
```