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
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
#[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_supervisionLast reading after the restart: 21.5The 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.