tellus
Event-sourced actors

When devices fail, again

Let a failing event-sourced device restart and recover its state by replay.

In the getting started tutorial, a device which failed on an invalid reading restarted with a fresh state and lost its last reading. An event-sourced device restarts the same way, but it recovers its state from its events.

Failing in handle

tellus/examples/tutorials/event_sourced/02_supervision.rs
fn handle(
    &self,
    _: &ActorContext<Self::Command>,
    incoming: Incoming<Self::Command>,
    _: &Self::State,
) -> Result<Effect<Self>, Self::Error> {
    let Incoming::Message(command) = incoming else {
        unreachable!("the device watches no actor, hence never gets a terminated signal")
    };

    match command {
        Command::Record(degrees) if degrees.is_nan() => Err(InvalidReading),

        Command::Record(degrees) => Ok(Effect::persist(Recorded(degrees))),

        Command::Read(reply_to) => {
            Ok(Effect::none().then(move |last_reading| reply_to.reply(*last_reading)))
        }
    }
}

handle validates a command against the state before it causes any event. A reading which is not a number fails the device: handle returns an error, and no event is persisted. The failure goes to the device's supervision strategy, exactly as for a plain actor.

Restarting with replay

tellus/examples/tutorials/event_sourced/02_supervision.rs
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config = ActorConfig::default().with_supervision_strategy(SupervisionStrategy::Restart(
        RestartPolicy::new(MAX_RESTARTS),
    ));
    let system = ActorSystem::event_sourced_with_config(
        Device,
        Persistence::new(InMemoryStore::default()),
        config,
    );

    system.root().tell(Command::Record(21.5));
    system.root().tell(Command::Record(f64::NAN));

    let last_reading = system
        .root()
        .ask(Duration::from_secs(5), Command::Read)
        .await
        .context("asking for the last reading")?;
    match last_reading {
        Some(degrees) => println!("Last reading after the restart: {degrees:.1}"),
        None => println!("No reading after the restart"),
    }

    system.stop();
    system
        .terminated()
        .await
        .context("awaiting actor system termination")
}

The device runs under the Restart strategy. When the invalid reading fails it, tellus restarts it, and the restart recovers the state by replaying the events from the store, just like a new actor system did in the previous step. The mailbox survives the restart, so the queued Read is handled by the recovered device.

Run it

cargo run --quiet -p tellus --features persistence-in-memory --example event_sourced_02_supervision
Last reading after the restart: 21.5

The plain device of the getting started tutorial answered "no reading" in the same situation.

What you learned

Validation belongs in handle, before any event exists. A failure restarts an event-sourced actor like any other, and the restart recovers its state from the store. Next, recovery gets faster.