rustbreak

repository·master·Indexed 19 days ago

https://github.com/theneikos/rustbreak

A fast, simple, and thread-safe self-contained file database for Rust, inspired by Daybreak. It supports arbitrary data persistence using Serde-compatible encodings such as Ron, Yaml, and Bincode. Rustbreak provides multiple storage backends including FileDatabase (with atomic saves via PathBackend), MemoryDatabase, and MmapDatabase. It utilizes a closure-based API for read and write operations and requires the entire database to fit into memory.

Tokens
7.2K
Snippets
27
Records
34
Agent score
64%

What's inside rustbreak

  1. How to create different types of databases

    master

    Rustbreak provides several database constructors depending on your storage needs:

    • FileDatabase: Use FileDatabase::from_path for persistent storage on disk. Note that PathBackend is the only backend that supports atomic saves.
    • MemoryDatabase: Use MemoryDatabase::memory for in-memory storage. This is fast but data is lost when the process exits unless saved.
    • MmapDatabase: Use MmapDatabase::mmap or MmapDatabase::mmap_with_size for memory-mapped files (requires the mmap feature).
    • Database: Use Database::from_parts for custom configurations.
  2. Limitations and Trade-offs of Rustbreak

    master

    Before using Rustbreak, be aware of the following constraints:

    • Memory Constraints: The entire database must fit into memory. Rustbreak does not support partial loads or saves. If the database size exceeds available RAM, the application will encounter an Out Of Memory (OOM) error.
    • Atomicity: Not all backends support atomic saves. If your program crashes during a save operation, you may end up with incomplete or corrupted data. Only PathBackend (used via FileDatabase) guarantees atomic saves.
  3. Install Rustbreak via Cargo

    master

    Add rustbreak to your Cargo.toml. You must enable an encoding feature (e.g., ron_enc, yaml_enc, or bin_enc) to use specific serialization formats.

    [dependencies.rustbreak]
    version = "2"
    features = ["ron_enc"]
  4. Understand MmapStorage resizing behavior

    master

    When using MmapStorage, the underlying memory allocation grows dynamically but never shrinks. This is managed by the Backend implementation of put_data.

    Resizing Logic:

    1. If data.len() is greater than the current mmap.len, a resize is triggered.
    2. The new size is calculated as max(old_size * 2, new_size).
    3. Warning: The resize_no_copy implementation used internally does not copy the original data to the new mapping; however, the put_data method handles the write immediately after resizing to ensure the new data is correctly placed.

    This behavior makes MmapStorage efficient for growing datasets but potentially memory-intensive if the capacity grows significantly larger than the actual data used.

  5. How Rustbreak databases work

    master

    Rustbreak is a configurable single-file database that allows you to store any serializable Rust struct. It is composed of three main components:

    1. Data: The actual Rust type you want to store (must implement Serialize and DeserializeOwned).
    2. Backend: The storage mechanism (e.g., MemoryBackend, FileBackend, PathBackend, or MmapStorage).
    3. DeSer: The serialization/deserialization strategy (e.g., Ron, Yaml, Bincode).

    Database Types

    • MemoryDatabase: Stores data in a Vec<u8> in memory. Useful for testing or transient storage.
    • FileDatabase: A classical file-based database. You provide a path or file.
    • PathDatabase: Similar to FileDatabase, but uses atomic saves to prevent data loss during panics. This is the preferred choice for file-based storage.
    • MmapDatabase: Uses a memory map backend (requires mmap feature).
  6. Quickstart: Using a MemoryDatabase with Ron encoding

    master

    To use a MemoryDatabase with Ron encoding, ensure the ron_enc feature is enabled in your Cargo.toml. You can perform write operations using .write() and read operations using .read(). Note that .write() provides mutable access to the underlying data, while .read() provides read-only access.

    extern crate rustbreak;
    use std::collections::HashMap;
    use rustbreak::{MemoryDatabase, deser::Ron};
    
    fn main() -> rustbreak::Result<()> {
        let db = MemoryDatabase::<HashMap<u32, String>, Ron>::memory(HashMap::new())?;
    
        println!("Writing to Database");
        db.write(|db| {
            db.insert(0, String::from("world"));
            db.insert(1, String::from("bar"));
        });
    
        db.read(|db| {
            println!("Hello: {:?}", db.get(&0));
        })?;
    
        Ok(())
    }
  7. Read and Write data from a Rustbreak database

    master

    Interacting with the database is done through closure-based methods:

    • db.write(|db| { ... }): Provides mutable access to the database. You can perform operations like .insert() inside this closure.
    • db.read(|db| { ... }): Provides read-only access. Attempting to mutate the database inside this closure will result in a compilation error.

    Important: For persistent databases, you must call save periodically to commit changes to storage. You can run save in parallel, but it will lock write access while the data is being written to storage.

  8. Configure Encodings (Yaml, Ron, Bincode)

    master

    Rustbreak uses Serde-compatible encodings. You must enable the corresponding feature in Cargo.toml to use the specific deserialization struct.

    EncodingFeatureDeserialization Struct
    Yamlyaml_encrustbreak::deser::Yaml
    Ronron_encrustbreak::deser::Ron
    Bincodebin_encrustbreak::deser::Bincode

    You can enable multiple features at once.

    # Example of enabling multiple encodings
    [dependencies.rustbreak]
    version = "2"
    features = ["yaml_enc", "bin_enc"]
  9. Initialize database from path with `load_from_path_or_default`

    master

    Use load_from_path_or_default to attempt to load a database from a path. If the file is missing, it will initialize the database using the Default implementation of your data type.

    // Example using a PanicDefault strategy
    let db = Database::<PanicDefault, PathBackend, crate::deser::Ron>::load_from_path_or_default(path);
  10. Perform read-only operations with `read` and `borrow_data`

    master

    To access data without modifying it, use these methods:

    read

    Provides a read-only lock via a closure. Multiple readers can run in parallel.

    borrow_data

    Returns a RwLockReadGuard, allowing you to access the underlying struct directly outside of a closure.

    // Using read closure
    db.read(|data| {
        println!("Value: {:?}", data.get(&0));
    })?;
    
    // Using borrow_data
    let data = db.borrow_data()?;
    println!("Value: {:?}", data.get(&0));
  11. Migrate data types with `convert_data`

    master

    If you need to change the underlying Rust type stored in your database, use convert_data. This method consumes the current database and returns a new one with the new type, having applied a conversion function to the existing data.

    // Example: Converting a HashMap<u32, String> to a HashMap<u32, i32>
    let new_db = db.convert_data(|old_data| {
        old_data.into_iter()
            .map(|(k, v)| (k, v.parse::<i32>().unwrap_or(0)))
            .collect::<HashMap<u32, i32>>()
    })?;
  12. Manually manage data with `put_data` and `get_data`

    master

    You can bypass the standard locking API to directly inject or extract raw data:

    • put_data(data, save): Replaces the current database content with data. If save is true, it attempts to persist the change immediately.
    • get_data(load): Retrieves the current data. If load is true, it ensures the data is loaded from the backend before returning.
    // Put data and save immediately
    db.put_data(test_data(), true).expect("could not put data");
    
    // Get data from backend
    let data = db.get_data(true).expect("could not get data");