1use super::{
4 ClosureError, ClosureLimits, GenerationExportManifest, GenerationExportManifestError,
5 prove_generation_closure_async, validate_generation_export_manifest,
6};
7use crate::async_storage::AsyncObjectStore;
8use crate::cancellation::{CancellationError, CancellationToken};
9use crate::foundation::VolumeId;
10use crate::model::VolumeConfig;
11use crate::performance::{
12 MeasuredResult, OperationFailure, OperationReceipt, WorkBudget, WorkCounters, WorkError,
13};
14use crate::storage::{ObjectId, ObjectRead, ObjectReadRequest, ObjectStoreError};
15use bytes::Bytes;
16use std::mem::size_of;
17use thiserror::Error;
18
19#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
21pub struct TransferCursor(u64);
22
23impl TransferCursor {
24 pub const START: Self = Self(0);
26
27 #[must_use]
29 pub const fn new(next_object: u64) -> Self {
30 Self(next_object)
31 }
32
33 #[must_use]
35 pub const fn next_object(self) -> u64 {
36 self.0
37 }
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct GenerationTransferBatch {
44 pub first_object: TransferCursor,
46 pub next: Option<TransferCursor>,
48 pub objects: Vec<ObjectRead>,
50}
51
52#[derive(Debug, Error)]
54pub enum GenerationTransferError {
55 #[error(transparent)]
57 Cancelled(#[from] CancellationError),
58 #[error(transparent)]
60 Manifest(#[from] GenerationExportManifestError),
61 #[error(transparent)]
63 Closure(#[from] ClosureError),
64 #[error(transparent)]
66 Object(#[from] ObjectStoreError),
67 #[error("generation transfer manifest does not match its authenticated closure")]
69 ManifestMismatch,
70 #[error("generation transfer cursor is outside the manifest")]
72 InvalidCursor,
73 #[error("generation transfer batch bound must be positive")]
75 EmptyBatch,
76 #[error("generation transfer batch exceeds its object bound")]
78 TooManyObjects,
79 #[error("generation transfer request allocation failed")]
81 AllocationFailed,
82 #[error(transparent)]
84 Work(#[from] WorkError),
85}
86
87pub type GenerationTransferResult<T> = MeasuredResult<OperationReceipt<T>, GenerationTransferError>;
89
90pub async fn build_generation_export_manifest_async<S: AsyncObjectStore>(
97 store: &S,
98 volume_id: VolumeId,
99 config: VolumeConfig,
100 generation_root: ObjectId,
101 closure_limits: ClosureLimits,
102 budget: WorkBudget,
103 cancellation: &CancellationToken,
104) -> GenerationTransferResult<GenerationExportManifest> {
105 cancellation
106 .check()
107 .map_err(|error| OperationFailure::before_work(error.into()))?;
108 config
109 .validate()
110 .map_err(GenerationExportManifestError::from)
111 .map_err(|error| OperationFailure::before_work(error.into()))?;
112 let proof = prove_generation_closure_async(
113 store,
114 generation_root,
115 closure_limits,
116 budget,
117 cancellation,
118 )
119 .await
120 .map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
121 let manifest = GenerationExportManifest {
122 volume_id,
123 config,
124 generation_root,
125 generation_id: proof.generation_id,
126 objects: proof.objects,
127 file_count: proof.file_count,
128 };
129 validate_generation_export_manifest(&manifest, config.limits.maximum_objects_per_generation)
130 .map_err(|error| OperationFailure::new(error.into(), proof.work))?;
131 if proof.root.volume_id != volume_id {
132 return Err(OperationFailure::new(
133 GenerationTransferError::ManifestMismatch,
134 proof.work,
135 ));
136 }
137 Ok(OperationReceipt {
138 value: manifest,
139 work: proof.work,
140 })
141}
142
143pub async fn authenticate_generation_export_manifest_async<S: AsyncObjectStore>(
150 store: &S,
151 manifest: &GenerationExportManifest,
152 closure_limits: ClosureLimits,
153 budget: WorkBudget,
154 cancellation: &CancellationToken,
155) -> GenerationTransferResult<()> {
156 cancellation
157 .check()
158 .map_err(|error| OperationFailure::before_work(error.into()))?;
159 validate_generation_export_manifest(
160 manifest,
161 manifest.config.limits.maximum_objects_per_generation,
162 )
163 .map_err(|error| OperationFailure::before_work(error.into()))?;
164 let proof = prove_generation_closure_async(
165 store,
166 manifest.generation_root,
167 closure_limits,
168 budget,
169 cancellation,
170 )
171 .await
172 .map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
173 if proof.root.volume_id != manifest.volume_id
174 || proof.generation_id != manifest.generation_id
175 || proof.objects != manifest.objects
176 || proof.file_count != manifest.file_count
177 {
178 return Err(OperationFailure::new(
179 GenerationTransferError::ManifestMismatch,
180 proof.work,
181 ));
182 }
183 Ok(OperationReceipt {
184 value: (),
185 work: proof.work,
186 })
187}
188
189pub async fn export_generation_batch_async<S: AsyncObjectStore + ?Sized>(
199 store: &S,
200 manifest: &GenerationExportManifest,
201 cursor: TransferCursor,
202 maximum_objects: u32,
203 maximum_object_bytes: u64,
204 budget: WorkBudget,
205 cancellation: &CancellationToken,
206) -> GenerationTransferResult<GenerationTransferBatch> {
207 cancellation
208 .check()
209 .map_err(|error| OperationFailure::before_work(error.into()))?;
210 validate_generation_export_manifest(
211 manifest,
212 manifest.config.limits.maximum_objects_per_generation,
213 )
214 .map_err(|error| OperationFailure::before_work(error.into()))?;
215 if maximum_objects == 0 || maximum_object_bytes == 0 {
216 return Err(OperationFailure::before_work(
217 GenerationTransferError::EmptyBatch,
218 ));
219 }
220 let object_count = u64::try_from(manifest.objects.len()).unwrap_or(u64::MAX);
221 if cursor.0 > object_count {
222 return Err(OperationFailure::before_work(
223 GenerationTransferError::InvalidCursor,
224 ));
225 }
226 if cursor.0 == object_count {
227 return Ok(OperationReceipt {
228 value: GenerationTransferBatch {
229 first_object: cursor,
230 next: None,
231 objects: Vec::new(),
232 },
233 work: WorkCounters::default(),
234 });
235 }
236 let remaining_objects = object_count - cursor.0;
237 let count = remaining_objects.min(u64::from(maximum_objects));
238 let start = usize::try_from(cursor.0)
239 .map_err(|_| OperationFailure::before_work(GenerationTransferError::InvalidCursor))?;
240 let count_usize = usize::try_from(count)
241 .map_err(|_| OperationFailure::before_work(GenerationTransferError::TooManyObjects))?;
242 let request_bytes = count
243 .checked_mul(u64::try_from(size_of::<ObjectReadRequest>()).unwrap_or(u64::MAX))
244 .ok_or_else(|| {
245 OperationFailure::before_work(GenerationTransferError::Work(WorkError::Overflow))
246 })?;
247 let mut work = WorkCounters {
248 bytes_copied: request_bytes,
249 allocation_operations: 1,
250 peak_allocation_bytes: request_bytes,
251 ..WorkCounters::default()
252 };
253 work.verify(budget)
254 .map_err(|error| OperationFailure::before_work(error.into()))?;
255 let mut requests = Vec::new();
256 requests
257 .try_reserve_exact(count_usize)
258 .map_err(|_| OperationFailure::new(GenerationTransferError::AllocationFailed, work))?;
259 let admitted_object_bytes =
260 maximum_object_bytes.min(manifest.config.limits.maximum_object_bytes);
261 #[allow(
262 clippy::indexing_slicing,
263 reason = "`cursor.0 > object_count` was already rejected above, and `cursor.0 == \
264 object_count` returns early, so `cursor.0 < object_count`; `count = \
265 remaining_objects.min(maximum_objects)` where `remaining_objects = \
266 object_count - cursor.0`, so `count <= remaining_objects`, giving `start + \
267 count_usize <= start + remaining_objects == object_count == \
268 manifest.objects.len()`"
269 )]
270 for object_id in &manifest.objects[start..start + count_usize] {
271 requests.push(ObjectReadRequest {
272 object_id: *object_id,
273 maximum_bytes: admitted_object_bytes,
274 });
275 }
276 let nested = store
277 .read_many(
278 &requests,
279 work.remaining(budget).map_err(|error| {
280 OperationFailure::new(GenerationTransferError::Work(error), work)
281 })?,
282 cancellation,
283 )
284 .await
285 .map_err(|failure| failure.map_with_prior_work(work, GenerationTransferError::from))?;
286 let nested_peak = request_bytes.saturating_add(nested.work.peak_allocation_bytes);
287 work = work
288 .checked_add(nested.work)
289 .map_err(|error| OperationFailure::new(error.into(), work))?;
290 work.peak_allocation_bytes = work.peak_allocation_bytes.max(nested_peak);
291 work.verify(budget)
292 .map_err(|error| OperationFailure::new(error.into(), work))?;
293 let next_index = cursor.0 + count;
294 Ok(OperationReceipt {
295 value: GenerationTransferBatch {
296 first_object: cursor,
297 next: (next_index < object_count).then_some(TransferCursor(next_index)),
298 objects: nested.value,
299 },
300 work,
301 })
302}
303
304pub async fn import_generation_batch_async<S: AsyncObjectStore + ?Sized>(
314 store: &S,
315 manifest: &GenerationExportManifest,
316 cursor: TransferCursor,
317 bodies: &[Bytes],
318 maximum_objects: u32,
319 budget: WorkBudget,
320 cancellation: &CancellationToken,
321) -> GenerationTransferResult<TransferCursor> {
322 cancellation
323 .check()
324 .map_err(|error| OperationFailure::before_work(error.into()))?;
325 validate_generation_export_manifest(
326 manifest,
327 manifest.config.limits.maximum_objects_per_generation,
328 )
329 .map_err(|error| OperationFailure::before_work(error.into()))?;
330 if maximum_objects == 0 {
331 return Err(OperationFailure::before_work(
332 GenerationTransferError::EmptyBatch,
333 ));
334 }
335 let object_count = u64::try_from(manifest.objects.len()).unwrap_or(u64::MAX);
336 let body_count = u64::try_from(bodies.len()).unwrap_or(u64::MAX);
337 if cursor.0 > object_count {
338 return Err(OperationFailure::before_work(
339 GenerationTransferError::InvalidCursor,
340 ));
341 }
342 if cursor.0 == object_count && bodies.is_empty() {
343 return Ok(OperationReceipt {
344 value: cursor,
345 work: WorkCounters::default(),
346 });
347 }
348 if bodies.is_empty() {
349 return Err(OperationFailure::before_work(
350 GenerationTransferError::EmptyBatch,
351 ));
352 }
353 if body_count > u64::from(maximum_objects)
354 || cursor
355 .0
356 .checked_add(body_count)
357 .is_none_or(|end| end > object_count)
358 {
359 return Err(OperationFailure::before_work(
360 GenerationTransferError::TooManyObjects,
361 ));
362 }
363 let start = usize::try_from(cursor.0)
364 .map_err(|_| OperationFailure::before_work(GenerationTransferError::InvalidCursor))?;
365 let mut work = WorkCounters::default();
366 for (offset, body) in bodies.iter().enumerate() {
367 cancellation
368 .check()
369 .map_err(|error| OperationFailure::new(error.into(), work))?;
370 #[allow(
371 clippy::indexing_slicing,
372 reason = "the check above rejects `body_count > maximum_objects` or \
373 `cursor.0.checked_add(body_count)` overflowing or exceeding `object_count`, \
374 so `cursor.0 + body_count <= object_count` (== manifest.objects.len()); \
375 `start == cursor.0` as usize and `offset < bodies.len() == body_count`, so \
376 `start + offset < start + body_count <= manifest.objects.len()`"
377 )]
378 let object_id = manifest.objects[start + offset];
379 let receipt = store
380 .put(
381 object_id,
382 body.clone(),
383 work.remaining(budget).map_err(|error| {
384 OperationFailure::new(GenerationTransferError::Work(error), work)
385 })?,
386 cancellation,
387 )
388 .await
389 .map_err(|failure| failure.map_with_prior_work(work, GenerationTransferError::from))?;
390 work = work
391 .checked_add(receipt.work)
392 .map_err(|error| OperationFailure::new(error.into(), work))?;
393 }
394 Ok(OperationReceipt {
395 value: TransferCursor(cursor.0 + body_count),
396 work,
397 })
398}
399
400#[cfg(test)]
401#[path = "tests/transfer.rs"]
402mod tests;