durable-actors 0.5.2

Standalone regional durable-actors control plane, host, and durability runtime
Documentation
use anyhow::{Result, ensure};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::actor::ActorKey;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WritePlan {
    pub stream: crate::replication::ReplicaStream,
    pub state_version: u64,
    pub object_name: String,
    pub expires_at_ms: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub replication: Option<crate::replication::ReplicationTicket>,
}

#[async_trait]
pub trait SnapshotReader: Send + Sync {
    async fn read_snapshot(&self, region: &str, object: &str) -> Result<bytes::Bytes>;
}

pub fn snapshot_object_name(actor: &ActorKey, state_version: u64, nonce: &str) -> Result<String> {
    actor.validate()?;
    ensure!(state_version > 0, "actor state version must be positive");
    ensure!(
        nonce.len() == 32 && nonce.bytes().all(|byte| byte.is_ascii_hexdigit()),
        "actor state object nonce is invalid"
    );
    Ok(format!(
        "{}{nonce}/{state_version}.json",
        snapshot_prefix(actor)?
    ))
}

pub(crate) fn snapshot_prefix(actor: &ActorKey) -> Result<String> {
    crate::storage_paths::snapshots(actor)
}

pub fn validate_snapshot_object_name(
    actor: &ActorKey,
    state_version: u64,
    object_name: &str,
) -> Result<()> {
    validate_object_name(object_name)?;
    let prefix = snapshot_prefix(actor)?;
    let mut parts = object_name.strip_prefix(&prefix).unwrap_or("").split('/');
    let valid = matches!(parts.next(), Some(nonce) if nonce.len() == 32 && nonce.bytes().all(|byte| byte.is_ascii_hexdigit()))
        && parts.next() == Some(format!("{state_version}.json").as_str())
        && parts.next().is_none();
    ensure!(valid, "actor state object name does not match its commit");
    Ok(())
}

pub fn validate_bucket(bucket: &str) -> Result<()> {
    ensure!(
        !bucket.is_empty()
            && bucket.trim() == bucket
            && bucket.len() <= 222
            && !bucket.contains('/'),
        "invalid storage bucket"
    );
    Ok(())
}

fn validate_object_name(object_name: &str) -> Result<()> {
    ensure!(
        object_name.starts_with(crate::storage_paths::ROOT)
            && object_name.len() <= 1024
            && !object_name.chars().any(char::is_control),
        "actor state object name is invalid"
    );
    Ok(())
}

#[cfg(test)]
#[path = "../tests/unit/storage.rs"]
mod tests;