1use super::query_error;
33use std::path::{Path, PathBuf};
34
35use super::execute::journal_workspace;
36use super::state::{MigrationLock, MigrationState, Phase, SwitchState};
37
38#[path = "switchover/live.rs"]
39mod live;
40pub(crate) use live::{
41 finalize_staged_live_switch, rollback_staged_live_switch, stage_live_switch,
42};
43
44pub const ARCHIVE_SUFFIX: &str = ".archive";
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SwitchOutcome {
50 pub activated: PathBuf,
52 pub archive: PathBuf,
54}
55
56pub fn switch_over(store: &Path, destination: &Path) -> Result<SwitchOutcome, crate::MemoryError> {
67 run_switch(store, destination, true, false)
68}
69
70pub(crate) fn commit_retained_switch(
71 store: &Path,
72 destination: &Path,
73) -> Result<SwitchOutcome, crate::MemoryError> {
74 run_switch(store, destination, false, true)
75}
76
77fn run_switch(
78 store: &Path,
79 destination: &Path,
80 verify_open: bool,
81 allow_completed: bool,
82) -> Result<SwitchOutcome, crate::MemoryError> {
83 let workspace = journal_workspace(destination)?;
84 let lock = MigrationLock::acquire(&workspace, "migrate-switch").map_err(query_error)?;
85 let result = switch_locked(
86 store,
87 destination,
88 &workspace,
89 &lock,
90 verify_open,
91 allow_completed,
92 );
93 super::execute::reconcile(result, lock.release())
94}
95
96fn switch_locked(
97 store: &Path,
98 destination: &Path,
99 workspace: &Path,
100 lock: &MigrationLock,
101 verify_open: bool,
102 allow_completed: bool,
103) -> Result<SwitchOutcome, crate::MemoryError> {
104 let mut state = entry_state(workspace, allow_completed)?;
105 let slots = Slots::resolve(store, &state, destination)?;
106 loop {
107 match state.phase {
108 Phase::Prepared => {
109 return Err(query_error(
110 "the destination has not been validated; validate it first \
111 — the switch moves stores and must not be the step that \
112 discovers a bad rebuild",
113 ));
114 }
115 Phase::DestinationValidated => step_archive(&slots, &mut state, workspace, lock)?,
116 Phase::SourceArchived => step_activate(&slots, &mut state, workspace, lock)?,
117 Phase::DestinationActivated => {
118 step_commit(&slots, &mut state, workspace, lock, verify_open)?;
119 }
120 Phase::Committed => {
121 return Ok(outcome(&slots));
122 }
123 }
124 }
125}
126
127fn outcome(slots: &Slots) -> SwitchOutcome {
128 SwitchOutcome {
129 activated: slots.source.clone(),
130 archive: slots.archive.clone(),
131 }
132}
133
134fn entry_state(
141 workspace: &Path,
142 allow_completed: bool,
143) -> Result<MigrationState, crate::MemoryError> {
144 let state = MigrationState::read(workspace)
145 .map_err(query_error)?
146 .ok_or_else(|| {
147 query_error(format!(
148 "no migration journal at {}; there is nothing to switch",
149 workspace.display()
150 ))
151 })?;
152 if state.phase == Phase::Committed && !allow_completed {
153 return Err(query_error(
154 "this migration is complete; there is nothing left to switch, and \
155 replaying a step would act on a store that is already the new one",
156 ));
157 }
158 Ok(state)
159}
160
161struct Slots {
164 source: PathBuf,
165 archive: PathBuf,
166 destination: PathBuf,
167}
168
169impl Slots {
170 fn resolve(
171 store: &Path,
172 state: &MigrationState,
173 destination: &Path,
174 ) -> Result<Self, crate::MemoryError> {
175 let source = canonical_slot(store)?;
176 if source != state.source_path {
177 return Err(query_error(format!(
178 "this journal describes a migration of '{}', and the request \
179 names '{}'; a switch cannot be transferred between stores",
180 state.source_path.display(),
181 source.display()
182 )));
183 }
184 let name = source
185 .file_name()
186 .and_then(|name| name.to_str())
187 .ok_or_else(|| {
188 query_error(format!(
189 "the source {} has no usable directory name to derive the \
190 archive slot from",
191 source.display()
192 ))
193 })?;
194 Ok(Self {
195 archive: source.with_file_name(format!("{name}{ARCHIVE_SUFFIX}")),
196 destination: canonical_slot(destination)?,
197 source,
198 })
199 }
200
201 fn on_disk(&self) -> SwitchState {
202 SwitchState {
203 source: self.source.exists(),
204 archive: self.archive.exists(),
205 destination: self.destination.exists(),
206 }
207 }
208}
209
210fn step_archive(
212 slots: &Slots,
213 state: &mut MigrationState,
214 workspace: &Path,
215 lock: &MigrationLock,
216) -> Result<(), crate::MemoryError> {
217 archive_source(slots, state)?;
218 advance(state, Phase::SourceArchived, workspace, lock)
219}
220
221fn archive_source(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
222 match slots.on_disk() {
223 SwitchState {
229 source: true,
230 archive: false,
231 destination: true,
232 } => {
233 require_journalled_fingerprint(&slots.source, state, "source")?;
234 rename_durably(&slots.source, &slots.archive)?;
235 }
236 SwitchState {
241 source: false,
242 archive: true,
243 destination: true,
244 } => {
245 require_journalled_fingerprint(&slots.archive, state, "archive")?;
246 }
247 SwitchState {
248 source: true,
249 archive: true,
250 ..
251 } => {
252 return Err(query_error(format!(
253 "the archive slot {} is already occupied; renaming the source \
254 over it would destroy whatever it holds — move it aside \
255 deliberately, or remove it if it is yours to remove",
256 slots.archive.display()
257 )));
258 }
259 other => return Err(unrecognised_disk(other, Phase::DestinationValidated)),
260 }
261 Ok(())
262}
263
264fn step_activate(
266 slots: &Slots,
267 state: &mut MigrationState,
268 workspace: &Path,
269 lock: &MigrationLock,
270) -> Result<(), crate::MemoryError> {
271 activate_destination(slots, state)?;
272 advance(state, Phase::DestinationActivated, workspace, lock)
273}
274
275fn activate_destination(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
276 match slots.on_disk() {
277 SwitchState {
278 source: false,
279 archive: true,
280 destination: true,
281 } => {
282 require_journalled_fingerprint(&slots.archive, state, "archive")?;
283 rename_durably(&slots.destination, &slots.source)?;
284 }
285 SwitchState {
293 source: true,
294 archive: true,
295 destination: false,
296 } => late_activation(slots, state)?,
297 SwitchState {
304 source: true,
305 archive: false,
306 destination: true,
307 } => redo_after_manual_restore(slots, state)?,
308 other => return Err(unrecognised_disk(other, Phase::SourceArchived)),
309 }
310 Ok(())
311}
312
313fn late_activation(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
317 require_target_stamp(slots, state)?;
318 require_journalled_fingerprint(&slots.archive, state, "archive")
319}
320
321fn redo_after_manual_restore(
326 slots: &Slots,
327 state: &MigrationState,
328) -> Result<(), crate::MemoryError> {
329 require_journalled_fingerprint(&slots.source, state, "restored source")?;
330 rename_durably(&slots.source, &slots.archive)?;
331 rename_durably(&slots.destination, &slots.source)
332}
333
334fn step_commit(
336 slots: &Slots,
337 state: &mut MigrationState,
338 workspace: &Path,
339 lock: &MigrationLock,
340 verify_open: bool,
341) -> Result<(), crate::MemoryError> {
342 require_target_stamp(slots, state)?;
343 if verify_open {
344 let _opens = velesdb_core::Database::open(&slots.source)?;
345 }
346 if slots.archive.exists() {
347 require_journalled_fingerprint(&slots.archive, state, "archive")?;
355 std::fs::remove_dir_all(&slots.archive).map_err(|err| {
356 query_error(format!(
357 "the activated store is verified but the archive {} could not \
358 be freed: {err}; nothing is lost — re-run the switch",
359 slots.archive.display()
360 ))
361 })?;
362 }
363 advance(state, Phase::Committed, workspace, lock)
364}
365
366fn require_journalled_fingerprint(
372 path: &Path,
373 state: &MigrationState,
374 role: &str,
375) -> Result<(), crate::MemoryError> {
376 let observed = super::filesystem::fingerprint(path)?;
377 if observed == state.source_fingerprint {
378 return Ok(());
379 }
380 Err(query_error(format!(
381 "the {role} at {} no longer fingerprints as the store this journal \
382 describes — something wrote to it after the journal was written. \
383 Nothing was moved or deleted; a store that changed hands must be \
384 inspected, not migrated on a stale journal",
385 path.display(),
386 )))
387}
388
389fn require_target_stamp(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
392 let stamped = crate::embedding_provenance::read(&slots.source)
393 .map_err(query_error)?
394 .filter(|stamp| {
395 stamp.model == state.target_model && stamp.dimension == state.target_dimension
396 });
397 if stamped.is_some() {
398 return Ok(());
399 }
400 Err(query_error(format!(
401 "what occupies {} does not carry the target's provenance stamp \
402 ('{}', {} dimensions), so it cannot be assumed to be the activated \
403 destination; the archive and the destination are left untouched — \
404 inspect {} by hand",
405 slots.source.display(),
406 state.target_model,
407 state.target_dimension,
408 slots.source.display(),
409 )))
410}
411
412fn advance(
413 state: &mut MigrationState,
414 phase: Phase,
415 workspace: &Path,
416 lock: &MigrationLock,
417) -> Result<(), crate::MemoryError> {
418 state.phase = phase;
419 state.write(workspace, lock).map_err(query_error)
420}
421
422fn unrecognised_disk(observed: SwitchState, at: Phase) -> crate::MemoryError {
423 let recovery = observed.recovery();
424 query_error(format!(
425 "the journal stands at {at:?} but the disk does not match any step of \
426 this migration (source: {}, archive: {}, destination: {}). The \
427 recovery table says: {recovery:?}",
428 observed.source, observed.archive, observed.destination,
429 ))
430}
431
432fn canonical_slot(path: &Path) -> Result<PathBuf, crate::MemoryError> {
435 let name = path
436 .file_name()
437 .ok_or_else(|| query_error(format!("{} has no final path component", path.display())))?;
438 let parent = path
439 .parent()
440 .filter(|parent| !parent.as_os_str().is_empty());
441 let base = match parent {
442 Some(parent) => parent
443 .canonicalize()
444 .map_err(|err| query_error(format!("cannot resolve {}: {err}", parent.display())))?,
445 None => std::env::current_dir()
446 .map_err(|err| query_error(format!("cannot resolve the working directory: {err}")))?,
447 };
448 Ok(base.join(name))
449}
450
451fn rename_durably(from: &Path, to: &Path) -> Result<(), crate::MemoryError> {
454 std::fs::rename(from, to).map_err(|err| {
455 query_error(format!(
456 "cannot rename {} to {}: {err}",
457 from.display(),
458 to.display()
459 ))
460 })?;
461 if let Some(parent) = to.parent() {
462 let directory = std::fs::File::open(parent).map_err(|err| {
463 query_error(format!(
464 "cannot open {} to sync it: {err}",
465 parent.display()
466 ))
467 })?;
468 directory.sync_all().map_err(|err| {
469 query_error(format!(
470 "the rename of {} is visible but not yet durable: {err}; do \
471 not power off before re-running",
472 to.display()
473 ))
474 })?;
475 }
476 Ok(())
477}