use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use super::PalaceRegistry;
use crate::memory_core::palace::PalaceId;
use crate::memory_core::palace_emptiness::{
LegacyProbe, PalaceNotEmpty, check_palace_empty, conservative_legacy_probe,
};
use crate::memory_core::store::palace_format::mirror_to_preserve;
use crate::memory_core::store::palace_store::PalaceStore;
use crate::palace_alias::{AliasChange, AliasUndo, PalaceAliasStore, try_alias_target_if_absent};
pub const DEFAULT_RENAME_BUSY_WAIT: Duration = Duration::from_secs(2);
const RELEASE_POLL: Duration = Duration::from_millis(20);
const TRASH_DIR: &str = ".trash";
pub struct RenameOptions<'a> {
pub replace_empty: bool,
pub legacy_probe: &'a LegacyProbe,
pub busy_wait: Duration,
}
impl Default for RenameOptions<'static> {
fn default() -> Self {
Self {
replace_empty: false,
legacy_probe: &conservative_legacy_probe,
busy_wait: DEFAULT_RENAME_BUSY_WAIT,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameOutcome {
pub old: PalaceId,
pub new: PalaceId,
pub trashed_target: Option<PathBuf>,
pub resumed: bool,
pub alias_changes: Vec<AliasChange>,
pub name_rewritten: bool,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PalaceRenameError {
#[error("palace {0:?} not found")]
NotFound(String),
#[error("palace id {alias:?} is an alias of palace {target:?}; rename {target:?} instead")]
SourceIsAlias {
alias: String,
target: String,
},
#[error("cannot rename to {new:?}: {reason}")]
InvalidTarget {
new: String,
reason: String,
},
#[error("{new:?} is an alias of palace {target:?}; remove that alias first")]
TargetIsAlias {
new: String,
target: String,
},
#[error("palace {new:?} already exists (it is empty; pass replace_empty to replace it)")]
TargetExists {
new: String,
},
#[error("palace {new:?} already exists and is not empty: {reason}")]
TargetNotEmpty {
new: String,
#[source]
reason: PalaceNotEmpty,
},
#[error("palace {palace:?} is busy ({detail}); retry when it is idle")]
Busy {
palace: String,
detail: String,
},
#[error("{context}: {source}")]
Io {
context: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
}
impl PalaceRenameError {
pub fn is_conflict(&self) -> bool {
matches!(
self,
Self::SourceIsAlias { .. }
| Self::InvalidTarget { .. }
| Self::TargetIsAlias { .. }
| Self::TargetExists { .. }
| Self::TargetNotEmpty { .. }
)
}
fn io(
context: impl Into<String>,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Self {
Self::Io {
context: context.into(),
source: source.into(),
}
}
}
type RenameResult<T> = Result<T, PalaceRenameError>;
type Mover<'a> = &'a dyn Fn(&Path, &Path) -> std::io::Result<()>;
impl PalaceRegistry {
pub fn rename_palace(
&self,
data_root: &Path,
old: &str,
new: &str,
opts: &RenameOptions<'_>,
) -> RenameResult<RenameOutcome> {
self.rename_palace_with(data_root, old, new, opts, &|from, to| {
std::fs::rename(from, to)
})
}
pub(crate) fn rename_palace_with(
&self,
data_root: &Path,
old: &str,
new: &str,
opts: &RenameOptions<'_>,
mover: Mover<'_>,
) -> RenameResult<RenameOutcome> {
let (old, new) = (old.trim(), new.trim());
if old == new {
return Err(invalid(new, "it is the palace's current id"));
}
if !crate::palace_id_is_valid(new) {
return Err(invalid(
new,
"not a valid palace id ([a-z0-9][a-z0-9-]{0,62})",
));
}
let mut keys = vec![canonical(data_root, old)?, canonical(data_root, new)?];
keys.sort_unstable();
keys.dedup();
let mutexes: Vec<Arc<Mutex<()>>> = keys
.iter()
.map(|k| {
self.open_locks
.entry(PalaceId::new(k.clone()))
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
})
.collect();
let mut guards = Vec::with_capacity(mutexes.len());
for (key, mutex) in keys.iter().zip(&mutexes) {
guards.push(
mutex
.try_lock_for(opts.busy_wait)
.ok_or_else(|| busy(key, "its open-lock is held"))?,
);
}
let mut cached = keys.clone();
cached.extend([old.to_string(), new.to_string()]);
cached.sort_unstable();
cached.dedup();
self.release_all(&cached, opts.busy_wait)?;
let outcome = self.rename_locked(data_root, old, new, opts, mover)?;
let old_id = PalaceId::new(old);
self.gaps_cache.remove(&old_id);
self.unopenable.remove(&old_id);
Ok(outcome)
}
fn release_all(&self, keys: &[String], wait: Duration) -> RenameResult<()> {
let deadline = Instant::now() + wait;
for key in keys {
let id = PalaceId::new(key.clone());
while self.handles.lock().contains(&id) && !self.release_if_unreferenced(&id) {
if Instant::now() >= deadline {
return Err(busy(key, "an open handle is still in use"));
}
std::thread::sleep(RELEASE_POLL);
}
}
Ok(())
}
fn rename_locked(
&self,
data_root: &Path,
old: &str,
new: &str,
opts: &RenameOptions<'_>,
mover: Mover<'_>,
) -> RenameResult<RenameOutcome> {
let (old_dir, new_dir) = (data_root.join(old), data_root.join(new));
let aliased_to_new = PalaceAliasStore::load_aliases(data_root)
.map_err(|e| PalaceRenameError::io("read the palace alias map", e))?
.get(old)
.is_some_and(|t| t == new);
let mut outcome = RenameOutcome {
old: PalaceId::new(old),
new: PalaceId::new(new),
trashed_target: None,
resumed: false,
alias_changes: Vec::new(),
name_rewritten: false,
};
if !present(&old_dir)? {
if aliased_to_new && present(&new_dir)? && palace_id_at(&new_dir)? == old {
outcome.resumed = true;
outcome.name_rewritten = rewrite_palace_json(&new_dir, old, new)?;
return Ok(outcome);
}
return Err(match strict_alias(data_root, old)? {
Some(target) => PalaceRenameError::SourceIsAlias {
alias: old.to_string(),
target,
},
None => PalaceRenameError::NotFound(old.to_string()),
});
}
outcome.resumed = aliased_to_new;
mirror_to_preserve(&old_dir, old)
.map_err(|e| PalaceRenameError::io(format!("rename palace {old:?}"), e))?;
let replace = if present(&new_dir)? {
self.check_target(&new_dir, new, opts)?;
true
} else {
match strict_alias(data_root, new)? {
Some(target) if target != old => {
return Err(PalaceRenameError::TargetIsAlias {
new: new.to_string(),
target,
});
}
_ => false,
}
};
let undo = PalaceAliasStore::rename_target_with_undo(data_root, old, new)
.map_err(|e| PalaceRenameError::io("write the palace alias map", e))?;
if replace {
let trash = trash_path(data_root, new);
let moved = trash
.parent()
.map_or(Ok(()), std::fs::create_dir_all)
.and_then(|()| mover(&new_dir, &trash));
if let Err(e) = moved {
let rolled = rollback(data_root, &undo, None, mover);
return Err(PalaceRenameError::io(
format!(
"move empty target {} to the trash; {rolled}",
new_dir.display()
),
e,
));
}
outcome.trashed_target = Some(trash);
}
if let Err(e) = mover(&old_dir, &new_dir) {
let restore = outcome
.trashed_target
.as_deref()
.map(|t| (t, new_dir.as_path()));
let rolled = rollback(data_root, &undo, restore, mover);
return Err(PalaceRenameError::io(
format!(
"move {} to {}; {rolled}",
old_dir.display(),
new_dir.display()
),
e,
));
}
outcome.alias_changes = undo.changes;
outcome.name_rewritten = rewrite_palace_json(&new_dir, old, new)?;
Ok(outcome)
}
fn check_target(
&self,
new_dir: &Path,
new: &str,
opts: &RenameOptions<'_>,
) -> RenameResult<()> {
let not_empty = |reason| PalaceRenameError::TargetNotEmpty {
new: new.to_string(),
reason,
};
let palace = PalaceStore::load_palace(new_dir)
.map_err(|e| not_empty(PalaceNotEmpty::Unconfirmed(format!("{e:#}"))))?;
let handle = self
.open_handle(&palace)
.map_err(|e| not_empty(PalaceNotEmpty::Unconfirmed(format!("{e:#}"))))?;
let verdict = check_palace_empty(&handle, new_dir, opts.legacy_probe);
drop(handle);
verdict.map_err(not_empty)?;
if !opts.replace_empty {
return Err(PalaceRenameError::TargetExists {
new: new.to_string(),
});
}
Ok(())
}
}
fn invalid(new: &str, reason: &str) -> PalaceRenameError {
PalaceRenameError::InvalidTarget {
new: new.to_string(),
reason: reason.to_string(),
}
}
fn busy(palace: &str, detail: &str) -> PalaceRenameError {
PalaceRenameError::Busy {
palace: palace.to_string(),
detail: detail.to_string(),
}
}
fn strict_alias(data_root: &Path, id: &str) -> RenameResult<Option<String>> {
try_alias_target_if_absent(data_root, id)
.map_err(|e| PalaceRenameError::io(format!("resolve palace id {id:?}"), e))
}
fn canonical(data_root: &Path, id: &str) -> RenameResult<String> {
Ok(strict_alias(data_root, id)?.unwrap_or_else(|| id.to_string()))
}
fn present(dir: &Path) -> RenameResult<bool> {
PalaceStore::metadata_present(dir)
.map_err(|e| PalaceRenameError::io(format!("probe {}", dir.display()), e))
}
fn palace_id_at(dir: &Path) -> RenameResult<String> {
PalaceStore::load_palace(dir)
.map(|p| p.id.0)
.map_err(|e| PalaceRenameError::io(format!("read {}/palace.json", dir.display()), e))
}
fn rewrite_palace_json(new_dir: &Path, old: &str, new: &str) -> RenameResult<bool> {
let fail = |e| {
PalaceRenameError::io(
format!(
"rewrite {}/palace.json (re-run the rename to finish it)",
new_dir.display()
),
e,
)
};
let mut palace = PalaceStore::load_palace(new_dir).map_err(fail)?;
let name_rewritten = palace.name == old;
palace.id = PalaceId::new(new);
if name_rewritten {
palace.name = new.to_string();
}
palace.data_dir = new_dir.to_path_buf();
PalaceStore::save_palace(&palace).map_err(fail)?;
Ok(name_rewritten)
}
fn trash_path(data_root: &Path, new: &str) -> PathBuf {
let stamp = chrono::Utc::now().format("%Y%m%dT%H%M%S%.3fZ");
let base = data_root
.join(TRASH_DIR)
.join(format!("{new}-replaced-{stamp}"));
let mut candidate = base.clone();
let mut n = 1u32;
while !matches!(candidate.try_exists(), Ok(false)) && n < 1000 {
candidate = PathBuf::from(format!("{}-{n}", base.display()));
n += 1;
}
candidate
}
fn rollback(
data_root: &Path,
undo: &AliasUndo,
restore: Option<(&Path, &Path)>,
mover: Mover<'_>,
) -> String {
let mut parts = Vec::new();
if let Some((trash, target)) = restore {
match mover(trash, target) {
Ok(()) => parts.push("the replaced target was restored".to_string()),
Err(e) => parts.push(format!(
"the replaced target stays at {} ({e})",
trash.display()
)),
}
}
match PalaceAliasStore::undo(data_root, undo) {
Ok(skipped) if skipped.is_empty() => {
parts.push("the alias keys were rolled back".to_string())
}
Ok(skipped) => parts.push(format!(
"the alias keys were rolled back except {skipped:?}, which another writer changed"
)),
Err(e) => parts.push(format!("the alias rollback failed ({e:#})")),
}
parts.join("; ")
}
#[cfg(test)]
#[path = "registry_rename_tests.rs"]
mod tests;