use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use crate::nexus::effect::EffectMarker;
use crate::nexus::row::Row;
pub const CHECKPOINT_BIT: u128 = 1 << 35;
#[derive(Copy, Clone, Debug)]
pub struct CheckpointEffect;
impl EffectMarker for CheckpointEffect {
const BIT: u128 = CHECKPOINT_BIT;
const NAME: &'static str = "Checkpoint";
}
pub type CheckpointRow = Row<CHECKPOINT_BIT>;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CheckpointId {
name: String,
sequence: u64,
}
impl CheckpointId {
pub fn new(name: impl Into<String>, sequence: u64) -> Self {
CheckpointId {
name: name.into(),
sequence,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn sequence(&self) -> u64 {
self.sequence
}
}
impl fmt::Display for CheckpointId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.name, self.sequence)
}
}
pub trait Checkpointable: Clone + 'static {
fn to_bytes(&self) -> Vec<u8>;
fn from_bytes(bytes: &[u8]) -> Option<Self>;
}
macro_rules! impl_checkpointable_int {
($($ty:ty),+ $(,)?) => {
$(impl Checkpointable for $ty {
fn to_bytes(&self) -> Vec<u8> {
self.to_le_bytes().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Option<Self> {
const N: usize = core::mem::size_of::<$ty>();
let arr: [u8; N] = bytes.get(..N)?.try_into().ok()?;
Some(<$ty>::from_le_bytes(arr))
}
})+
};
}
impl_checkpointable_int!(i32, i64, u64);
impl Checkpointable for bool {
fn to_bytes(&self) -> Vec<u8> {
vec![u8::from(*self)]
}
fn from_bytes(bytes: &[u8]) -> Option<Self> {
bytes.first().map(|&b| b != 0)
}
}
impl Checkpointable for String {
fn to_bytes(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Option<Self> {
String::from_utf8(bytes.to_vec()).ok()
}
}
impl<T: Checkpointable> Checkpointable for Vec<T> {
fn to_bytes(&self) -> Vec<u8> {
let mut result = Vec::with_capacity(8 + self.len() * 16);
result.extend_from_slice(&(self.len() as u64).to_le_bytes());
for item in self {
let bytes = item.to_bytes();
result.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
result.extend_from_slice(&bytes);
}
result
}
fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 8 {
return None;
}
let len = usize::try_from(u64::from_le_bytes(bytes[..8].try_into().ok()?)).ok()?;
let mut result = Vec::with_capacity(len);
let mut offset = 8;
for _ in 0..len {
if offset + 8 > bytes.len() {
return None;
}
let item_len = usize::try_from(u64::from_le_bytes(
bytes[offset..offset + 8].try_into().ok()?,
))
.ok()?;
offset += 8;
if offset + item_len > bytes.len() {
return None;
}
let item = T::from_bytes(&bytes[offset..offset + item_len])?;
result.push(item);
offset += item_len;
}
Some(result)
}
}
impl Checkpointable for () {
fn to_bytes(&self) -> Vec<u8> {
Vec::new()
}
fn from_bytes(_bytes: &[u8]) -> Option<Self> {
Some(())
}
}
impl<A: Checkpointable, B: Checkpointable> Checkpointable for (A, B) {
fn to_bytes(&self) -> Vec<u8> {
let a_bytes = self.0.to_bytes();
let b_bytes = self.1.to_bytes();
let mut result = Vec::with_capacity(8 + a_bytes.len() + b_bytes.len());
result.extend_from_slice(&(a_bytes.len() as u64).to_le_bytes());
result.extend_from_slice(&a_bytes);
result.extend_from_slice(&b_bytes);
result
}
fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 8 {
return None;
}
let a_len = usize::try_from(u64::from_le_bytes(bytes[..8].try_into().ok()?)).ok()?;
if bytes.len() < 8 + a_len {
return None;
}
let a = A::from_bytes(&bytes[8..8 + a_len])?;
let b = B::from_bytes(&bytes[8 + a_len..])?;
Some((a, b))
}
}
#[derive(Clone, Debug)]
pub struct Checkpoint {
pub id: CheckpointId,
pub state: Vec<u8>,
pub metadata: BTreeMap<String, String>,
pub timestamp: String,
}
impl Checkpoint {
pub fn new<T: Checkpointable>(id: CheckpointId, value: &T) -> Self {
Checkpoint {
id,
state: value.to_bytes(),
metadata: BTreeMap::new(),
timestamp: String::new(), }
}
pub fn with_metadata<T: Checkpointable>(
id: CheckpointId,
value: &T,
metadata: BTreeMap<String, String>,
) -> Self {
Checkpoint {
id,
state: value.to_bytes(),
metadata,
timestamp: String::new(),
}
}
pub fn restore<T: Checkpointable>(&self) -> Option<T> {
T::from_bytes(&self.state)
}
pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.get(key)
}
pub fn state_size(&self) -> usize {
self.state.len()
}
}
#[derive(Clone, Debug, Default)]
pub struct CheckpointStore {
checkpoints: BTreeMap<CheckpointId, Checkpoint>,
next_sequence: u64,
max_checkpoints: usize,
}
impl CheckpointStore {
pub fn new() -> Self {
CheckpointStore {
checkpoints: BTreeMap::new(),
next_sequence: 0,
max_checkpoints: 0,
}
}
pub fn with_max_checkpoints(max: usize) -> Self {
CheckpointStore {
checkpoints: BTreeMap::new(),
next_sequence: 0,
max_checkpoints: max,
}
}
pub fn save<T: Checkpointable>(&mut self, name: impl Into<String>, value: &T) -> CheckpointId {
self.store(name, |id| Checkpoint::new(id, value))
}
pub fn save_with_metadata<T: Checkpointable>(
&mut self,
name: impl Into<String>,
value: &T,
metadata: BTreeMap<String, String>,
) -> CheckpointId {
self.store(name, |id| Checkpoint::with_metadata(id, value, metadata))
}
fn store(
&mut self,
name: impl Into<String>,
make: impl FnOnce(CheckpointId) -> Checkpoint,
) -> CheckpointId {
let id = CheckpointId::new(name, self.next_sequence);
self.next_sequence += 1;
self.checkpoints.insert(id.clone(), make(id.clone()));
self.evict_oldest_over_limit();
id
}
fn evict_oldest_over_limit(&mut self) {
if self.max_checkpoints > 0
&& self.checkpoints.len() > self.max_checkpoints
&& let Some(oldest_id) = self
.checkpoints
.keys()
.min_by_key(|id| id.sequence())
.cloned()
{
self.checkpoints.remove(&oldest_id);
}
}
pub fn load(&self, id: &CheckpointId) -> Option<&Checkpoint> {
self.checkpoints.get(id)
}
pub fn load_latest(&self, name: &str) -> Option<&Checkpoint> {
self.checkpoints
.iter()
.rev()
.find(|(id, _)| id.name() == name)
.map(|(_, cp)| cp)
}
pub fn delete(&mut self, id: &CheckpointId) -> bool {
self.checkpoints.remove(id).is_some()
}
pub fn delete_by_name(&mut self, name: &str) -> usize {
let to_remove: Vec<_> = self
.checkpoints
.keys()
.filter(|id| id.name() == name)
.cloned()
.collect();
let count = to_remove.len();
for id in to_remove {
self.checkpoints.remove(&id);
}
count
}
pub fn clear(&mut self) {
self.checkpoints.clear();
}
pub fn len(&self) -> usize {
self.checkpoints.len()
}
pub fn is_empty(&self) -> bool {
self.checkpoints.is_empty()
}
pub fn list_ids(&self) -> Vec<&CheckpointId> {
self.checkpoints.keys().collect()
}
pub fn total_size(&self) -> usize {
self.checkpoints.values().map(Checkpoint::state_size).sum()
}
}
pub struct CheckpointComputation<A> {
run_fn: Box<dyn FnOnce(&mut CheckpointContext) -> A>,
}
impl<A: 'static> CheckpointComputation<A> {
pub fn new<F: FnOnce(&mut CheckpointContext) -> A + 'static>(f: F) -> Self {
CheckpointComputation {
run_fn: Box::new(f),
}
}
pub fn run(self, ctx: &mut CheckpointContext) -> A {
(self.run_fn)(ctx)
}
pub fn pure(value: A) -> Self
where
A: Clone,
{
CheckpointComputation::new(move |_| value)
}
pub fn map<B: 'static, F: FnOnce(A) -> B + 'static>(self, f: F) -> CheckpointComputation<B> {
CheckpointComputation::new(move |ctx| {
let a = (self.run_fn)(ctx);
f(a)
})
}
pub fn and_then<B: 'static, F: FnOnce(A) -> CheckpointComputation<B> + 'static>(
self,
f: F,
) -> CheckpointComputation<B> {
CheckpointComputation::new(move |ctx| {
let a = (self.run_fn)(ctx);
f(a).run(ctx)
})
}
}
pub struct CheckpointContext {
pub store: CheckpointStore,
pub enabled: bool,
pub stats: CheckpointStats,
}
#[derive(Clone, Debug, Default)]
pub struct CheckpointStats {
pub checkpoints_created: usize,
pub checkpoints_restored: usize,
pub bytes_checkpointed: usize,
}
impl CheckpointContext {
pub fn new() -> Self {
CheckpointContext {
store: CheckpointStore::new(),
enabled: true,
stats: CheckpointStats::default(),
}
}
pub fn with_store(store: CheckpointStore) -> Self {
CheckpointContext {
store,
enabled: true,
stats: CheckpointStats::default(),
}
}
pub fn checkpoint<T: Checkpointable>(
&mut self,
name: impl Into<String>,
value: &T,
) -> CheckpointId {
if !self.enabled {
return CheckpointId::new("disabled", 0);
}
let id = self.store.save(name, value);
self.stats.checkpoints_created += 1;
self.stats.bytes_checkpointed += self.store.load(&id).map_or(0, Checkpoint::state_size);
id
}
pub fn restore<T: Checkpointable>(&mut self, id: &CheckpointId) -> Option<T> {
let checkpoint = self.store.load(id)?;
let value = checkpoint.restore()?;
self.stats.checkpoints_restored += 1;
Some(value)
}
pub fn restore_latest<T: Checkpointable>(&mut self, name: &str) -> Option<T> {
let checkpoint = self.store.load_latest(name)?;
let value = checkpoint.restore()?;
self.stats.checkpoints_restored += 1;
Some(value)
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn stats(&self) -> &CheckpointStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = CheckpointStats::default();
}
}
impl Default for CheckpointContext {
fn default() -> Self {
Self::new()
}
}
pub fn checkpoint<T: Checkpointable + 'static>(
name: impl Into<String> + 'static,
value: T,
) -> CheckpointComputation<CheckpointId> {
let name = name.into();
CheckpointComputation::new(move |ctx| ctx.checkpoint(&name, &value))
}
pub fn restore<T: Checkpointable + 'static>(id: CheckpointId) -> CheckpointComputation<Option<T>> {
CheckpointComputation::new(move |ctx| ctx.restore(&id))
}
pub fn restore_latest<T: Checkpointable + 'static>(
name: impl Into<String> + 'static,
) -> CheckpointComputation<Option<T>> {
let name = name.into();
CheckpointComputation::new(move |ctx| ctx.restore_latest(&name))
}
pub fn pure<A: Clone + 'static>(value: A) -> CheckpointComputation<A> {
CheckpointComputation::pure(value)
}
type StepFn<S, A> = Box<dyn Fn(&S) -> StepResult<S, A>>;
pub struct ResumableComputation<S: Checkpointable, A> {
state: S,
step: StepFn<S, A>,
checkpoint_prefix: String,
}
#[derive(Clone, Debug)]
pub enum StepResult<S, A> {
Continue(S),
Done(A),
Checkpoint(S),
}
impl<S: Checkpointable, A: Clone + 'static> ResumableComputation<S, A> {
pub fn new<F: Fn(&S) -> StepResult<S, A> + 'static>(
initial_state: S,
step: F,
checkpoint_prefix: impl Into<String>,
) -> Self {
ResumableComputation {
state: initial_state,
step: Box::new(step),
checkpoint_prefix: checkpoint_prefix.into(),
}
}
pub fn run(mut self, ctx: &mut CheckpointContext) -> A {
let mut step_count = 0u64;
loop {
match (self.step)(&self.state) {
StepResult::Continue(new_state) => {
self.state = new_state;
step_count += 1;
}
StepResult::Done(result) => {
return result;
}
StepResult::Checkpoint(new_state) => {
self.state = new_state.clone();
let name = alloc::format!("{}_{}", self.checkpoint_prefix, step_count);
ctx.checkpoint(&name, &new_state);
step_count += 1;
}
}
}
}
pub fn resume(ctx: &mut CheckpointContext, checkpoint_prefix: &str) -> Option<S> {
ctx.restore_latest(checkpoint_prefix)
}
pub fn state(&self) -> &S {
&self.state
}
pub fn set_state(&mut self, state: S) {
self.state = state;
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_checkpoint_id() {
let id = CheckpointId::new("test", 42);
assert_eq!(id.name(), "test");
assert_eq!(id.sequence(), 42);
assert_eq!(id.to_string(), "test@42");
}
#[test]
fn test_checkpointable_i32() {
let value: i32 = 12345;
let bytes = value.to_bytes();
let restored = i32::from_bytes(&bytes);
assert_eq!(restored, Some(12345));
}
#[test]
fn test_checkpointable_i64() {
let value: i64 = 1_234_567_890_123;
let bytes = value.to_bytes();
let restored = i64::from_bytes(&bytes);
assert_eq!(restored, Some(1_234_567_890_123));
}
#[test]
fn test_checkpointable_string() {
let value = String::from("hello world");
let bytes = value.to_bytes();
let restored = String::from_bytes(&bytes);
assert_eq!(restored, Some(String::from("hello world")));
}
#[test]
fn test_checkpointable_vec() {
let value: Vec<i32> = vec![1, 2, 3, 4, 5];
let bytes = value.to_bytes();
let restored = Vec::<i32>::from_bytes(&bytes);
assert_eq!(restored, Some(vec![1, 2, 3, 4, 5]));
}
#[test]
fn test_checkpointable_tuple() {
let value: (i32, String) = (42, String::from("test"));
let bytes = value.to_bytes();
let restored = <(i32, String)>::from_bytes(&bytes);
assert_eq!(restored, Some((42, String::from("test"))));
}
#[test]
fn test_checkpoint_store_basic() {
let mut store = CheckpointStore::new();
let id1 = store.save("state_a", &100i32);
let id2 = store.save("state_b", &200i32);
assert_eq!(store.len(), 2);
let cp1 = store
.load(&id1)
.expect("checkpoint id1 should exist after save");
let cp2 = store
.load(&id2)
.expect("checkpoint id2 should exist after save");
assert_eq!(cp1.restore::<i32>(), Some(100));
assert_eq!(cp2.restore::<i32>(), Some(200));
}
#[test]
fn test_checkpoint_store_latest() {
let mut store = CheckpointStore::new();
store.save("counter", &10i32);
store.save("counter", &20i32);
store.save("counter", &30i32);
let latest = store
.load_latest("counter")
.expect("latest checkpoint for 'counter' should exist after three saves");
assert_eq!(latest.restore::<i32>(), Some(30));
}
#[test]
fn test_checkpoint_store_delete() {
let mut store = CheckpointStore::new();
let id = store.save("temp", &42i32);
assert_eq!(store.len(), 1);
assert!(store.delete(&id));
assert_eq!(store.len(), 0);
}
#[test]
fn test_checkpoint_store_max_checkpoints() {
let mut store = CheckpointStore::with_max_checkpoints(3);
store.save("a", &1i32);
store.save("b", &2i32);
store.save("c", &3i32);
assert_eq!(store.len(), 3);
store.save("d", &4i32);
assert_eq!(store.len(), 3); }
#[test]
fn test_checkpoint_store_evicts_oldest_by_sequence() {
let mut store = CheckpointStore::with_max_checkpoints(2);
let id_z_first = store.save("z", &1i32); let id_a = store.save("a", &2i32); let id_z_second = store.save("z", &3i32);
assert_eq!(store.len(), 2);
assert!(
store.load(&id_z_first).is_none(),
"oldest (z, 0) must be evicted"
);
assert!(store.load(&id_a).is_some(), "(a, 1) must survive");
assert!(store.load(&id_z_second).is_some(), "(z, 2) must survive");
}
#[test]
fn test_checkpoint_store_save_with_metadata_evicts_oldest_by_sequence() {
let mut store = CheckpointStore::with_max_checkpoints(2);
let id_z_first = store.save_with_metadata("z", &1i32, BTreeMap::new());
let id_a = store.save_with_metadata("a", &2i32, BTreeMap::new());
let id_z_second = store.save_with_metadata("z", &3i32, BTreeMap::new());
assert_eq!(store.len(), 2);
assert!(
store.load(&id_z_first).is_none(),
"oldest (z, 0) must be evicted"
);
assert!(store.load(&id_a).is_some(), "(a, 1) must survive");
assert!(store.load(&id_z_second).is_some(), "(z, 2) must survive");
}
#[test]
fn test_checkpoint_context() {
let mut ctx = CheckpointContext::new();
let id = ctx.checkpoint("progress", &50i32);
let restored: Option<i32> = ctx.restore(&id);
assert_eq!(restored, Some(50));
assert_eq!(ctx.stats().checkpoints_created, 1);
assert_eq!(ctx.stats().checkpoints_restored, 1);
}
#[test]
fn test_checkpoint_context_disabled() {
let mut ctx = CheckpointContext::new();
ctx.disable();
let id = ctx.checkpoint("progress", &50i32);
assert_eq!(id.name(), "disabled");
}
#[test]
fn test_checkpoint_computation() {
let comp = checkpoint("state", 42i32);
let mut ctx = CheckpointContext::new();
let id = comp.run(&mut ctx);
let restore_comp = restore::<i32>(id);
let value = restore_comp.run(&mut ctx);
assert_eq!(value, Some(42));
}
#[test]
fn test_checkpoint_computation_chain() {
let comp = checkpoint("first", 10i32)
.and_then(|id1| checkpoint("second", 20i32).map(move |id2| (id1, id2)));
let mut ctx = CheckpointContext::new();
let (id1, id2) = comp.run(&mut ctx);
assert_eq!(ctx.restore::<i32>(&id1), Some(10));
assert_eq!(ctx.restore::<i32>(&id2), Some(20));
}
#[test]
fn test_resumable_computation() {
let step = |state: &(i32, i32)| {
let (current, target) = *state;
if current >= target {
StepResult::Done(current)
} else if current % 10 == 0 && current > 0 {
StepResult::Checkpoint((current + 1, target))
} else {
StepResult::Continue((current + 1, target))
}
};
let comp = ResumableComputation::new((0i32, 25i32), step, "counter");
let mut ctx = CheckpointContext::new();
let result = comp.run(&mut ctx);
assert_eq!(result, 25);
assert!(ctx.stats().checkpoints_created >= 2); }
#[test]
fn test_resumable_computation_resume() {
let mut ctx = CheckpointContext::new();
ctx.checkpoint("progress", &(15i32, 30i32));
let state: Option<(i32, i32)> =
ResumableComputation::<(i32, i32), i32>::resume(&mut ctx, "progress");
assert_eq!(state, Some((15, 30)));
}
#[test]
fn test_checkpoint_metadata() {
let mut store = CheckpointStore::new();
let mut metadata = BTreeMap::new();
metadata.insert(String::from("version"), String::from("1.0"));
metadata.insert(String::from("author"), String::from("test"));
let id = store.save_with_metadata("state", &42i32, metadata);
let cp = store
.load(&id)
.expect("checkpoint should exist after save_with_metadata");
assert_eq!(cp.get_metadata("version"), Some(&String::from("1.0")));
assert_eq!(cp.get_metadata("author"), Some(&String::from("test")));
}
#[test]
fn test_total_size() {
let mut store = CheckpointStore::new();
store.save("a", &1i32); store.save("b", &2i64); store.save("c", &String::from("hello"));
assert_eq!(store.total_size(), 4 + 8 + 5);
}
#[test]
fn test_realistic_checkpoint_workflow() {
let mut ctx = CheckpointContext::new();
let mut state = 0i32;
let mut checkpoint_count = 0;
for i in 0..100 {
state += i;
if i % 25 == 0 && i > 0 {
ctx.checkpoint(alloc::format!("iteration_{i}"), &state);
checkpoint_count += 1;
}
}
assert_eq!(checkpoint_count, 3); assert_eq!(ctx.stats().checkpoints_created, 3);
let restored: Option<i32> = ctx.restore_latest("iteration_50");
assert!(restored.is_some());
assert_eq!(restored, Some(1275));
}
}