durable-actors 0.4.3

Standalone regional durable-actors control plane, host, and durability runtime
Documentation
use crate::request_traces::{
    TracePage, TraceRecord,
    replay::{InvalidTraceCursor, ReplayQuery},
};
use anyhow::Result;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use rusqlite::{Connection, Transaction, params};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Cursor {
    project_id: String,
    generation: String,
    position: u64,
}

pub(super) struct Metadata {
    pub(super) generation: String,
    pub(super) head: u64,
    pub(super) pruned: u64,
    pub(super) total: u64,
}

pub(super) fn query(
    connection: &mut Connection,
    project_id: &str,
    query: &ReplayQuery,
) -> Result<TracePage> {
    let transaction = connection.transaction()?;
    let metadata = metadata(&transaction, project_id)?;
    let cursor = query
        .cursor
        .as_deref()
        .map(|value| decode(value, project_id))
        .transpose()?;
    if cursor
        .as_ref()
        .is_some_and(|c| c.generation == metadata.generation && c.position > metadata.head)
    {
        return Err(InvalidTraceCursor.into());
    }
    let reset = cursor
        .as_ref()
        .is_some_and(|c| c.generation != metadata.generation || c.position < metadata.pruned);
    let after = cursor.filter(|_| !reset).map(|c| c.position);
    let mut records = select(&transaction, project_id, query.limit, after, metadata.head)?;
    let more = after.is_some() && records.len() > query.limit;
    records.truncate(query.limit);
    let position = if more {
        records.last().unwrap().sequence
    } else {
        metadata.head
    };
    let resume_cursor = encode(&Cursor {
        project_id: project_id.into(),
        generation: metadata.generation.clone(),
        position,
    })?;
    Ok(TracePage {
        epoch: metadata.generation,
        cursor: position,
        capacity: query.limit,
        evicted: metadata.total.saturating_sub(500),
        dropped: 0,
        persistence_failed: false,
        records,
        next_cursor: more.then(|| resume_cursor.clone()),
        resume_cursor,
        reset,
    })
}

pub(super) fn metadata(transaction: &Transaction<'_>, project_id: &str) -> Result<Metadata> {
    Ok(transaction.query_row(
        "SELECT generation, COALESCE(p.pruned, 0), COALESCE(p.total, 0), COALESCE(p.head, 0) FROM trace_meta LEFT JOIN trace_projects p ON p.project_id = ?1",
        [project_id],
        |row| Ok(Metadata { generation: row.get(0)?, pruned: row.get::<_, i64>(1)? as u64, total: row.get::<_, i64>(2)? as u64, head: row.get::<_, i64>(3)? as u64 }),
    )?)
}

fn select(
    transaction: &Transaction<'_>,
    project_id: &str,
    limit: usize,
    after: Option<u64>,
    head: u64,
) -> Result<Vec<TraceRecord>> {
    let order = if after.is_some() {
        "position ASC"
    } else {
        "started_at_ms DESC, position DESC"
    };
    let mut statement = transaction.prepare(&format!(
        "SELECT position, event FROM traces WHERE project_id = ?4 AND position > ?1 AND position <= ?2 ORDER BY {order} LIMIT ?3"
    ))?;
    let rows = statement.query_map(
        params![
            after.unwrap_or(0) as i64,
            head as i64,
            (limit + 1) as i64,
            project_id
        ],
        |row| Ok((row.get::<_, i64>(0)? as u64, row.get::<_, String>(1)?)),
    )?;
    rows.map(|row| {
        let (sequence, json) = row?;
        Ok(TraceRecord {
            sequence,
            event: serde_json::from_str(&json)?,
        })
    })
    .collect()
}

fn encode(cursor: &Cursor) -> Result<String> {
    Ok(URL_SAFE_NO_PAD.encode(serde_json::to_vec(cursor)?))
}
fn decode(value: &str, project_id: &str) -> Result<Cursor> {
    let bytes = URL_SAFE_NO_PAD
        .decode(value)
        .map_err(|_| InvalidTraceCursor)?;
    let cursor: Cursor = serde_json::from_slice(&bytes).map_err(|_| InvalidTraceCursor)?;
    if cursor.project_id != project_id || cursor.position > i64::MAX as u64 {
        return Err(InvalidTraceCursor.into());
    }
    Ok(cursor)
}

pub(super) fn resume_cursor(project_id: &str, generation: &str, position: u64) -> Result<String> {
    encode(&Cursor {
        project_id: project_id.into(),
        generation: generation.into(),
        position,
    })
}