durable-actors 0.6.0

Standalone regional durable-actors control plane, host, and durability runtime
Documentation
use std::{collections::BTreeMap, time::Duration};

use anyhow::Result;
use tokio_util::sync::CancellationToken;

use super::{PostgresTracePersistence, write_transaction};

const PRUNE_BATCH_SIZE: i64 = 1000;

impl PostgresTracePersistence {
    pub(crate) fn start_retention(&self, stop: CancellationToken) {
        let store = self.clone();
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(60));
            interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
            let cleanup = async {
                loop {
                    interval.tick().await;
                    if let Err(error) = store.prune_expired().await {
                        tracing::warn!(%error, "actor analytics retention failed");
                    }
                }
            };
            tokio::select! { _ = stop.cancelled() => {}, _ = cleanup => {} }
        });
    }

    async fn prune_expired(&self) -> Result<()> {
        while self.prune_batch().await? == PRUNE_BATCH_SIZE as u64 {}
        Ok(())
    }

    pub(crate) async fn prune_batch(&self) -> Result<u64> {
        let mut client = self.database.connection().await?;
        let transaction = write_transaction(&mut client).await?;
        let rows = transaction
            .query(
                "SELECT project_id, position FROM durable_actors_traces
                 WHERE received_at < now() - $1::double precision * interval '1 second'
                 ORDER BY received_at
                 LIMIT $2",
                &[&self.retention.as_secs_f64(), &PRUNE_BATCH_SIZE],
            )
            .await?;
        let mut projects: BTreeMap<String, Vec<i64>> = BTreeMap::new();
        for row in rows {
            projects.entry(row.get(0)).or_default().push(row.get(1));
        }
        let mut deleted = 0;
        for (project, positions) in projects {
            transaction
                .query_one(
                    "SELECT project_id FROM durable_actors_trace_projects
                     WHERE project_id = $1
                     FOR UPDATE",
                    &[&project],
                )
                .await?;
            let row = transaction
                .query_one(
                    "WITH deleted AS (
                         DELETE FROM durable_actors_traces
                         WHERE project_id = $1 AND position = ANY($2)
                         RETURNING position
                     )
                     SELECT COUNT(*), MAX(position) FROM deleted",
                    &[&project, &positions],
                )
                .await?;
            let count: i64 = row.get(0);
            let pruned: Option<i64> = row.get(1);
            transaction
                .execute(
                    "UPDATE durable_actors_trace_projects
                     SET pruned = GREATEST(pruned, $2), evicted = evicted + $3
                     WHERE project_id = $1",
                    &[&project, &pruned, &count],
                )
                .await?;
            deleted += count as u64;
        }
        transaction.commit().await?;
        Ok(deleted)
    }
}