use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
fs, io,
path::PathBuf,
};
use wdev::Device;
use wkv::{
RangeIndexChunkedSerializer, RangeIndexError, RangeIndexManager, RangeIndexMigrationReader,
StorageBackendType, StoreSession,
};
#[derive(Debug, thiserror::Error)]
pub enum MigrationError {
#[error("ERR {0}")]
Invalid(String),
#[error(transparent)]
Range(#[from] RangeIndexError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Store(#[from] wkv::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishMigratedIndexResult {
Success,
SkippedAlreadyExists,
SkippedReplaceNotSupported,
Failed,
}
impl Display for PublishMigratedIndexResult {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Success => write!(f, "Success"),
Self::SkippedAlreadyExists => write!(f, "SkippedAlreadyExists"),
Self::SkippedReplaceNotSupported => write!(f, "SkippedReplaceNotSupported"),
Self::Failed => write!(f, "Failed"),
}
}
}
pub const DEFAULT_MIGRATION_CHUNK_SIZE: usize = 256 * 1024;
pub const DEFAULT_FILE_READ_BUFFER_SIZE: usize = 1 << 20;
pub struct RangeIndexManager_Migration;
impl RangeIndexManager_Migration {
pub async fn get_range_index_keys_for_migration<D: Device>(
session: &StoreSession<D>,
keys: &[Vec<u8>],
) -> Result<HashSet<Vec<u8>>, MigrationError> {
let mut range_index_keys = HashSet::new();
for key in keys {
match session.load_range_index_stub(key).await {
Ok(Some(_)) => {
range_index_keys.insert(key.clone());
}
Ok(None) | Err(RangeIndexError::NotFound | RangeIndexError::WrongType) => {
}
Err(e) => return Err(e.into()),
}
}
Ok(range_index_keys)
}
pub async fn snapshot_range_index_and_create_reader<D: Device>(
session: &StoreSession<D>,
key: &[u8],
) -> Result<RangeIndexMigrationReader<fs::File>, MigrationError> {
let (_, stub) = session
.load_range_index_stub(key)
.await?
.ok_or(RangeIndexError::NotFound)?;
let engine = &session.store.range_index;
if stub.storage_backend == StorageBackendType::Memory.to_u8() {
return Err(MigrationError::Invalid(
"SnapshotForMigration: memory-only trees cannot be migrated".to_string(),
));
}
let stub_bytes = stub.encode();
let migration_path = engine.derive_temp_migration_path();
let key_hash = RangeIndexManager::key_hash_of(key);
let key_id = RangeIndexManager::key_id_of(key);
let _xlock = engine.locks().write(key_hash);
match engine.get_tree(key) {
Some(tree) => {
let entry = engine
.live_indexes()
.pin()
.get(&key_id)
.cloned()
.ok_or_else(|| MigrationError::Invalid("live entry vanished".to_string()))?;
entry
.snapshot_under_claim(&tree, &migration_path)
.map_err(|e| MigrationError::Invalid(e.to_string()))?;
}
None => {
let data_path = engine.data_file_path_for_key(key);
if !data_path.exists() {
return Err(MigrationError::Invalid(format!(
"SnapshotForMigration: data.bftree not found: {}",
data_path.display()
)));
}
fs::copy(&data_path, &migration_path)?;
}
}
drop(_xlock);
let total_bytes = fs::metadata(&migration_path)?.len();
log::info!(
"SnapshotForMigration: snapshot file {}, size {total_bytes} bytes",
migration_path.display()
);
let serializer = RangeIndexChunkedSerializer::new(key, &stub_bytes, total_bytes);
let file = fs::File::open(&migration_path)?;
let reader = RangeIndexMigrationReader::new(
serializer,
file,
Some(migration_path),
DEFAULT_FILE_READ_BUFFER_SIZE,
)
.map_err(|e| MigrationError::Invalid(e.to_string()))?;
Ok(reader)
}
pub async fn key_exists<D: Device>(
session: &StoreSession<D>,
key: &[u8],
) -> Result<bool, wkv::Error> {
if session.read(key).await?.is_some() {
return Ok(true);
}
Ok(
session
.load_meta(key)
.await?
.is_some_and(|meta| meta.size > 0),
)
}
pub fn derive_temp_migration_path(engine: &RangeIndexManager) -> PathBuf {
engine.derive_temp_migration_path()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn publish_result_display_matches_csharp_names() {
assert_eq!(PublishMigratedIndexResult::Success.to_string(), "Success");
assert_eq!(
PublishMigratedIndexResult::SkippedAlreadyExists.to_string(),
"SkippedAlreadyExists"
);
assert_eq!(
PublishMigratedIndexResult::SkippedReplaceNotSupported.to_string(),
"SkippedReplaceNotSupported"
);
assert_eq!(PublishMigratedIndexResult::Failed.to_string(), "Failed");
}
#[test]
fn constants_match_csharp_values() {
assert_eq!(DEFAULT_MIGRATION_CHUNK_SIZE, 256 * 1024);
assert_eq!(DEFAULT_FILE_READ_BUFFER_SIZE, 1 << 20);
}
}