velesdb_memory/migration/state/
switch.rs1#[derive(
6 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
7)]
8#[serde(rename_all = "snake_case")]
9pub enum Phase {
10 Prepared,
12 DestinationValidated,
14 SourceArchived,
16 DestinationActivated,
18 Committed,
20}
21
22pub const PHASES: &[Phase] = &[
25 Phase::Prepared,
26 Phase::DestinationValidated,
27 Phase::SourceArchived,
28 Phase::DestinationActivated,
29 Phase::Committed,
30];
31
32#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
38#[serde(tag = "recovery", rename_all = "snake_case")]
39pub enum Recovery {
40 Continue {
42 next: Phase,
44 rationale: String,
46 },
47 Restore {
50 action: String,
52 },
53 Refuse {
55 reason: String,
57 },
58}
59
60impl Phase {
61 pub(super) fn may_follow(self, previous: Self) -> bool {
67 match previous {
68 Self::Prepared => matches!(self, Self::Prepared | Self::DestinationValidated),
69 Self::DestinationValidated => {
70 matches!(self, Self::DestinationValidated | Self::SourceArchived)
71 }
72 Self::SourceArchived => {
73 matches!(self, Self::SourceArchived | Self::DestinationActivated)
74 }
75 Self::DestinationActivated => {
76 matches!(self, Self::DestinationActivated | Self::Committed)
77 }
78 Self::Committed => matches!(self, Self::Committed),
79 }
80 }
81
82 #[must_use]
88 pub fn recovery(self) -> Recovery {
89 match self {
90 Self::Prepared => Recovery::Continue {
91 next: Self::DestinationValidated,
92 rationale: "nothing has been moved: the destination is built from the source, \
93 which is still in place and still authoritative."
94 .to_owned(),
95 },
96 Self::DestinationValidated => Recovery::Continue {
97 next: Self::SourceArchived,
98 rationale: "the destination is built and checked, and the source is untouched. \
99 The next step is the first that moves anything."
100 .to_owned(),
101 },
102 Self::SourceArchived => Recovery::Restore {
103 action: "move the archive back to the source name. The destination was never \
104 activated, so the source is the only authority and putting it back is \
105 the whole recovery."
106 .to_owned(),
107 },
108 Self::DestinationActivated => Recovery::Continue {
109 next: Self::Committed,
110 rationale: "the destination is in place and the archive still exists. Going \
111 forward releases the archive; going back would discard a destination \
112 that is already the live store."
113 .to_owned(),
114 },
115 Self::Committed => Recovery::Refuse {
116 reason: "the migration finished. There is nothing to resume, and re-running any \
117 step would act on a store that is already the new one."
118 .to_owned(),
119 },
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct SwitchState {
132 pub source: bool,
134 pub archive: bool,
136 pub destination: bool,
138}
139
140const SWITCH_NOTHING: &str =
141 "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.";
142const SWITCH_ORPHAN_DESTINATION: &str =
143 "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.";
144const SWITCH_ARCHIVE_ONLY: &str =
145 "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.";
146const SWITCH_MID_RENAME: &str =
147 "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.";
148const SWITCH_UNTOUCHED: &str =
149 "only the source exists, exactly as before any migration. Start over from the beginning; nothing needs undoing.";
150const SWITCH_BUILT_NOT_SWITCHED: &str =
151 "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.";
152const SWITCH_TWO_AUTHORITIES: &str =
153 "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.";
154const SWITCH_IMPOSSIBLE: &str =
155 "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.";
156
157impl SwitchState {
158 #[must_use]
161 pub fn all() -> Vec<Self> {
162 let mut out = Vec::with_capacity(8);
163 for source in [false, true] {
164 for archive in [false, true] {
165 for destination in [false, true] {
166 out.push(Self {
167 source,
168 archive,
169 destination,
170 });
171 }
172 }
173 }
174 out
175 }
176
177 #[must_use]
183 pub fn recovery(self) -> Recovery {
184 let refuse = |reason: &str| Recovery::Refuse {
185 reason: reason.to_owned(),
186 };
187 let restore = |action: &str| Recovery::Restore {
188 action: action.to_owned(),
189 };
190 let go = |next: Phase, rationale: &str| Recovery::Continue {
191 next,
192 rationale: rationale.to_owned(),
193 };
194 match (self.source, self.archive, self.destination) {
195 (false, false, false) => refuse(SWITCH_NOTHING),
196 (false, false, true) => refuse(SWITCH_ORPHAN_DESTINATION),
197 (false, true, false) => restore(SWITCH_ARCHIVE_ONLY),
198 (false, true, true) => restore(SWITCH_MID_RENAME),
199 (true, false, false) => go(Phase::Prepared, SWITCH_UNTOUCHED),
200 (true, false, true) => go(Phase::DestinationValidated, SWITCH_BUILT_NOT_SWITCHED),
201 (true, true, false) => refuse(SWITCH_TWO_AUTHORITIES),
202 (true, true, true) => refuse(SWITCH_IMPOSSIBLE),
203 }
204 }
205}