soft-cycle 0.2.0

Async controller for coordinating soft restarts and graceful shutdowns with shared listeners
Documentation
# soft-cycle

[![Crates.io](https://img.shields.io/crates/v/soft-cycle)](https://crates.io/crates/soft-cycle)
[![docs.rs](https://img.shields.io/docsrs/soft-cycle)](https://docs.rs/soft-cycle)
[![License: MIT](https://img.shields.io/crates/l/soft-cycle)](LICENSE)
[![CI](https://github.com/GeminiLab/soft-cycle/actions/workflows/ci.yml/badge.svg)](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();
}
```