candystore 1.0.0

A lean, efficient and fast persistent in-process key-value store
Documentation
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};

use candystore::{CandyStore, Config};

mod common;

fn first_data_file_path(dir: &std::path::Path) -> std::path::PathBuf {
    std::fs::read_dir(dir)
        .unwrap()
        .filter_map(|entry| entry.ok())
        .find(|entry| entry.file_name().to_string_lossy().starts_with("data_"))
        .unwrap()
        .path()
}

fn zero_range(path: &std::path::Path, start: u64, len: usize) {
    let mut file = OpenOptions::new().write(true).open(path).unwrap();
    file.seek(SeekFrom::Start(start)).unwrap();
    file.write_all(&vec![0u8; len]).unwrap();
    file.sync_all().unwrap();
}

#[test]
fn test_zeroed_tail_data_file_lookup() {
    let dir = tempfile::tempdir().unwrap();
    let config = Config::default();

    {
        let store = CandyStore::open(dir.path(), config).unwrap();
        for idx in 0..1000 {
            store
                .set(
                    format!("key:{idx:04}").as_bytes(),
                    format!("val:{idx:04}").as_bytes(),
                )
                .unwrap();
        }
        store.flush().unwrap();
    }

    let data_path = first_data_file_path(dir.path());
    let file_len = common::logical_data_len(&data_path);
    let zero_len = 2400usize;
    zero_range(&data_path, 4096 + file_len - zero_len as u64, zero_len);

    let store = CandyStore::open(dir.path(), config).unwrap();
    let mut missing = 0;
    for idx in 0..1000 {
        let key = format!("key:{idx:04}");
        match store.get(key.as_bytes()).unwrap() {
            Some(value) => assert_eq!(value, format!("val:{idx:04}").into_bytes()),
            None => missing += 1,
        }
    }

    assert!(missing > 0);
    assert!(missing < 1000);
}

#[test]
fn test_truncated_data_file_queues() {
    let dir = tempfile::tempdir().unwrap();
    let config = Config::default();
    let num_queues = 50;
    let items_per_queue = 10;

    {
        let store = CandyStore::open(dir.path(), config).unwrap();
        for item_idx in 0..items_per_queue {
            for queue_idx in 0..num_queues {
                store
                    .push_to_queue_tail(
                        format!("queue:{queue_idx}").as_str(),
                        format!("val:{item_idx:04}").as_bytes(),
                    )
                    .unwrap();
            }
        }
        store.flush().unwrap();
    }

    let data_path = first_data_file_path(dir.path());
    let file = OpenOptions::new().write(true).open(&data_path).unwrap();
    let file_len = common::logical_data_len(&data_path);
    file.set_len(4096 + file_len - 2400).unwrap();

    let store = CandyStore::open(dir.path(), config).unwrap();

    let mut total_missing = 0;
    for queue_idx in 0..num_queues {
        let queue_key = format!("queue:{queue_idx}");
        let items: Vec<_> = store
            .iter_queue(&queue_key)
            .collect::<candystore::Result<Vec<_>>>()
            .unwrap();

        assert!(items.len() <= items_per_queue);
        total_missing += items_per_queue - items.len();

        for (idx, (_queue_idx, value)) in items.into_iter().enumerate() {
            assert_eq!(value, format!("val:{idx:04}").into_bytes());
        }
    }

    assert!(total_missing > 0);
    assert!(total_missing < num_queues * items_per_queue);
}

#[test]
fn test_truncated_data_file_lists() {
    let dir = tempfile::tempdir().unwrap();
    let config = Config::default();
    let num_lists = 50;
    let items_per_list = 10;

    {
        let store = CandyStore::open(dir.path(), config).unwrap();
        for item_idx in 0..items_per_list {
            for list_idx in 0..num_lists {
                store
                    .set_in_list(
                        format!("list:{list_idx}").as_str(),
                        format!("key:{item_idx:04}").as_bytes(),
                        format!("val:{item_idx:04}").as_bytes(),
                    )
                    .unwrap();
            }
        }
        store.flush().unwrap();
    }

    let data_path = first_data_file_path(dir.path());
    let file = OpenOptions::new().write(true).open(&data_path).unwrap();
    let file_len = common::logical_data_len(&data_path);
    file.set_len(4096 + file_len - 2400).unwrap();

    let store = CandyStore::open(dir.path(), config).unwrap();

    let mut total_missing = 0;
    for list_idx in 0..num_lists {
        let list_key = format!("list:{list_idx}");
        let items: Vec<_> = store
            .iter_list(&list_key)
            .collect::<candystore::Result<Vec<_>>>()
            .unwrap();

        assert!(items.len() <= items_per_list);
        total_missing += items_per_list - items.len();

        for (idx, (key, value)) in items.into_iter().enumerate() {
            assert_eq!(key, format!("key:{idx:04}").into_bytes());
            assert_eq!(value, format!("val:{idx:04}").into_bytes());
        }
    }

    assert!(total_missing > 0);
    assert!(total_missing < num_lists * items_per_list);
}