durable-actors 0.10.0

Standalone regional durable-actors control plane, host, and durability runtime
use std::time::Duration;

use anyhow::Result;
use async_trait::async_trait;
use moka::{future::Cache, ops::compute::Op};

use super::LifecycleKind;
use crate::{actor_state::ActorStorageKey, redis_client::Redis};

#[async_trait]
pub(crate) trait RoutingHints: Send + Sync {
    async fn record(&self, key: &str, kind: LifecycleKind) -> Result<()>;
    async fn get(&self, key: &str) -> Result<Option<LifecycleKind>>;
}

pub(crate) struct MemoryRoutingHints(Cache<String, LifecycleKind>);

impl MemoryRoutingHints {
    pub(crate) fn new(retention: Duration) -> Self {
        Self(
            Cache::builder()
                .max_capacity(100_000)
                .time_to_live(retention)
                .build(),
        )
    }
}

#[async_trait]
impl RoutingHints for MemoryRoutingHints {
    async fn record(&self, key: &str, kind: LifecycleKind) -> Result<()> {
        self.0
            .entry(key.to_owned())
            .and_compute_with(|previous| async move {
                if previous.is_some_and(|entry| *entry.value() == LifecycleKind::Stopped) {
                    Op::Nop
                } else {
                    Op::Put(kind)
                }
            })
            .await;
        Ok(())
    }

    async fn get(&self, key: &str) -> Result<Option<LifecycleKind>> {
        Ok(self.0.get(key).await)
    }
}

pub(crate) struct RedisRoutingHints {
    redis: Redis,
    namespace: String,
    retention: Duration,
}

impl RedisRoutingHints {
    pub(crate) fn new(redis: Redis, namespace: &str, retention: Duration) -> Self {
        Self {
            redis,
            namespace: namespace.into(),
            retention,
        }
    }
}

#[async_trait]
impl RoutingHints for RedisRoutingHints {
    async fn record(&self, key: &str, kind: LifecycleKind) -> Result<()> {
        let mut command = redis::cmd("SET");
        command
            .arg(format!("{}:{key}", self.namespace))
            .arg(serde_json::to_string(&kind)?)
            .arg("EX")
            .arg(self.retention.as_secs().max(1));
        if kind == LifecycleKind::Draining {
            command.arg("NX");
        }
        let _: Option<String> = self.redis.run(command).await?;
        Ok(())
    }

    async fn get(&self, key: &str) -> Result<Option<LifecycleKind>> {
        let mut command = redis::cmd("GET");
        command.arg(format!("{}:{key}", self.namespace));
        let value: Option<String> = self.redis.run(command).await?;
        value
            .map(|value| Ok(serde_json::from_str(&value)?))
            .transpose()
    }
}

pub(super) fn session_key(
    actor: &ActorStorageKey,
    host: &str,
    session: &str,
    epoch: u64,
) -> String {
    serde_json::to_string(&(actor, host, session, epoch)).expect("serializable actor session")
}