1use crate::{LmdbBlobReader, PoolCatalogLocation, PoolStore};
2use hashtree_core::store::StoreError;
3use hashtree_core::{sha256, types::Hash};
4use std::time::{Duration, Instant};
5
6pub const DEFAULT_POOL_MIGRATION_MAX_BUFFER_BYTES: usize = 64 * 1024 * 1024;
11
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct PoolMigrationBatch {
14 pub scanned: usize,
16 pub already_present: usize,
18 pub verified: usize,
20 pub verified_source_entries: Vec<(Hash, u64)>,
24 pub source_entries: Vec<(Hash, u64)>,
28 pub inserted: usize,
29 pub inserted_bytes: u64,
30 pub write_batches: usize,
32 pub peak_buffered_bytes: u64,
34 pub scan_micros: u64,
36 pub catalog_probe_micros: u64,
38 pub source_read_micros: u64,
40 pub source_read_groups: usize,
42 pub source_verify_micros: u64,
44 pub target_write_micros: u64,
46 pub last_hash: Option<Hash>,
47 pub source_exhausted: bool,
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct LmdbSourceAuditBatch {
52 pub scanned: usize,
53 pub verified: usize,
54 pub verified_bytes: u64,
55 pub verified_source_entries: Vec<(Hash, u64)>,
56 pub peak_buffered_bytes: u64,
57 pub scan_micros: u64,
58 pub source_read_micros: u64,
59 pub source_verify_micros: u64,
60 pub last_hash: Option<Hash>,
61 pub source_exhausted: bool,
62}
63
64pub fn audit_lmdb_source_batch_with_max_buffer_bytes(
67 source: &LmdbBlobReader,
68 after: Option<Hash>,
69 limit: usize,
70 max_buffer_bytes: usize,
71) -> Result<LmdbSourceAuditBatch, StoreError> {
72 if limit == 0 {
73 return Ok(LmdbSourceAuditBatch::default());
74 }
75 if max_buffer_bytes == 0 {
76 return Err(StoreError::Other(
77 "source audit max buffer bytes must be non-zero".into(),
78 ));
79 }
80 let scan_started = Instant::now();
81 let hashes = source.scan_hashes_after(after, limit)?;
82 let scan_micros = elapsed_micros(scan_started.elapsed());
83 if hashes.is_empty() {
84 return Ok(LmdbSourceAuditBatch {
85 source_exhausted: true,
86 scan_micros,
87 ..LmdbSourceAuditBatch::default()
88 });
89 }
90 let mut batch = LmdbSourceAuditBatch {
91 scanned: hashes.len(),
92 scan_micros,
93 last_hash: hashes.last().copied(),
94 ..LmdbSourceAuditBatch::default()
95 };
96 let mut next = 0usize;
97 while next < hashes.len() {
98 let source_read_started = Instant::now();
99 let items = source.read_hashes_bounded(&hashes[next..], max_buffer_bytes as u64)?;
100 batch.source_read_micros = batch
101 .source_read_micros
102 .saturating_add(elapsed_micros(source_read_started.elapsed()));
103 if items.is_empty() {
104 return Err(StoreError::Other(
105 "bounded source audit read made no progress".into(),
106 ));
107 }
108 let source_verify_started = Instant::now();
109 let mut buffered_bytes = 0u64;
110 for (offset, (hash, data)) in items.iter().enumerate() {
111 let expected_hash = hashes.get(next + offset).ok_or_else(|| {
112 StoreError::Other("bounded source read exceeded the requested hash set".into())
113 })?;
114 if hash != expected_hash {
115 return Err(StoreError::Other(format!(
116 "bounded source read returned {:?}, expected {:?}",
117 hash, expected_hash
118 )));
119 }
120 if sha256(data) != *hash {
121 return Err(StoreError::Other(format!(
122 "source returned corrupt bytes for {hash:?}"
123 )));
124 }
125 let len = data.len() as u64;
126 buffered_bytes = buffered_bytes.saturating_add(len);
127 batch.verified_bytes = batch.verified_bytes.saturating_add(len);
128 batch.verified_source_entries.push((*hash, len));
129 }
130 batch.source_verify_micros = batch
131 .source_verify_micros
132 .saturating_add(elapsed_micros(source_verify_started.elapsed()));
133 batch.verified = batch.verified.saturating_add(items.len());
134 batch.peak_buffered_bytes = batch.peak_buffered_bytes.max(buffered_bytes);
135 next = next.saturating_add(items.len());
136 }
137 Ok(batch)
138}
139
140pub fn migrate_lmdb_batch(
146 source: &LmdbBlobReader,
147 target: &PoolStore,
148 after: Option<Hash>,
149 limit: usize,
150) -> Result<PoolMigrationBatch, StoreError> {
151 migrate_lmdb_batch_with_max_buffer_bytes(
152 source,
153 target,
154 after,
155 limit,
156 DEFAULT_POOL_MIGRATION_MAX_BUFFER_BYTES,
157 )
158}
159
160pub fn migrate_lmdb_batch_with_max_buffer_bytes(
168 source: &LmdbBlobReader,
169 target: &PoolStore,
170 after: Option<Hash>,
171 limit: usize,
172 max_buffer_bytes: usize,
173) -> Result<PoolMigrationBatch, StoreError> {
174 migrate_lmdb_batch_with_max_buffer_bytes_and_authorizer(
175 source,
176 target,
177 after,
178 limit,
179 max_buffer_bytes,
180 &mut |_, _| Ok(()),
181 )
182}
183
184pub fn migrate_lmdb_batch_with_max_buffer_bytes_and_authorizer(
193 source: &LmdbBlobReader,
194 target: &PoolStore,
195 after: Option<Hash>,
196 limit: usize,
197 max_buffer_bytes: usize,
198 authorize_target_write: &mut dyn FnMut(Option<Hash>, usize) -> Result<(), StoreError>,
199) -> Result<PoolMigrationBatch, StoreError> {
200 if limit == 0 {
201 return Ok(PoolMigrationBatch::default());
202 }
203 if max_buffer_bytes == 0 {
204 return Err(StoreError::Other(
205 "migration max buffer bytes must be non-zero".into(),
206 ));
207 }
208 let scan_started = Instant::now();
209 let hashes = source.scan_hashes_after(after, limit)?;
210 let scan_micros = elapsed_micros(scan_started.elapsed());
211 if hashes.is_empty() {
212 return Ok(PoolMigrationBatch {
213 source_exhausted: true,
214 scan_micros,
215 ..PoolMigrationBatch::default()
216 });
217 }
218
219 let mut batch = migrate_lmdb_hashes_with_max_buffer_bytes_and_authorizer(
220 source,
221 target,
222 &hashes,
223 after,
224 max_buffer_bytes,
225 authorize_target_write,
226 )?;
227 batch.scan_micros = scan_micros;
228 Ok(batch)
229}
230
231pub fn migrate_lmdb_hashes_with_max_buffer_bytes_and_authorizer(
237 source: &LmdbBlobReader,
238 target: &PoolStore,
239 hashes: &[Hash],
240 checkpoint_cursor: Option<Hash>,
241 max_buffer_bytes: usize,
242 authorize_target_write: &mut dyn FnMut(Option<Hash>, usize) -> Result<(), StoreError>,
243) -> Result<PoolMigrationBatch, StoreError> {
244 if hashes.is_empty() {
245 return Ok(PoolMigrationBatch::default());
246 }
247 if max_buffer_bytes == 0 {
248 return Err(StoreError::Other(
249 "migration max buffer bytes must be non-zero".into(),
250 ));
251 }
252 if hashes.windows(2).any(|pair| pair[0] >= pair[1]) {
253 return Err(StoreError::Other(
254 "explicit migration hashes must be unique and strictly sorted".into(),
255 ));
256 }
257 let mut batch = PoolMigrationBatch {
258 scanned: hashes.len(),
259 last_hash: hashes.last().copied(),
260 ..PoolMigrationBatch::default()
261 };
262 let catalog_probe_started = Instant::now();
263 let committed = target.committed_hashes_in_sorted_candidates(hashes)?;
264 batch.catalog_probe_micros = elapsed_micros(catalog_probe_started.elapsed());
265 let max_buffer_bytes = max_buffer_bytes as u64;
266 let mut next = 0usize;
267 while next < hashes.len() {
268 let source_read_started = Instant::now();
269 let items = source.read_hashes_bounded(&hashes[next..], max_buffer_bytes)?;
270 batch.source_read_micros = batch
271 .source_read_micros
272 .saturating_add(elapsed_micros(source_read_started.elapsed()));
273 if items.is_empty() {
274 return Err(StoreError::Other(
275 "bounded source read made no migration progress".into(),
276 ));
277 }
278 let source_verify_started = Instant::now();
279 let mut buffered_bytes = 0u64;
280 for (hash, data) in &items {
281 if sha256(data) != *hash {
282 return Err(StoreError::Other(format!(
283 "source returned corrupt bytes for {hash:?}"
284 )));
285 }
286 buffered_bytes = buffered_bytes.saturating_add(data.len() as u64);
287 batch
288 .verified_source_entries
289 .push((*hash, data.len() as u64));
290 }
291 batch.source_verify_micros = batch
292 .source_verify_micros
293 .saturating_add(elapsed_micros(source_verify_started.elapsed()));
294 batch.verified = batch.verified.saturating_add(items.len());
295 batch.peak_buffered_bytes = batch.peak_buffered_bytes.max(buffered_bytes);
296 let item_count = items.len();
297 let mut writes = Vec::new();
298 for (offset, (hash, data)) in items.into_iter().enumerate() {
299 if committed[next + offset] {
300 let target_data = target.get_sync(&hash)?.ok_or_else(|| {
301 StoreError::Other(format!(
302 "target catalog claims committed blob {hash:?}, but its bytes are unavailable"
303 ))
304 })?;
305 if target_data != data {
306 return Err(StoreError::Other(format!(
307 "target catalog claims committed blob {hash:?}, but its bytes differ from source"
308 )));
309 }
310 batch.already_present = batch.already_present.saturating_add(1);
311 } else {
312 writes.push((hash, data));
313 }
314 }
315 let target_write_started = Instant::now();
316 if !writes.is_empty() {
317 authorize_target_write(checkpoint_cursor, writes.len())?;
318 }
319 let report = target.put_many_report_sync(&writes)?;
320 batch.target_write_micros = batch
321 .target_write_micros
322 .saturating_add(elapsed_micros(target_write_started.elapsed()));
323 batch.inserted = batch.inserted.saturating_add(report.inserted);
324 batch.inserted_bytes = batch.inserted_bytes.saturating_add(report.inserted_bytes);
325 if !writes.is_empty() {
326 batch.write_batches = batch.write_batches.saturating_add(1);
327 }
328 next = next.saturating_add(item_count);
329 }
330 Ok(batch)
331}
332
333pub fn reconcile_lmdb_source_batch_with_max_buffer_bytes_and_authorizer(
341 source: &LmdbBlobReader,
342 target: &PoolStore,
343 after: Option<Hash>,
344 limit: usize,
345 max_buffer_bytes: usize,
346 authorize_target_write: &mut dyn FnMut(Option<Hash>, usize) -> Result<(), StoreError>,
347) -> Result<PoolMigrationBatch, StoreError> {
348 if limit == 0 {
349 return Ok(PoolMigrationBatch::default());
350 }
351 if max_buffer_bytes == 0 {
352 return Err(StoreError::Other(
353 "migration max buffer bytes must be non-zero".into(),
354 ));
355 }
356 let scan_started = Instant::now();
357 let hashes = source.scan_hashes_after(after, limit)?;
358 let scan_micros = elapsed_micros(scan_started.elapsed());
359 if hashes.is_empty() {
360 return Ok(PoolMigrationBatch {
361 source_exhausted: true,
362 scan_micros,
363 ..PoolMigrationBatch::default()
364 });
365 }
366 let mut batch = reconcile_lmdb_source_hashes_with_max_buffer_bytes_and_authorizer(
367 source,
368 target,
369 &hashes,
370 max_buffer_bytes,
371 authorize_target_write,
372 )?;
373 batch.scan_micros = scan_micros;
374 Ok(batch)
375}
376
377pub fn reconcile_lmdb_source_hashes_with_max_buffer_bytes_and_authorizer(
380 source: &LmdbBlobReader,
381 target: &PoolStore,
382 hashes: &[Hash],
383 max_buffer_bytes: usize,
384 authorize_target_write: &mut dyn FnMut(Option<Hash>, usize) -> Result<(), StoreError>,
385) -> Result<PoolMigrationBatch, StoreError> {
386 if hashes.is_empty() {
387 return Ok(PoolMigrationBatch::default());
388 }
389 if max_buffer_bytes == 0 {
390 return Err(StoreError::Other(
391 "migration max buffer bytes must be non-zero".into(),
392 ));
393 }
394 if hashes.windows(2).any(|pair| pair[0] >= pair[1]) {
395 return Err(StoreError::Other(
396 "source reconciliation hashes must be unique and strictly sorted".into(),
397 ));
398 }
399
400 let catalog_probe_started = Instant::now();
401 let locations = target.catalog_locations_in_sorted_candidates(hashes)?;
402 let catalog_probe_micros = elapsed_micros(catalog_probe_started.elapsed());
403 if locations.len() != hashes.len() {
404 return Err(StoreError::Other(
405 "target catalog lookup returned the wrong result count".into(),
406 ));
407 }
408
409 let mut source_entries = Vec::with_capacity(hashes.len());
410 let mut needed = Vec::new();
411 let mut already_present = 0usize;
412 for (hash, location) in hashes.iter().copied().zip(locations) {
413 match location {
414 PoolCatalogLocation::Stored { size, .. } => {
415 source_entries.push((hash, size));
416 already_present = already_present.saturating_add(1);
417 }
418 PoolCatalogLocation::Pending { size, .. } => {
419 needed.push((hash, Some(size)));
420 }
421 PoolCatalogLocation::Missing => needed.push((hash, None)),
422 PoolCatalogLocation::Moving { size, .. } => {
423 return Err(StoreError::Other(format!(
424 "target Moving location is non-terminal for {hash:?} (declared size {size})"
425 )));
426 }
427 }
428 }
429
430 let mut batch = PoolMigrationBatch {
431 scanned: hashes.len(),
432 already_present,
433 catalog_probe_micros,
434 last_hash: hashes.last().copied(),
435 ..PoolMigrationBatch::default()
436 };
437 let max_buffer_bytes = max_buffer_bytes as u64;
438 let mut next = 0usize;
439 while next < needed.len() {
440 let source_read_started = Instant::now();
441 let needed_hashes = needed[next..]
442 .iter()
443 .map(|(hash, _)| *hash)
444 .collect::<Vec<_>>();
445 let items = source.read_hashes_bounded(&needed_hashes, max_buffer_bytes)?;
446 batch.source_read_micros = batch
447 .source_read_micros
448 .saturating_add(elapsed_micros(source_read_started.elapsed()));
449 if items.is_empty() {
450 return Err(StoreError::Other(
451 "bounded key-only source reconciliation read made no progress".into(),
452 ));
453 }
454 batch.source_read_groups = batch.source_read_groups.saturating_add(1);
455
456 let source_verify_started = Instant::now();
457 let mut buffered_bytes = 0u64;
458 let mut writes = Vec::with_capacity(items.len());
459 for (offset, (hash, data)) in items.into_iter().enumerate() {
460 let (expected_hash, pending_size) = needed.get(next + offset).ok_or_else(|| {
461 StoreError::Other(
462 "bounded key-only source read exceeded the requested hash set".into(),
463 )
464 })?;
465 let size = data.len() as u64;
466 if hash != *expected_hash || sha256(&data) != *expected_hash {
467 return Err(StoreError::Other(format!(
468 "source payload differs from its content-addressed key for {expected_hash:?}"
469 )));
470 }
471 if pending_size.is_some_and(|expected_size| expected_size != size) {
472 return Err(StoreError::Other(format!(
473 "target Pending size {} differs from source size {size} for {expected_hash:?}",
474 pending_size.expect("checked pending size")
475 )));
476 }
477 buffered_bytes = buffered_bytes.saturating_add(size);
478 source_entries.push((*expected_hash, size));
479 batch.verified_source_entries.push((*expected_hash, size));
480 writes.push((*expected_hash, data));
481 }
482 batch.source_verify_micros = batch
483 .source_verify_micros
484 .saturating_add(elapsed_micros(source_verify_started.elapsed()));
485 batch.verified = batch.verified.saturating_add(writes.len());
486 batch.peak_buffered_bytes = batch.peak_buffered_bytes.max(buffered_bytes);
487 authorize_target_write(writes.last().map(|(hash, _)| *hash), writes.len())?;
488 let target_write_started = Instant::now();
489 let report = target.put_many_optimistic_report_sync(&writes)?;
490 batch.target_write_micros = batch
491 .target_write_micros
492 .saturating_add(elapsed_micros(target_write_started.elapsed()));
493 batch.inserted = batch.inserted.saturating_add(report.inserted);
494 batch.inserted_bytes = batch.inserted_bytes.saturating_add(report.inserted_bytes);
495 batch.write_batches = batch.write_batches.saturating_add(1);
496 next = next.saturating_add(writes.len());
497 }
498
499 source_entries.sort_unstable_by_key(|(hash, _)| *hash);
500 if source_entries.len() != hashes.len()
501 || source_entries
502 .iter()
503 .zip(hashes)
504 .any(|((entry_hash, _), expected_hash)| entry_hash != expected_hash)
505 {
506 return Err(StoreError::Other(
507 "key-only source reconciliation did not cover the exact source page".into(),
508 ));
509 }
510 batch.source_entries = source_entries;
511 Ok(batch)
512}
513
514pub fn reconcile_lmdb_source_entries_with_max_buffer_bytes_and_authorizer(
521 source: &LmdbBlobReader,
522 target: &PoolStore,
523 source_entries: &[(Hash, u64)],
524 max_buffer_bytes: usize,
525 authorize_target_write: &mut dyn FnMut(Option<Hash>, usize) -> Result<(), StoreError>,
526) -> Result<PoolMigrationBatch, StoreError> {
527 let union_entries = source_entries
528 .iter()
529 .map(|(hash, size)| (*hash, *size, 0usize))
530 .collect::<Vec<_>>();
531 reconcile_lmdb_source_union_page_with_max_buffer_bytes_and_authorizer(
532 &[source],
533 target,
534 &union_entries,
535 max_buffer_bytes,
536 authorize_target_write,
537 )
538}
539
540pub fn reconcile_lmdb_source_union_page_with_max_buffer_bytes_and_authorizer(
549 sources: &[&LmdbBlobReader],
550 target: &PoolStore,
551 source_entries: &[(Hash, u64, usize)],
552 max_buffer_bytes: usize,
553 authorize_target_write: &mut dyn FnMut(Option<Hash>, usize) -> Result<(), StoreError>,
554) -> Result<PoolMigrationBatch, StoreError> {
555 if source_entries.is_empty() {
556 return Ok(PoolMigrationBatch::default());
557 }
558 if sources.is_empty() {
559 return Err(StoreError::Other(
560 "receipt-proven source union has no body sources".into(),
561 ));
562 }
563 if max_buffer_bytes == 0 {
564 return Err(StoreError::Other(
565 "migration max buffer bytes must be non-zero".into(),
566 ));
567 }
568 if source_entries
569 .windows(2)
570 .any(|entries| entries[0].0 >= entries[1].0)
571 {
572 return Err(StoreError::Other(
573 "receipt-proven source entries must be strictly sorted".into(),
574 ));
575 }
576 if source_entries
577 .iter()
578 .any(|(_, _, source_index)| *source_index >= sources.len())
579 {
580 return Err(StoreError::Other(
581 "receipt-proven source union references an unknown body source".into(),
582 ));
583 }
584 let hashes = source_entries
585 .iter()
586 .map(|(hash, _, _)| *hash)
587 .collect::<Vec<_>>();
588 let catalog_probe_started = Instant::now();
589 let locations = target.catalog_locations_in_sorted_candidates(&hashes)?;
590 let catalog_probe_micros = elapsed_micros(catalog_probe_started.elapsed());
591 if locations.len() != hashes.len() {
592 return Err(StoreError::Other(
593 "target catalog lookup returned the wrong result count".into(),
594 ));
595 }
596
597 let mut needed = Vec::new();
598 let mut already_present = 0usize;
599 for ((hash, expected_size, source_index), location) in
600 source_entries.iter().copied().zip(locations)
601 {
602 match location {
603 PoolCatalogLocation::Stored { size, .. } if size == expected_size => {
604 already_present = already_present.saturating_add(1);
605 }
606 PoolCatalogLocation::Stored { size, .. } => {
607 return Err(StoreError::Other(format!(
608 "target Stored size {size} differs from source size {expected_size} for {hash:?}"
609 )));
610 }
611 PoolCatalogLocation::Pending { size, .. } if size == expected_size => {
612 needed.push((hash, expected_size, source_index));
613 }
614 PoolCatalogLocation::Missing => needed.push((hash, expected_size, source_index)),
615 PoolCatalogLocation::Pending { size, .. } => {
616 return Err(StoreError::Other(format!(
617 "target Pending size {size} differs from source size {expected_size} for {hash:?}"
618 )));
619 }
620 PoolCatalogLocation::Moving { size, .. } => {
621 return Err(StoreError::Other(format!(
622 "target Moving location is non-terminal for {hash:?} (declared size {size})"
623 )));
624 }
625 }
626 }
627
628 let mut batch = PoolMigrationBatch {
629 scanned: hashes.len(),
630 already_present,
631 source_entries: source_entries
632 .iter()
633 .map(|(hash, size, _)| (*hash, *size))
634 .collect(),
635 catalog_probe_micros,
636 last_hash: hashes.last().copied(),
637 ..PoolMigrationBatch::default()
638 };
639 let max_buffer_bytes = max_buffer_bytes as u64;
640 let mut next = 0usize;
641 while next < needed.len() {
642 let mut end = next;
643 let mut chunk_bytes = 0u64;
644 while end < needed.len() {
645 let size = needed[end].1;
646 if end > next && chunk_bytes.saturating_add(size) > max_buffer_bytes {
647 break;
648 }
649 chunk_bytes = chunk_bytes.saturating_add(size);
650 end += 1;
651 if chunk_bytes >= max_buffer_bytes {
652 break;
653 }
654 }
655 let mut bodies = std::collections::HashMap::with_capacity(end - next);
656 let source_read_started = Instant::now();
657 for (source_index, source) in sources.iter().enumerate() {
658 let source_hashes = needed[next..end]
659 .iter()
660 .filter_map(|(hash, _, candidate_source)| {
661 (*candidate_source == source_index).then_some(*hash)
662 })
663 .collect::<Vec<_>>();
664 if source_hashes.is_empty() {
665 continue;
666 }
667 let items = source.read_hashes_bounded(&source_hashes, max_buffer_bytes)?;
668 if items.len() != source_hashes.len() {
669 return Err(StoreError::Other(
670 "bounded source-union read returned an incomplete body group".into(),
671 ));
672 }
673 batch.source_read_groups = batch.source_read_groups.saturating_add(1);
674 for (hash, data) in items {
675 if bodies.insert(hash, data).is_some() {
676 return Err(StoreError::Other(
677 "source-union body group returned a duplicate hash".into(),
678 ));
679 }
680 }
681 }
682 batch.source_read_micros = batch
683 .source_read_micros
684 .saturating_add(elapsed_micros(source_read_started.elapsed()));
685 if bodies.len() != end - next {
686 return Err(StoreError::Other(
687 "source-union body reads did not cover the exact needed chunk".into(),
688 ));
689 }
690 let source_verify_started = Instant::now();
691 let mut items = Vec::with_capacity(end - next);
692 for (expected_hash, expected_size, _) in &needed[next..end] {
693 let data = bodies.remove(expected_hash).ok_or_else(|| {
694 StoreError::Other(format!("source-union body group omitted {expected_hash:?}"))
695 })?;
696 if data.len() as u64 != *expected_size || sha256(&data) != *expected_hash {
697 return Err(StoreError::Other(format!(
698 "source payload differs from its receipt-bound hash/size for {expected_hash:?}"
699 )));
700 }
701 batch
702 .verified_source_entries
703 .push((*expected_hash, *expected_size));
704 items.push((*expected_hash, data));
705 }
706 batch.source_verify_micros = batch
707 .source_verify_micros
708 .saturating_add(elapsed_micros(source_verify_started.elapsed()));
709 batch.verified = batch.verified.saturating_add(items.len());
710 batch.peak_buffered_bytes = batch.peak_buffered_bytes.max(chunk_bytes);
711 authorize_target_write(items.last().map(|(hash, _)| *hash), items.len())?;
712 let target_write_started = Instant::now();
713 let report = target.put_many_optimistic_report_sync(&items)?;
714 batch.target_write_micros = batch
715 .target_write_micros
716 .saturating_add(elapsed_micros(target_write_started.elapsed()));
717 batch.inserted = batch.inserted.saturating_add(report.inserted);
718 batch.inserted_bytes = batch.inserted_bytes.saturating_add(report.inserted_bytes);
719 batch.write_batches = batch.write_batches.saturating_add(1);
720 next = end;
721 }
722 Ok(batch)
723}
724
725fn elapsed_micros(elapsed: Duration) -> u64 {
726 u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX)
727}