durable-actors 0.5.2

Standalone regional durable-actors control plane, host, and durability runtime
Documentation
use super::{ActorReplicaFleet, store::Group};
use crate::{
    bucket::RuntimeStorage,
    clock::{Clock, SystemClock},
    placement::ObjectPlacementStore,
};
use anyhow::Result;
use std::{sync::Arc, time::Duration};
use tokio_util::sync::CancellationToken;

impl ActorReplicaFleet {
    pub(crate) fn start(self: &Arc<Self>, storage: Arc<RuntimeStorage>, stop: CancellationToken) {
        let fleet = self.clone();
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(20));
            loop {
                tokio::select! { _ = stop.cancelled() => return, _ = interval.tick() => {} }
                tokio::select! {
                    _ = stop.cancelled() => return,
                    result = fleet.reconcile(&storage) => {
                        if let Err(error) = result { tracing::warn!(%error, "actor replica reconciliation failed"); }
                    }
                }
            }
        });
    }

    async fn reconcile(&self, storage: &RuntimeStorage) -> Result<()> {
        for group in self.store.candidates().await? {
            let id = group.scope.identity();
            if let Err(error) = self.reconcile_group(storage, group).await {
                tracing::warn!(%error, activation = %id, "actor replica cleanup deferred");
            }
            self.store.checked(&id).await?;
        }
        Ok(())
    }

    async fn reconcile_group(&self, storage: &RuntimeStorage, group: Group) -> Result<()> {
        let owner = storage.get_owner(&group.scope.actor.storage_key()).await?;
        let now = SystemClock.now_ms()?;
        let active = owner.is_some_and(|owner| {
            owner.owner == group.scope.host
                && owner.lease.session_id == group.scope.session
                && owner.lease.expires_at_ms > now
        });
        if active {
            return self.retire_superseded(storage, &group).await;
        }
        let group = self.store.retire(&group.scope.identity()).await?;
        storage.retire_replication(&group.scope).await?;
        for slot in &group.slots {
            for instance in &slot.instances {
                self.provider.retire(&instance.host).await?;
            }
        }
        self.store.delete(&group.scope.identity()).await
    }

    async fn retire_superseded(&self, storage: &RuntimeStorage, group: &Group) -> Result<()> {
        if group.slots.iter().all(|slot| slot.instances.len() == 1) {
            return Ok(());
        }
        let members = storage.replica_members(&group.scope).await?;
        for slot in &group.slots {
            for instance in slot
                .instances
                .iter()
                .take(slot.instances.len().saturating_sub(1))
            {
                if members.iter().any(|m| m.host_id == instance.host) {
                    continue;
                }
                self.provider.retire(&instance.host).await?;
                self.store
                    .forget_instance(&group.scope.identity(), &instance.host)
                    .await?;
            }
        }
        Ok(())
    }
}

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