use std::io;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use crate::raft::types::{decode, encode};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt as _;
use tracing::info;
use crate::raft::types::Snapshot;
use crate::raft::types::SnapshotMeta;
pub fn snapshot_dump_file(snapshot_id_dir: &Path) -> String {
snapshot_id_dir
.join("dump")
.as_path()
.to_str()
.unwrap()
.to_string()
}
pub fn snapshot_meta_file(snapshot_id_dir: &Path) -> String {
snapshot_id_dir
.join("meta")
.as_path()
.to_str()
.unwrap()
.to_string()
}
pub fn snapshot_data_file(snapshot_id_dir: &Path) -> String {
snapshot_id_dir
.join("snapshot")
.as_path()
.to_str()
.unwrap()
.to_string()
}
pub fn snapshot_last_snapshot_id_file(snapshot_dir: &Path) -> String {
snapshot_dir
.join("last_snapshot_id")
.as_path()
.to_str()
.unwrap()
.to_string()
}
pub fn snapshot_id_dir(snapshot_dir: &Path, snapshot_id: &str) -> PathBuf {
snapshot_dir.join(snapshot_id)
}
pub async fn save_last_snapshot_id_file(
snapshot_dir: &Path,
last_snapshot_id: &str,
) -> io::Result<()> {
let last_snapshot_id_file = snapshot_last_snapshot_id_file(snapshot_dir);
let mut file = File::create(&last_snapshot_id_file).await?;
file.write_all(last_snapshot_id.as_bytes()).await?;
file.flush().await?;
Ok(())
}
pub(crate) async fn get_last_snapshot_id(snapshot_dir: &Path) -> io::Result<String> {
let last_snapshot_file = snapshot_last_snapshot_id_file(snapshot_dir);
let mut file = File::open(&last_snapshot_file).await?;
let mut content = String::new();
file.read_to_string(&mut content).await?;
Ok(content)
}
pub async fn save_snapshot_meta(snapshot_id_dir: &Path, meta: SnapshotMeta) -> io::Result<()> {
let meta_file = snapshot_meta_file(snapshot_id_dir);
let data = encode(&meta).map_err(|e| {
io::Error::new(
ErrorKind::InvalidData,
format!("Serialize meta data error: {}", e),
)
})?;
let mut file = File::create(&meta_file).await?;
file.write_all(&data).await?;
file.flush().await?;
Ok(())
}
pub async fn get_snapshot_meta(snapshot_id_dir: &PathBuf) -> io::Result<SnapshotMeta> {
let mut file = File::open(snapshot_id_dir).await?;
let mut data = Vec::new();
file.read_to_end(&mut data).await?;
decode(&data)
.map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("Deserialize error: {}", e)))
}
pub async fn get_current_snapshot(snapshot_dir: &Path) -> io::Result<Option<Snapshot>> {
let snapshot_id = match get_last_snapshot_id(snapshot_dir).await {
Ok(id) => id,
Err(e) if e.kind() == ErrorKind::NotFound => {
info!("No snapshot found, returning None");
return Ok(None);
}
Err(e) => return Err(e),
};
let snapshot_id_dir = snapshot_id_dir(snapshot_dir, &snapshot_id);
let snapshot_meta_file = snapshot_meta_file(&snapshot_id_dir);
let snapshot_meta = match get_snapshot_meta(&PathBuf::from(snapshot_meta_file)).await {
Ok(meta) => meta,
Err(e) if e.kind() == ErrorKind::NotFound => {
info!(
"Snapshot metadata not found for snapshot_id={}, returning None",
snapshot_id
);
return Ok(None);
}
Err(e) => return Err(e),
};
let snapshot_data_file = snapshot_data_file(&snapshot_id_dir);
let res = match File::open(&snapshot_data_file).await {
Ok(file) => file,
Err(e) if e.kind() == ErrorKind::NotFound => {
info!(
"Snapshot file not found for snapshot_id={}, returning None",
snapshot_id
);
return Ok(None);
}
Err(e) => return Err(e),
};
Ok(Some(Snapshot {
meta: snapshot_meta,
snapshot: res,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::raft::types::TypeConfig;
use openraft::Membership;
use openraft::SnapshotMeta;
use openraft::StoredMembership;
use tempfile::tempdir;
async fn create_test_snapshot(snapshot_dir: &PathBuf, snapshot_id: &str) {
let last_snapshot_id_file = snapshot_last_snapshot_id_file(snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, snapshot_id)
.await
.unwrap();
let snapshot_id_dir = snapshot_id_dir(snapshot_dir, snapshot_id);
tokio::fs::create_dir_all(&snapshot_id_dir).await.unwrap();
let meta = SnapshotMeta::<TypeConfig> {
snapshot_id: snapshot_id.to_string(),
..Default::default()
};
save_snapshot_meta(&snapshot_id_dir, meta).await.unwrap();
let data_file = snapshot_data_file(&snapshot_id_dir);
tokio::fs::File::create(&data_file).await.unwrap();
}
#[tokio::test]
async fn test_get_current_snapshot_success() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
create_test_snapshot(&snapshot_dir, "test_snapshot_001").await;
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
let snapshot = result.unwrap();
assert!(snapshot.is_some());
let snapshot = snapshot.unwrap();
assert_eq!(snapshot.meta.snapshot_id, "test_snapshot_001");
}
#[tokio::test]
async fn test_get_current_snapshot_no_directory() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().join("nonexistent_snapshots");
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_get_current_snapshot_no_last_snapshot_id_file() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
tokio::fs::create_dir_all(&snapshot_dir).await.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_get_current_snapshot_no_meta_file() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
let snapshot_id = "test_snapshot_002";
let last_snapshot_id_file = snapshot_last_snapshot_id_file(&snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, snapshot_id)
.await
.unwrap();
let snapshot_id_dir = snapshot_id_dir(&snapshot_dir, snapshot_id);
tokio::fs::create_dir_all(&snapshot_id_dir).await.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_get_current_snapshot_no_data_file() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
let snapshot_id = "test_snapshot_003";
let last_snapshot_id_file = snapshot_last_snapshot_id_file(&snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, snapshot_id)
.await
.unwrap();
let snapshot_id_dir = snapshot_id_dir(&snapshot_dir, snapshot_id);
tokio::fs::create_dir_all(&snapshot_id_dir).await.unwrap();
let meta = SnapshotMeta::<TypeConfig>::default();
save_snapshot_meta(&snapshot_id_dir, meta).await.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_get_current_snapshot_corrupted_meta() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
let snapshot_id = "test_snapshot_004";
let last_snapshot_id_file = snapshot_last_snapshot_id_file(&snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, snapshot_id)
.await
.unwrap();
let snapshot_id_dir = snapshot_id_dir(&snapshot_dir, snapshot_id);
tokio::fs::create_dir_all(&snapshot_id_dir).await.unwrap();
let meta_file = snapshot_meta_file(&snapshot_id_dir);
tokio::fs::write(&meta_file, b"invalid corrupted data")
.await
.unwrap();
let data_file = snapshot_data_file(&snapshot_id_dir);
tokio::fs::File::create(&data_file).await.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidData);
}
#[tokio::test]
async fn test_get_current_snapshot_empty_snapshot_id() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
let last_snapshot_id_file = snapshot_last_snapshot_id_file(&snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, "").await.unwrap();
let snapshot_id_dir = snapshot_id_dir(&snapshot_dir, "");
tokio::fs::create_dir_all(&snapshot_id_dir).await.unwrap();
let meta = SnapshotMeta::<TypeConfig>::default();
save_snapshot_meta(&snapshot_id_dir, meta).await.unwrap();
let data_file = snapshot_data_file(&snapshot_id_dir);
tokio::fs::File::create(&data_file).await.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
let snapshot = result.unwrap();
assert!(snapshot.is_some());
let snapshot = snapshot.unwrap();
assert_eq!(snapshot.meta.snapshot_id, "");
}
#[tokio::test]
async fn test_get_current_snapshot_file_handle_valid() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
create_test_snapshot(&snapshot_dir, "test_snapshot_005").await;
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
let snapshot = result.unwrap();
assert!(snapshot.is_some());
let snapshot = snapshot.unwrap();
let metadata = snapshot.snapshot.metadata().await;
assert!(metadata.is_ok());
let file_metadata = metadata.unwrap();
assert!(file_metadata.is_file());
}
#[tokio::test]
async fn test_get_current_snapshot_multiple_snapshots() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
create_test_snapshot(&snapshot_dir, "snapshot_001").await;
create_test_snapshot(&snapshot_dir, "snapshot_002").await;
create_test_snapshot(&snapshot_dir, "snapshot_003").await;
let last_snapshot_id_file = snapshot_last_snapshot_id_file(&snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, "snapshot_002")
.await
.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
let snapshot = result.unwrap();
assert!(snapshot.is_some());
let snapshot = snapshot.unwrap();
assert_eq!(snapshot.meta.snapshot_id, "snapshot_002");
}
#[tokio::test]
async fn test_get_current_snapshot_metadata_integrity() {
let temp_dir = tempdir().unwrap();
let snapshot_dir = temp_dir.path().to_path_buf();
let snapshot_id = "test_snapshot_006";
let last_snapshot_id_file = snapshot_last_snapshot_id_file(&snapshot_dir);
tokio::fs::write(&last_snapshot_id_file, snapshot_id)
.await
.unwrap();
let snapshot_id_dir = snapshot_id_dir(&snapshot_dir, snapshot_id);
tokio::fs::create_dir_all(&snapshot_id_dir).await.unwrap();
let membership = Membership::<TypeConfig>::default();
let stored_membership = StoredMembership::new(None, membership);
let meta = SnapshotMeta::<TypeConfig> {
snapshot_id: snapshot_id.to_string(),
last_membership: stored_membership.clone(),
last_log_id: None,
};
save_snapshot_meta(&snapshot_id_dir, meta.clone())
.await
.unwrap();
let data_file = snapshot_data_file(&snapshot_id_dir);
tokio::fs::File::create(&data_file).await.unwrap();
let result = get_current_snapshot(&snapshot_dir).await;
assert!(result.is_ok());
let snapshot = result.unwrap();
assert!(snapshot.is_some());
let loaded_snapshot = snapshot.unwrap();
assert_eq!(loaded_snapshot.meta.snapshot_id, snapshot_id);
assert_eq!(loaded_snapshot.meta.last_membership, stored_membership);
assert_eq!(loaded_snapshot.meta.last_log_id, None);
}
}