little-durable-objects 0.1.9

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

use crate::{
    actor::ActorKey,
    grpc::proto::{ControlPlaneReply, ControlPlaneRequest},
    host::HostId,
    host_leases::{HostLease, HostLeaseRequest},
    storage_urls::StateWriteTicket,
};

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ControlPlaneCommand {
    RegisterLease {
        request: HostLeaseRequest,
    },
    UnregisterLease {
        host_id: HostId,
    },
    PrepareStateWrite {
        actor: ActorKey,
        host_id: HostId,
        owner_epoch: u64,
        expected_version: u64,
    },
    CommitState {
        actor: ActorKey,
        host_id: HostId,
        owner_epoch: u64,
        expected_version: u64,
        state_object: String,
        request_id: String,
    },
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ControlPlaneCommandReply {
    Unit,
    Lease {
        lease: HostLease,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        replacement_token: Option<String>,
    },
    StateWriteTicket {
        ticket: StateWriteTicket,
    },
    StateCommitted {
        state_version: u64,
        next_write: Option<StateWriteTicket>,
    },
}

pub(crate) fn encode_command(command: ControlPlaneCommand) -> Result<ControlPlaneRequest> {
    Ok(ControlPlaneRequest {
        command_json: serde_json::to_vec(&command)?,
    })
}

pub(crate) fn decode_command(request: ControlPlaneRequest) -> Result<ControlPlaneCommand> {
    Ok(serde_json::from_slice(&request.command_json)?)
}

pub(crate) fn encode_reply(reply: ControlPlaneCommandReply) -> Result<ControlPlaneReply> {
    Ok(ControlPlaneReply {
        reply_json: serde_json::to_vec(&reply)?,
    })
}

pub(crate) fn decode_reply(reply: ControlPlaneReply) -> Result<ControlPlaneCommandReply> {
    Ok(serde_json::from_slice(&reply.reply_json)?)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn state_commit_commands_are_plain_json_messages() -> Result<()> {
        let encoded = encode_command(ControlPlaneCommand::CommitState {
            actor: ActorKey {
                namespace_id: "project-1".into(),
                actor_type: "counter".into(),
                actor_id: "counter-1".into(),
            },
            host_id: HostId::new("host.v1.project-1.revision-1.host-1"),
            owner_epoch: 3,
            expected_version: 6,
            state_object:
                "snapshots/01/0123456789abcdef0123456789abcdef/project-1/counter/counter-1/7.json"
                    .into(),
            request_id: "request-7".into(),
        })?;
        let json = String::from_utf8(encoded.command_json)?;
        assert!(json.contains("commit_state"));
        assert!(json.contains("\"expected_version\":6"));
        assert!(json.contains("\"request_id\":\"request-7\""));
        Ok(())
    }
}