use super::query_error;
use std::path::{Path, PathBuf};
use super::execute::journal_workspace;
use super::state::{MigrationLock, MigrationState, Phase, SwitchState};
#[path = "switchover/live.rs"]
mod live;
pub(crate) use live::{
finalize_staged_live_switch, rollback_staged_live_switch, stage_live_switch,
};
pub const ARCHIVE_SUFFIX: &str = ".archive";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwitchOutcome {
pub activated: PathBuf,
pub archive: PathBuf,
}
pub fn switch_over(store: &Path, destination: &Path) -> Result<SwitchOutcome, crate::MemoryError> {
run_switch(store, destination, true, false)
}
pub(crate) fn commit_retained_switch(
store: &Path,
destination: &Path,
) -> Result<SwitchOutcome, crate::MemoryError> {
run_switch(store, destination, false, true)
}
fn run_switch(
store: &Path,
destination: &Path,
verify_open: bool,
allow_completed: bool,
) -> Result<SwitchOutcome, crate::MemoryError> {
let workspace = journal_workspace(destination)?;
let lock = MigrationLock::acquire(&workspace, "migrate-switch").map_err(query_error)?;
let result = switch_locked(
store,
destination,
&workspace,
&lock,
verify_open,
allow_completed,
);
super::execute::reconcile(result, lock.release())
}
fn switch_locked(
store: &Path,
destination: &Path,
workspace: &Path,
lock: &MigrationLock,
verify_open: bool,
allow_completed: bool,
) -> Result<SwitchOutcome, crate::MemoryError> {
let mut state = entry_state(workspace, allow_completed)?;
let slots = Slots::resolve(store, &state, destination)?;
loop {
match state.phase {
Phase::Prepared => {
return Err(query_error(
"the destination has not been validated; validate it first \
— the switch moves stores and must not be the step that \
discovers a bad rebuild",
));
}
Phase::DestinationValidated => step_archive(&slots, &mut state, workspace, lock)?,
Phase::SourceArchived => step_activate(&slots, &mut state, workspace, lock)?,
Phase::DestinationActivated => {
step_commit(&slots, &mut state, workspace, lock, verify_open)?;
}
Phase::Committed => {
return Ok(outcome(&slots));
}
}
}
}
fn outcome(slots: &Slots) -> SwitchOutcome {
SwitchOutcome {
activated: slots.source.clone(),
archive: slots.archive.clone(),
}
}
fn entry_state(
workspace: &Path,
allow_completed: bool,
) -> Result<MigrationState, crate::MemoryError> {
let state = MigrationState::read(workspace)
.map_err(query_error)?
.ok_or_else(|| {
query_error(format!(
"no migration journal at {}; there is nothing to switch",
workspace.display()
))
})?;
if state.phase == Phase::Committed && !allow_completed {
return Err(query_error(
"this migration is complete; there is nothing left to switch, and \
replaying a step would act on a store that is already the new one",
));
}
Ok(state)
}
struct Slots {
source: PathBuf,
archive: PathBuf,
destination: PathBuf,
}
impl Slots {
fn resolve(
store: &Path,
state: &MigrationState,
destination: &Path,
) -> Result<Self, crate::MemoryError> {
let source = canonical_slot(store)?;
if source != state.source_path {
return Err(query_error(format!(
"this journal describes a migration of '{}', and the request \
names '{}'; a switch cannot be transferred between stores",
state.source_path.display(),
source.display()
)));
}
let name = source
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
query_error(format!(
"the source {} has no usable directory name to derive the \
archive slot from",
source.display()
))
})?;
Ok(Self {
archive: source.with_file_name(format!("{name}{ARCHIVE_SUFFIX}")),
destination: canonical_slot(destination)?,
source,
})
}
fn on_disk(&self) -> SwitchState {
SwitchState {
source: self.source.exists(),
archive: self.archive.exists(),
destination: self.destination.exists(),
}
}
}
fn step_archive(
slots: &Slots,
state: &mut MigrationState,
workspace: &Path,
lock: &MigrationLock,
) -> Result<(), crate::MemoryError> {
archive_source(slots, state)?;
advance(state, Phase::SourceArchived, workspace, lock)
}
fn archive_source(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
match slots.on_disk() {
SwitchState {
source: true,
archive: false,
destination: true,
} => {
require_journalled_fingerprint(&slots.source, state, "source")?;
rename_durably(&slots.source, &slots.archive)?;
}
SwitchState {
source: false,
archive: true,
destination: true,
} => {
require_journalled_fingerprint(&slots.archive, state, "archive")?;
}
SwitchState {
source: true,
archive: true,
..
} => {
return Err(query_error(format!(
"the archive slot {} is already occupied; renaming the source \
over it would destroy whatever it holds — move it aside \
deliberately, or remove it if it is yours to remove",
slots.archive.display()
)));
}
other => return Err(unrecognised_disk(other, Phase::DestinationValidated)),
}
Ok(())
}
fn step_activate(
slots: &Slots,
state: &mut MigrationState,
workspace: &Path,
lock: &MigrationLock,
) -> Result<(), crate::MemoryError> {
activate_destination(slots, state)?;
advance(state, Phase::DestinationActivated, workspace, lock)
}
fn activate_destination(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
match slots.on_disk() {
SwitchState {
source: false,
archive: true,
destination: true,
} => {
require_journalled_fingerprint(&slots.archive, state, "archive")?;
rename_durably(&slots.destination, &slots.source)?;
}
SwitchState {
source: true,
archive: true,
destination: false,
} => late_activation(slots, state)?,
SwitchState {
source: true,
archive: false,
destination: true,
} => redo_after_manual_restore(slots, state)?,
other => return Err(unrecognised_disk(other, Phase::SourceArchived)),
}
Ok(())
}
fn late_activation(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
require_target_stamp(slots, state)?;
require_journalled_fingerprint(&slots.archive, state, "archive")
}
fn redo_after_manual_restore(
slots: &Slots,
state: &MigrationState,
) -> Result<(), crate::MemoryError> {
require_journalled_fingerprint(&slots.source, state, "restored source")?;
rename_durably(&slots.source, &slots.archive)?;
rename_durably(&slots.destination, &slots.source)
}
fn step_commit(
slots: &Slots,
state: &mut MigrationState,
workspace: &Path,
lock: &MigrationLock,
verify_open: bool,
) -> Result<(), crate::MemoryError> {
require_target_stamp(slots, state)?;
if verify_open {
let _opens = velesdb_core::Database::open(&slots.source)?;
}
if slots.archive.exists() {
require_journalled_fingerprint(&slots.archive, state, "archive")?;
std::fs::remove_dir_all(&slots.archive).map_err(|err| {
query_error(format!(
"the activated store is verified but the archive {} could not \
be freed: {err}; nothing is lost — re-run the switch",
slots.archive.display()
))
})?;
}
advance(state, Phase::Committed, workspace, lock)
}
fn require_journalled_fingerprint(
path: &Path,
state: &MigrationState,
role: &str,
) -> Result<(), crate::MemoryError> {
let observed = super::filesystem::fingerprint(path)?;
if observed == state.source_fingerprint {
return Ok(());
}
Err(query_error(format!(
"the {role} at {} no longer fingerprints as the store this journal \
describes — something wrote to it after the journal was written. \
Nothing was moved or deleted; a store that changed hands must be \
inspected, not migrated on a stale journal",
path.display(),
)))
}
fn require_target_stamp(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
let stamped = crate::embedding_provenance::read(&slots.source)
.map_err(query_error)?
.filter(|stamp| {
stamp.model == state.target_model && stamp.dimension == state.target_dimension
});
if stamped.is_some() {
return Ok(());
}
Err(query_error(format!(
"what occupies {} does not carry the target's provenance stamp \
('{}', {} dimensions), so it cannot be assumed to be the activated \
destination; the archive and the destination are left untouched — \
inspect {} by hand",
slots.source.display(),
state.target_model,
state.target_dimension,
slots.source.display(),
)))
}
fn advance(
state: &mut MigrationState,
phase: Phase,
workspace: &Path,
lock: &MigrationLock,
) -> Result<(), crate::MemoryError> {
state.phase = phase;
state.write(workspace, lock).map_err(query_error)
}
fn unrecognised_disk(observed: SwitchState, at: Phase) -> crate::MemoryError {
let recovery = observed.recovery();
query_error(format!(
"the journal stands at {at:?} but the disk does not match any step of \
this migration (source: {}, archive: {}, destination: {}). The \
recovery table says: {recovery:?}",
observed.source, observed.archive, observed.destination,
))
}
fn canonical_slot(path: &Path) -> Result<PathBuf, crate::MemoryError> {
let name = path
.file_name()
.ok_or_else(|| query_error(format!("{} has no final path component", path.display())))?;
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty());
let base = match parent {
Some(parent) => parent
.canonicalize()
.map_err(|err| query_error(format!("cannot resolve {}: {err}", parent.display())))?,
None => std::env::current_dir()
.map_err(|err| query_error(format!("cannot resolve the working directory: {err}")))?,
};
Ok(base.join(name))
}
fn rename_durably(from: &Path, to: &Path) -> Result<(), crate::MemoryError> {
std::fs::rename(from, to).map_err(|err| {
query_error(format!(
"cannot rename {} to {}: {err}",
from.display(),
to.display()
))
})?;
if let Some(parent) = to.parent() {
let directory = std::fs::File::open(parent).map_err(|err| {
query_error(format!(
"cannot open {} to sync it: {err}",
parent.display()
))
})?;
directory.sync_all().map_err(|err| {
query_error(format!(
"the rename of {} is visible but not yet durable: {err}; do \
not power off before re-running",
to.display()
))
})?;
}
Ok(())
}