microsandbox_image/checkpoint/
layer_selection.rs1use std::ops::Range;
7
8use thiserror::Error;
9
10#[derive(Clone, Debug, Eq, Error, PartialEq)]
16pub enum LayerSelectionError {
17 #[error("a disk chain must contain at least one layer")]
19 EmptyChain,
20 #[error("compaction requires at least two layers, including the base")]
22 TooFewLayersToCompact,
23 #[error("cannot compact {requested} layers: only {sealed} sealed layers are available")]
25 CompactionIncludesWritableHead {
26 requested: usize,
28 sealed: usize,
30 },
31 #[error("cannot export the last {requested} layers of a {available}-layer checkpoint")]
33 InvalidExportCount {
34 requested: usize,
36 available: usize,
38 },
39 #[error(
41 "the export baseline is not an exact physical prefix; export the new base first or save a complete archive"
42 )]
43 IncompatibleExportBase,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct DiskCompactionPlan {
52 input_layers: usize,
53 prefix_layers: usize,
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct DiskLayerExportPlan {
62 checkpoint_layers: usize,
63 required_layers: usize,
64}
65
66impl DiskCompactionPlan {
71 pub fn new(runtime_layers: usize, layers: Option<usize>) -> Result<Self, LayerSelectionError> {
73 let sealed = runtime_layers
74 .checked_sub(1)
75 .ok_or(LayerSelectionError::EmptyChain)?;
76 let prefix_layers = match layers {
77 Some(0 | 1) => return Err(LayerSelectionError::TooFewLayersToCompact),
78 _ if sealed < 2 => 0,
79 Some(requested) => requested.min(sealed),
80 None => sealed,
81 };
82 Ok(Self {
83 input_layers: runtime_layers,
84 prefix_layers,
85 })
86 }
87
88 pub fn prefix(&self) -> Range<usize> {
90 0..self.prefix_layers
91 }
92
93 pub fn retained(&self) -> Range<usize> {
95 self.prefix_layers..self.input_layers
96 }
97
98 pub fn output_layers(&self) -> usize {
100 if self.prefix_layers == 0 {
101 self.input_layers
102 } else {
103 self.input_layers - self.prefix_layers + 1
104 }
105 }
106
107 pub fn is_noop(&self) -> bool {
109 self.prefix_layers == 0
110 }
111}
112
113impl DiskLayerExportPlan {
114 pub fn complete(checkpoint_layers: usize) -> Result<Self, LayerSelectionError> {
116 if checkpoint_layers == 0 {
117 return Err(LayerSelectionError::EmptyChain);
118 }
119 Ok(Self {
120 checkpoint_layers,
121 required_layers: 0,
122 })
123 }
124
125 pub fn last(checkpoint_layers: usize, layers: usize) -> Result<Self, LayerSelectionError> {
127 let mut plan = Self::complete(checkpoint_layers)?;
128 if layers == 0 || layers > checkpoint_layers {
129 return Err(LayerSelectionError::InvalidExportCount {
130 requested: layers,
131 available: checkpoint_layers,
132 });
133 }
134 plan.required_layers = checkpoint_layers - layers;
135 Ok(plan)
136 }
137
138 pub fn since<T: PartialEq>(target: &[T], baseline: &[T]) -> Result<Self, LayerSelectionError> {
144 let mut plan = Self::complete(target.len())?;
145 if baseline.is_empty() || !target.starts_with(baseline) {
146 return Err(LayerSelectionError::IncompatibleExportBase);
147 }
148 plan.required_layers = baseline.len();
149 Ok(plan)
150 }
151
152 pub fn included(&self) -> Range<usize> {
154 self.required_layers..self.checkpoint_layers
155 }
156
157 pub fn required(&self) -> Range<usize> {
159 0..self.required_layers
160 }
161
162 pub fn is_disk_complete(&self) -> bool {
164 self.required_layers == 0
165 }
166}
167
168#[cfg(test)]
173mod tests {
174 use super::{DiskCompactionPlan, DiskLayerExportPlan, LayerSelectionError};
175
176 #[test]
177 fn compaction_count_includes_base_and_excludes_writable_head() {
178 let plan = DiskCompactionPlan::new(5, Some(3)).unwrap();
179 assert_eq!(plan.prefix(), 0..3);
180 assert_eq!(plan.retained(), 3..5);
181 assert_eq!(plan.output_layers(), 3);
182 assert!(!plan.is_noop());
183 }
184
185 #[test]
186 fn default_compacts_all_sealed_layers_only() {
187 let plan = DiskCompactionPlan::new(5, None).unwrap();
188 assert_eq!(plan.prefix(), 0..4);
189 assert_eq!(plan.retained(), 4..5);
190 assert_eq!(plan.output_layers(), 2);
191 }
192
193 #[test]
194 fn insufficient_sealed_layers_are_a_noop_not_a_conversion() {
195 for count in [1, 2] {
196 for requested in [None, Some(2), Some(usize::MAX)] {
197 let plan = DiskCompactionPlan::new(count, requested).unwrap();
198 assert!(plan.is_noop());
199 assert_eq!(plan.prefix(), 0..0);
200 assert_eq!(plan.retained(), 0..count);
201 assert_eq!(plan.output_layers(), count);
202 }
203 }
204 }
205
206 #[test]
207 fn explicit_compaction_uses_up_to_available_sealed_layers() {
208 assert_eq!(
209 DiskCompactionPlan::new(0, None),
210 Err(LayerSelectionError::EmptyChain)
211 );
212 for count in [0, 1] {
213 assert_eq!(
214 DiskCompactionPlan::new(5, Some(count)),
215 Err(LayerSelectionError::TooFewLayersToCompact)
216 );
217 }
218 for count in [5, 6, usize::MAX] {
219 let plan = DiskCompactionPlan::new(5, Some(count)).unwrap();
220 assert_eq!(plan.prefix(), 0..4);
221 assert_eq!(plan.retained(), 4..5);
222 assert_eq!(plan.output_layers(), 2);
223 }
224 }
225
226 #[test]
227 fn export_includes_the_checkpoint_top_layer() {
228 let plan = DiskLayerExportPlan::last(5, 2).unwrap();
229 assert_eq!(plan.included(), 3..5);
230 assert_eq!(plan.required(), 0..3);
231 assert!(!plan.is_disk_complete());
232 }
233
234 #[test]
235 fn exporting_all_layers_is_disk_complete() {
236 let plan = DiskLayerExportPlan::last(5, 5).unwrap();
237 assert_eq!(plan, DiskLayerExportPlan::complete(5).unwrap());
238 assert_eq!(plan.required(), 0..0);
239 assert!(plan.is_disk_complete());
240 }
241
242 #[test]
243 fn export_rejects_empty_and_out_of_range_counts() {
244 assert_eq!(
245 DiskLayerExportPlan::complete(0),
246 Err(LayerSelectionError::EmptyChain)
247 );
248 for count in [0, 6, usize::MAX] {
249 assert!(DiskLayerExportPlan::last(5, count).is_err());
250 }
251 }
252
253 #[test]
254 fn since_requires_the_exact_physical_prefix() {
255 let target = [("base", "raw"), ("a", "qcow2"), ("b", "qcow2")];
256 let plan = DiskLayerExportPlan::since(&target, &target[..2]).unwrap();
257 assert_eq!(plan.required(), 0..2);
258 assert_eq!(plan.included(), 2..3);
259 for invalid in [
260 vec![],
261 vec![("compacted", "raw")],
262 vec![("base", "qcow2")],
263 vec![("a", "qcow2")],
264 ] {
265 assert_eq!(
266 DiskLayerExportPlan::since(&target, &invalid),
267 Err(LayerSelectionError::IncompatibleExportBase)
268 );
269 }
270 assert!(DiskLayerExportPlan::since(&target[..1], &target).is_err());
271 }
272
273 #[test]
274 fn equal_disk_prefix_can_omit_all_disk_bytes_without_omitting_vm_state() {
275 let target = ["base", "a"];
278 let plan = DiskLayerExportPlan::since(&target, &target).unwrap();
279 assert_eq!(plan.included(), 2..2);
280 assert_eq!(plan.required(), 0..2);
281 assert!(!plan.is_disk_complete());
282 }
283}