RDB: Atomic rename without fork

C Redis forks for BGSAVE; the child inherits memory via copy-on-write. tokio cannot fork after the runtime spawns tasks. We clone the HashMap under the lock instead. Peak memory is briefly 2x; the architectural lesson (atomic rename) is identical.

06-rdb-snapshots/src/main.rs
rust
async fn snapshot_atomic(store: &Store) -> io::Result<usize> {
    let snap: HashMap<String, String> = { store.lock().await.store.clone() };
    let count = snap.len();
    let json = serde_json::to_vec(&snap).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
    tokio::fs::write(RDB_TMP, &json).await?;
    tokio::fs::rename(RDB_TMP, RDB_PATH).await?;
    Ok(count)
}

Clone under the lock, then drop the lock before writing. tokio::fs::rename is atomic on POSIX.

06-rdb-snapshots/src/main.rs
rust
async fn rdb_load(store: &Store) -> io::Result<usize> {
    let path = PathBuf::from(RDB_PATH);
    if !tokio::fs::try_exists(&path).await? { return Ok(0); }
    let bytes = tokio::fs::read(&path).await?;
    let map: HashMap<String, String> = serde_json::from_slice(&bytes)
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
    let n = map.len();
    store.lock().await.store = map;
    Ok(n)
}

On startup, read the RDB and replace the in-memory map.

Clone-under-lock vs fork+COW

Rust pays memory at snapshot time. C pays only for pages the parent dirties during the save.

Quiz: Quiz

Loading practice…