#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Phase {
Prepared,
DestinationValidated,
SourceArchived,
DestinationActivated,
Committed,
}
pub const PHASES: &[Phase] = &[
Phase::Prepared,
Phase::DestinationValidated,
Phase::SourceArchived,
Phase::DestinationActivated,
Phase::Committed,
];
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "recovery", rename_all = "snake_case")]
pub enum Recovery {
Continue {
next: Phase,
rationale: String,
},
Restore {
action: String,
},
Refuse {
reason: String,
},
}
impl Phase {
pub(super) fn may_follow(self, previous: Self) -> bool {
match previous {
Self::Prepared => matches!(self, Self::Prepared | Self::DestinationValidated),
Self::DestinationValidated => {
matches!(self, Self::DestinationValidated | Self::SourceArchived)
}
Self::SourceArchived => {
matches!(self, Self::SourceArchived | Self::DestinationActivated)
}
Self::DestinationActivated => {
matches!(self, Self::DestinationActivated | Self::Committed)
}
Self::Committed => matches!(self, Self::Committed),
}
}
#[must_use]
pub fn recovery(self) -> Recovery {
match self {
Self::Prepared => Recovery::Continue {
next: Self::DestinationValidated,
rationale: "nothing has been moved: the destination is built from the source, \
which is still in place and still authoritative."
.to_owned(),
},
Self::DestinationValidated => Recovery::Continue {
next: Self::SourceArchived,
rationale: "the destination is built and checked, and the source is untouched. \
The next step is the first that moves anything."
.to_owned(),
},
Self::SourceArchived => Recovery::Restore {
action: "move the archive back to the source name. The destination was never \
activated, so the source is the only authority and putting it back is \
the whole recovery."
.to_owned(),
},
Self::DestinationActivated => Recovery::Continue {
next: Self::Committed,
rationale: "the destination is in place and the archive still exists. Going \
forward releases the archive; going back would discard a destination \
that is already the live store."
.to_owned(),
},
Self::Committed => Recovery::Refuse {
reason: "the migration finished. There is nothing to resume, and re-running any \
step would act on a store that is already the new one."
.to_owned(),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SwitchState {
pub source: bool,
pub archive: bool,
pub destination: bool,
}
const SWITCH_NOTHING: &str =
"neither the source, the archive nor the destination exists. Whatever happened here is not recoverable from the filesystem, and inventing a starting point would be inventing data.";
const SWITCH_ORPHAN_DESTINATION: &str =
"only the destination exists. The source is gone and so is the archive, so nothing on disk says whether the destination is a completed migration or an abandoned one. Renaming it into place would be a guess.";
const SWITCH_ARCHIVE_ONLY: &str =
"move the archive back to the source name. It is the only copy of the data, and no destination was ever put in its place.";
const SWITCH_MID_RENAME: &str =
"move the archive back to the source name, leaving the destination where it is. The switch stopped between its two renames; the source is intact under the archive name and is still the authority.";
const SWITCH_UNTOUCHED: &str =
"only the source exists, exactly as before any migration. Start over from the beginning; nothing needs undoing.";
const SWITCH_BUILT_NOT_SWITCHED: &str =
"the source is in place and a destination exists beside it. Nothing has been moved, so the destination can be validated against the source before anything is.";
const SWITCH_TWO_AUTHORITIES: &str =
"a source and an archive both exist and there is no destination. Two directories claim to hold the data and nothing distinguishes a half-finished restore from a half-finished archive. Deleting or renaming either would destroy the one that turns out to be current.";
const SWITCH_IMPOSSIBLE: &str =
"the source, the archive and the destination all exist. No sequence of this migration produces all three at once, so the directory has been touched by something else. Nothing here may be removed or renamed automatically.";
impl SwitchState {
#[must_use]
pub fn all() -> Vec<Self> {
let mut out = Vec::with_capacity(8);
for source in [false, true] {
for archive in [false, true] {
for destination in [false, true] {
out.push(Self {
source,
archive,
destination,
});
}
}
}
out
}
#[must_use]
pub fn recovery(self) -> Recovery {
let refuse = |reason: &str| Recovery::Refuse {
reason: reason.to_owned(),
};
let restore = |action: &str| Recovery::Restore {
action: action.to_owned(),
};
let go = |next: Phase, rationale: &str| Recovery::Continue {
next,
rationale: rationale.to_owned(),
};
match (self.source, self.archive, self.destination) {
(false, false, false) => refuse(SWITCH_NOTHING),
(false, false, true) => refuse(SWITCH_ORPHAN_DESTINATION),
(false, true, false) => restore(SWITCH_ARCHIVE_ONLY),
(false, true, true) => restore(SWITCH_MID_RENAME),
(true, false, false) => go(Phase::Prepared, SWITCH_UNTOUCHED),
(true, false, true) => go(Phase::DestinationValidated, SWITCH_BUILT_NOT_SWITCHED),
(true, true, false) => refuse(SWITCH_TWO_AUTHORITIES),
(true, true, true) => refuse(SWITCH_IMPOSSIBLE),
}
}
}