use anyhow::anyhow;
use kameo::prelude::*;
use serde::{Deserialize, Serialize};
#[cfg(feature = "tracing")]
use std::any;
#[cfg(feature = "tracing")]
use std::fmt::Debug;
#[cfg(feature = "tracing")]
use tracing::{debug, trace, warn};
use url::Url;
pub trait PersistentActor: Actor {
#[cfg(feature = "tracing")]
type Snapshot: Debug
+ Clone
+ Send
+ Sync
+ Serialize
+ for<'a> Deserialize<'a>
+ Into<<Self as Actor>::Args>
+ for<'a> From<&'a Self>;
#[cfg(not(feature = "tracing"))]
type Snapshot: Clone
+ Send
+ Sync
+ Serialize
+ for<'a> Deserialize<'a>
+ Into<<Self as Actor>::Args>
+ for<'a> From<&'a Self>;
fn register_persistent(persistence_key: Url, actor_ref: &ActorRef<Self>) -> anyhow::Result<()>;
fn persistence_key(actor_ref: &ActorRef<Self>) -> Option<Url>;
fn lookup_persistent(persistence_key: &Url) -> Option<ActorRef<Self>>;
fn save_snapshot(
&self,
actor_ref: &ActorRef<Self>,
) -> impl Future<Output = anyhow::Result<()>> {
Box::pin(async move {
let Some(key) = Self::persistence_key(actor_ref) else {
#[cfg(feature = "tracing")]
trace!(
"Actor {} is not persistent, skipping snapshot save.",
any::type_name::<Self>()
);
return Ok(());
};
let snapshot = Self::Snapshot::from(self);
Self::try_write(&key, snapshot).await?;
Ok(())
})
}
fn spawn_persistent(
persistence_key: Url,
args: <Self as Actor>::Args,
) -> impl Future<Output = anyhow::Result<ActorRef<Self>>> {
Box::pin(async move {
let actor_ref = Self::spawn(args);
Self::register_persistent(persistence_key, &actor_ref)?;
Ok(actor_ref)
})
}
fn respawn_persistent(
persistence_key: Url,
) -> impl Future<Output = anyhow::Result<ActorRef<Self>>> {
Box::pin(async move {
if let Some(actor_ref) = Self::lookup_persistent(&persistence_key) {
#[cfg(feature = "tracing")]
trace!(
"Found existing persistent actor {} with key {persistence_key:?}.",
any::type_name::<Self>(),
);
return Ok(actor_ref);
}
let data = Self::try_read(&persistence_key).await?;
let snapshot: Self::Snapshot = postcard::from_bytes(&data)?;
let actor_ref = Self::spawn_persistent(persistence_key, snapshot.into()).await?;
Ok(actor_ref)
})
}
fn try_respawn_persistent(
persistence_key: Url,
args: <Self as Actor>::Args,
) -> impl Future<Output = anyhow::Result<ActorRef<Self>>> {
Box::pin(async move {
match Self::respawn_persistent(persistence_key.clone()).await {
Ok(actor_ref) => Ok(actor_ref),
Err(_e) => {
#[cfg(feature = "tracing")]
warn!(
"Failed to respawn persistent actor {} with key {persistence_key:?}: {_e}. Creating a new instance.",
any::type_name::<Self>(),
);
Self::spawn_persistent(persistence_key, args).await
}
}
})
}
fn try_read(persistence_key: &Url) -> impl Future<Output = anyhow::Result<Vec<u8>>> {
Box::pin(async move {
match persistence_key.scheme() {
"file" => {
let path = persistence_key
.to_file_path()
.map_err(|_| anyhow!("Failed to convert Url to file path"))?;
if !path.exists() {
anyhow::bail!("persistence key does not exist: {path:?}");
}
Ok(std::fs::read(&path.join("index.bin"))?)
}
_ => Err(anyhow!(
"Unsupported scheme for persistence key: {}",
persistence_key.scheme()
)),
}
})
}
fn try_write(
persistence_key: &Url,
snapshot: Self::Snapshot,
) -> impl Future<Output = anyhow::Result<()>> {
Box::pin(async move {
#[cfg(feature = "tracing")]
debug!(
"Saving snapshot {snapshot:#?} for actor: {:?} with key: {persistence_key:?}",
any::type_name::<Self>(),
);
let data = postcard::to_stdvec(&snapshot)?;
match persistence_key.scheme() {
"file" => {
let path = persistence_key
.to_file_path()
.map_err(|_| anyhow!("Failed to convert Url to file path"))?;
if !path.exists() {
std::fs::create_dir_all(&path)?;
} else if !path.is_dir() {
anyhow::bail!("persistence key exists but is not a directory: {:?}", path);
}
std::fs::write(&path.join("index.bin"), data)?;
Ok(())
}
_ => Err(anyhow!(
"Unsupported scheme for persistencekey: {}",
persistence_key.scheme()
)),
}
})
}
}