1use std::collections::HashSet;
55use std::fs::{File, OpenOptions};
56use std::io::{Read, Seek, SeekFrom, Write};
57use std::path::{Path, PathBuf};
58use std::sync::Mutex;
59
60use anyhow::Context;
61use gunnar_core::txlog::{Crc32c, Entry};
62use gunnar_grpc::{ControlError, ControlResult};
63
64use super::Applied;
65use crate::stores::ServerStores;
66
67pub const PARTIAL_DIR: &str = "txlog-restore-partial";
70const DATA: &str = "entry.blob";
72const RECORD: &str = "entry.rec";
74const RECORD_MAGIC: &str = "gunnar-restore-partial v1";
76
77pub const COMMIT_EVERY: u64 = 8 << 20;
80
81pub const IN_MEMORY_UP_TO: u64 = 4 << 20;
85
86pub const TWIN_TOO_OLD: &str = "restore-twin-too-old";
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Staged {
93 pub seq: u64,
94 pub kind: u16,
95 pub blob_len: u64,
96 pub meta: Vec<u8>,
97 pub durable: u64,
99}
100
101impl Staged {
102 fn to_text(&self) -> String {
103 format!(
104 "{RECORD_MAGIC}\nseq {}\nkind {}\nblob_len {}\ndurable {}\nmeta {}\n",
105 self.seq,
106 self.kind,
107 self.blob_len,
108 self.durable,
109 hex::encode(&self.meta)
110 )
111 }
112
113 fn from_text(t: &str) -> Option<Staged> {
114 let mut lines = t.lines();
115 if lines.next()? != RECORD_MAGIC {
116 return None;
117 }
118 let mut field = |name: &str| {
119 lines
120 .next()?
121 .strip_prefix(name)?
122 .strip_prefix(' ')
123 .map(str::to_owned)
124 };
125 let seq = field("seq")?.parse().ok()?;
126 let kind = field("kind")?.parse().ok()?;
127 let blob_len = field("blob_len")?.parse().ok()?;
128 let durable: u64 = field("durable")?.parse().ok()?;
129 let meta = hex::decode(field("meta")?).ok()?;
130 (durable <= blob_len && seq > 0).then_some(Staged {
131 seq,
132 kind,
133 blob_len,
134 meta,
135 durable,
136 })
137 }
138}
139
140pub fn staged(root: &Path) -> Option<Staged> {
142 std::fs::read_to_string(root.join(PARTIAL_DIR).join(RECORD))
143 .ok()
144 .as_deref()
145 .and_then(Staged::from_text)
146}
147
148pub fn partial(root: &Path) -> Option<(u64, u64)> {
151 staged(root).map(|s| (s.seq, s.durable))
152}
153
154pub fn forget(root: &Path) -> anyhow::Result<()> {
158 let dir = root.join(PARTIAL_DIR);
159 match std::fs::remove_dir_all(&dir) {
160 Ok(()) => {}
161 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
162 Err(e) => return Err(e).with_context(|| format!("removing {}", dir.display())),
163 }
164 let r = File::open(root).with_context(|| format!("opening {} to flush it", root.display()))?;
165 r.sync_all().with_context(|| {
166 format!(
167 "flushing the removal of {PARTIAL_DIR} from {}",
168 root.display()
169 )
170 })?;
171 Ok(())
172}
173
174fn write_record(root: &Path, rec: &Staged) -> anyhow::Result<()> {
177 let dir = root.join(PARTIAL_DIR);
178 if !dir.is_dir() {
179 std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
180 crate::durable::sync_dir(root)
181 .with_context(|| format!("flushing {PARTIAL_DIR} into {}", root.display()))?;
182 }
183 crate::durable::write_atomic_unframed(&dir, RECORD, rec.to_text().as_bytes())
184}
185
186fn sanitize(root: &Path, next: u64) -> anyhow::Result<Option<(Staged, File)>> {
191 if !root.join(PARTIAL_DIR).exists() {
192 return Ok(None);
193 }
194 let Some(rec) = staged(root) else {
195 forget(root)?;
196 return Ok(None);
197 };
198 if rec.seq != next {
199 tracing::info!(
200 target: gunnar_core::observe::TARGET_TWIN,
201 staged = rec.seq, next, "a staged restore entry that is not the next one is dropped (applied already, or another history)"
202 );
203 forget(root)?;
204 return Ok(None);
205 }
206 let path = root.join(PARTIAL_DIR).join(DATA);
207 let file = match OpenOptions::new().read(true).write(true).open(&path) {
208 Ok(f) => f,
209 Err(_) => {
210 forget(root)?;
211 return Ok(None);
212 }
213 };
214 let len = file
215 .metadata()
216 .with_context(|| format!("reading {}", path.display()))?
217 .len();
218 if len < rec.durable {
219 tracing::warn!(
220 target: gunnar_core::observe::TARGET_TWIN,
221 seq = rec.seq, durable = rec.durable, on_disk = len,
222 "the staged restore entry is SHORTER than its record says was made durable; it is not trusted and starts again at byte 0"
223 );
224 drop(file);
225 forget(root)?;
226 return Ok(None);
227 }
228 let mut file = file;
229 if len > rec.durable {
230 file.set_len(rec.durable).with_context(|| {
231 format!(
232 "cutting {} back to its durable {} bytes",
233 path.display(),
234 rec.durable
235 )
236 })?;
237 file.sync_all()
238 .with_context(|| format!("flushing {}", path.display()))?;
239 }
240 file.seek(SeekFrom::Start(rec.durable))
242 .with_context(|| format!("seeking {}", path.display()))?;
243 Ok(Some((rec, file)))
244}
245
246static OPEN: Mutex<Option<HashSet<PathBuf>>> = Mutex::new(None);
248
249struct Held(PathBuf);
251
252impl Held {
253 fn take(root: &Path) -> Option<Held> {
254 let mut open = OPEN.lock().unwrap_or_else(|p| p.into_inner());
255 open.get_or_insert_with(HashSet::new)
256 .insert(root.to_path_buf())
257 .then(|| Held(root.to_path_buf()))
258 }
259}
260
261impl Drop for Held {
262 fn drop(&mut self) {
263 let mut open = OPEN.lock().unwrap_or_else(|p| p.into_inner());
264 if let Some(set) = open.as_mut() {
265 set.remove(&self.0);
266 }
267 }
268}
269
270enum Body {
272 Skip,
274 Memory(Vec<u8>),
276 Spool {
278 file: File,
279 rec: Staged,
280 written: u64,
281 },
282}
283
284struct Open {
286 seq: u64,
287 kind: u16,
288 meta: Vec<u8>,
289 blob_len: u64,
290 pos: u64,
292 explicit: bool,
294 held_before: u64,
296 body: Body,
297}
298
299pub struct Intake<'a> {
301 stores: &'a ServerStores,
302 _held: Held,
303 next: u64,
304 kept: Option<(Staged, File)>,
306 open: Option<Open>,
307 applied: u64,
308 lost: u64,
309 snap: Option<super::snapshot::RestoreSnapshot>,
312 snap_touched: bool,
314}
315
316impl<'a> Intake<'a> {
317 pub fn open(stores: &'a ServerStores, owner: &str) -> anyhow::Result<Option<Intake<'a>>> {
321 let Some(held) = Held::take(stores.root()) else {
322 return Ok(None);
323 };
324 let next = super::restore_begin_by(stores, owner)?;
325 let kept = sanitize(stores.root(), next)?;
326 if let Some((rec, _)) = &kept {
327 tracing::info!(
328 target: gunnar_core::observe::TARGET_TWIN,
329 seq = rec.seq, held = rec.durable, blob_len = rec.blob_len,
330 "a restore stream resumes an entry this box holds part of: {} of {} bytes of seq {}", rec.durable, rec.blob_len, rec.seq
331 );
332 }
333 let mut intake = Intake {
334 stores,
335 _held: held,
336 next,
337 kept,
338 open: None,
339 applied: 0,
340 lost: 0,
341 snap: None,
342 snap_touched: false,
343 };
344 if super::is_restoring(stores.root()) {
348 intake.snap = super::snapshot::RestoreSnapshot::resume(stores.root())?;
349 }
350 if let Some(s) = &intake.snap {
351 let (items, bytes) = s.position();
352 tracing::info!(
353 target: gunnar_core::observe::TARGET_TWIN,
354 id = s.id(), items, bytes, whole = s.whole().is_some(),
355 "a restore stream resumes the twin's snapshot this box holds part of: {items} item(s) and {bytes} byte(s) of the next"
356 );
357 if s.whole().is_some() {
358 intake
359 .install_snapshot()
360 .map_err(|e| anyhow::anyhow!("{e}"))?;
361 }
362 }
363 Ok(Some(intake))
364 }
365
366 fn install_snapshot(&mut self) -> ControlResult<()> {
369 let Some(s) = self.snap.take() else {
370 return Ok(());
371 };
372 let id = s.id().to_owned();
373 let staged = s
374 .finish()
375 .map_err(|err| ControlError::Internal(format!("the twin's snapshot {id}: {err:#}")))?;
376 let at = staged.at;
377 super::snapshot::install(self.stores, &staged).map_err(|err| {
378 ControlError::Internal(format!("installing the twin's snapshot {id}: {err:#}"))
379 })?;
380 drop(staged);
381 super::snapshot::forget_restore_snapshot_or_warn(self.stores.root());
382 tracing::warn!(target: gunnar_core::observe::TARGET_TWIN, id = %id, at, "the twin's snapshot, staged across streams, is installed: this box holds everything up to seq {at}; its log follows from {}", at + 1);
383 self.next = at + 1;
384 Ok(())
385 }
386
387 fn take_snapshot_piece(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()> {
391 let root = self.stores.root();
392 let id = chunk.snapshot_id.trim();
393 let (item, offset) = (chunk.snapshot_item.unwrap_or(0), chunk.offset.unwrap_or(0));
394 self.snap_touched = true;
395 if self.snap.as_ref().is_none_or(|s| s.id() != id) {
396 if (item, offset) != (0, 0) {
397 let held = self
398 .snap
399 .as_ref()
400 .map(|s| format!("part of snapshot {}", s.id()))
401 .unwrap_or_else(|| "none of it".into());
402 return Err(ControlError::FailedPrecondition(format!(
403 "a piece at item {item} byte {offset} of the twin's snapshot {id} arrived, and this box holds {held}; resend snapshot {id} from its first item"
404 )));
405 }
406 if let Some(old) = self.snap.take() {
407 tracing::warn!(target: gunnar_core::observe::TARGET_TWIN, old = old.id(), new = id, "the twin's snapshot is another one now; what this box staged of the old one is dropped and the new one is taken from its first item");
408 }
409 self.snap = Some(
410 super::snapshot::RestoreSnapshot::start(root, id).map_err(|err| {
411 ControlError::Internal(format!("staging the twin's snapshot {id}: {err:#}"))
412 })?,
413 );
414 }
415 let s = self.snap.as_mut().expect("a snapshot is staged");
416 let (done, held) = s.position();
417 if item < done {
418 return Ok(());
419 }
420 if item > done || offset > held {
421 return Err(ControlError::FailedPrecondition(format!(
422 "a piece at item {item} byte {offset} of the twin's snapshot {id} arrived, and this box holds {done} item(s) and {held} byte(s) of the next; resend from item {done} byte {held}"
423 )));
424 }
425 let skip = (held - offset).min(chunk.blob.len() as u64) as usize;
426 if skip == chunk.blob.len() && !chunk.last {
427 return Ok(());
428 }
429 let kind = u16::try_from(chunk.kind).map_err(|_| {
430 ControlError::InvalidArgument(format!("snapshot piece of kind {}", chunk.kind))
431 })?;
432 let piece = super::snapshot::Piece {
433 kind,
434 meta: &chunk.meta,
435 blob_len: chunk.blob_len,
436 blob: &chunk.blob[skip..],
437 last: chunk.last,
438 };
439 if let Err(err) = s.feed(piece, chunk.entry_crc) {
440 if let Some(bad) = err.downcast_ref::<super::snapshot::ItemCrcMismatch>() {
441 let said = bad.to_string();
442 self.snap = None;
443 super::snapshot::forget_restore_snapshot_or_warn(root);
444 return Err(ControlError::FailedPrecondition(format!(
445 "{said}: nothing of the twin's snapshot {id} is installed — this box drops what it staged of it and takes it again from its first item"
446 )));
447 }
448 return Err(ControlError::InvalidArgument(format!(
449 "the twin's snapshot {id}, item {item}: {err:#}"
450 )));
451 }
452 if s.whole().is_some() {
453 self.install_snapshot()?;
454 }
455 Ok(())
456 }
457
458 pub fn next(&self) -> u64 {
460 self.next
461 }
462
463 pub fn take(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()> {
465 if chunk.seq == 0 {
466 if let Some(o) = &self.open {
467 return Err(ControlError::InvalidArgument(format!(
468 "a snapshot piece arrived with seq {} unfinished",
469 o.seq
470 )));
471 }
472 if !chunk.snapshot_id.trim().is_empty() {
474 return self.take_snapshot_piece(chunk);
475 }
476 let kind = u16::try_from(chunk.kind).map_err(|_| {
477 ControlError::InvalidArgument(format!("snapshot piece of kind {}", chunk.kind))
478 })?;
479 let piece = super::snapshot::Piece {
480 kind,
481 meta: &chunk.meta,
482 blob_len: chunk.blob_len,
483 blob: &chunk.blob,
484 last: chunk.last,
485 };
486 if let Some(at) = super::snapshot::restore_piece(self.stores, piece).map_err(|err| {
487 ControlError::Internal(format!("installing the snapshot: {err:#}"))
488 })? {
489 self.next = at + 1;
490 }
491 return Ok(());
492 }
493 let kind = u16::try_from(chunk.kind).map_err(|_| {
494 ControlError::InvalidArgument(format!(
495 "seq {}: kind {} is not a frame kind",
496 chunk.seq, chunk.kind
497 ))
498 })?;
499 match &self.open {
500 Some(o) if o.seq == chunk.seq => {
501 if o.kind != kind || o.blob_len != chunk.blob_len {
502 return Err(ControlError::InvalidArgument(format!(
503 "seq {}: a later piece says kind {kind} of {} bytes, the first said kind {} of {}",
504 o.seq, chunk.blob_len, o.kind, o.blob_len
505 )));
506 }
507 if let Some(at) = chunk.offset {
508 if at != o.pos {
509 return Err(ControlError::InvalidArgument(format!(
510 "seq {}: a piece at byte {at} arrived where byte {} was due",
511 o.seq, o.pos
512 )));
513 }
514 }
515 }
516 Some(o) => {
517 return Err(ControlError::InvalidArgument(format!(
518 "seq {} arrived with seq {} unfinished",
519 chunk.seq, o.seq
520 )))
521 }
522 None => self.open_entry(chunk, kind)?,
523 }
524 let stores = self.stores;
525 let root = stores.root();
526 let o = self.open.as_mut().expect("an entry is open");
527 let len = chunk.blob.len() as u64;
528 if o.pos + len > o.blob_len {
529 return Err(ControlError::InvalidArgument(format!(
530 "seq {}: more than the {} blob bytes it declared arrived",
531 o.seq, o.blob_len
532 )));
533 }
534 match &mut o.body {
535 Body::Skip => {}
536 Body::Memory(v) => v.extend_from_slice(&chunk.blob),
537 Body::Spool { file, rec, written } => {
538 if o.pos + len > *written {
540 let from = (*written - o.pos) as usize;
541 file.write_all(&chunk.blob[from..])
542 .map_err(|e| internal(o.seq, "writing the staged entry", e))?;
543 *written = o.pos + len;
544 if *written - rec.durable >= COMMIT_EVERY {
545 commit(root, file, rec, *written)
546 .map_err(|e| internal(o.seq, "committing the staged entry", e))?;
547 }
548 }
549 }
550 }
551 o.pos += len;
552 if chunk.last {
553 self.finish_entry(chunk.entry_crc)?;
554 }
555 Ok(())
556 }
557
558 fn open_entry(&mut self, chunk: &gunnar_proto::LogChunk, kind: u16) -> ControlResult<()> {
560 let seq = chunk.seq;
561 let start = chunk.offset.unwrap_or(0);
562 let explicit = chunk.offset.is_some();
563 let mut open = Open {
564 seq,
565 kind,
566 meta: chunk.meta.clone(),
567 blob_len: chunk.blob_len,
568 pos: start,
569 explicit,
570 held_before: 0,
571 body: Body::Skip,
572 };
573 if start > chunk.blob_len {
574 return Err(ControlError::InvalidArgument(format!(
575 "seq {seq}: a piece at byte {start} of a {}-byte blob",
576 chunk.blob_len
577 )));
578 }
579 if seq < self.next {
580 self.open = Some(open);
581 return Ok(());
582 }
583 if seq > self.next {
584 return Err(ControlError::FailedPrecondition(format!(
585 "seq {seq} arrived where {} was due; resend from {}",
586 self.next, self.next
587 )));
588 }
589 let stores = self.stores;
590 let root = stores.root();
591 let held = match self.kept.take() {
593 Some((rec, file))
594 if rec.seq == seq
595 && rec.kind == kind
596 && rec.blob_len == chunk.blob_len
597 && (chunk.meta.is_empty() || chunk.meta == rec.meta) =>
598 {
599 Some((rec, file))
600 }
601 Some((rec, _)) => {
602 tracing::warn!(
603 target: gunnar_core::observe::TARGET_TWIN,
604 seq, staged_kind = rec.kind, staged_len = rec.blob_len, kind, blob_len = chunk.blob_len,
605 "the staged restore entry is not the entry now arriving at its number; it is dropped and the entry starts at byte 0"
606 );
607 forget(root).map_err(|e| internal(seq, "dropping the staged entry", e))?;
608 None
609 }
610 None => None,
611 };
612 let held_bytes = held.as_ref().map(|(rec, _)| rec.durable).unwrap_or(0);
613 if start > held_bytes {
614 self.kept = held;
615 return Err(ControlError::FailedPrecondition(format!(
616 "seq {seq}: this stream starts it at byte {start}, and this box holds {held_bytes} byte(s) of it; resend seq {seq} from byte {held_bytes}"
617 )));
618 }
619 open.held_before = held_bytes;
620 open.body = match held {
621 Some((rec, file)) => {
622 if open.meta.is_empty() {
623 open.meta.clone_from(&rec.meta);
624 }
625 Body::Spool {
626 file,
627 rec,
628 written: held_bytes,
629 }
630 }
631 None if start == 0
632 && (chunk.blob_len <= IN_MEMORY_UP_TO
633 || (chunk.last && chunk.blob.len() as u64 == chunk.blob_len)) =>
634 {
635 Body::Memory(Vec::with_capacity(gunnar_proto::reserve_for(
636 chunk.blob_len,
637 )))
638 }
639 None => {
640 let rec = Staged {
641 seq,
642 kind,
643 blob_len: chunk.blob_len,
644 meta: open.meta.clone(),
645 durable: 0,
646 };
647 let file =
648 start_spool(root, &rec).map_err(|e| internal(seq, "staging the entry", e))?;
649 Body::Spool {
650 file,
651 rec,
652 written: 0,
653 }
654 }
655 };
656 self.open = Some(open);
657 Ok(())
658 }
659
660 fn finish_entry(&mut self, entry_crc: Option<u32>) -> ControlResult<()> {
662 let stores = self.stores;
663 let root = stores.root();
664 let o = self.open.take().expect("an entry is open");
665 if o.pos != o.blob_len {
666 return Err(ControlError::InvalidArgument(format!(
667 "seq {}: {} of {} blob bytes arrived",
668 o.seq, o.pos, o.blob_len
669 )));
670 }
671 let (blob, staged) = match o.body {
672 Body::Skip => return Ok(()),
673 Body::Memory(v) => (v, false),
674 Body::Spool {
675 mut file,
676 mut rec,
677 written,
678 } => {
679 if written != o.blob_len {
680 return Err(ControlError::InvalidArgument(format!(
681 "seq {}: {written} of {} blob bytes are staged",
682 o.seq, o.blob_len
683 )));
684 }
685 commit(root, &mut file, &mut rec, written)
688 .map_err(|e| internal(o.seq, "committing the staged entry", e))?;
689 let mut blob = Vec::with_capacity(gunnar_proto::reserve_for(o.blob_len));
690 file.seek(SeekFrom::Start(0))
691 .and_then(|_| file.read_to_end(&mut blob))
692 .map_err(|e| internal(o.seq, "reading the staged entry back", e))?;
693 if blob.len() as u64 != o.blob_len {
694 forget(root).map_err(|e| internal(o.seq, "dropping the staged entry", e))?;
695 return Err(ControlError::FailedPrecondition(format!(
696 "seq {}: {} of {} staged bytes read back; they are dropped and seq {} starts again from byte 0",
697 o.seq,
698 blob.len(),
699 o.blob_len,
700 o.seq
701 )));
702 }
703 (blob, true)
704 }
705 };
706 if let Some(want) = entry_crc {
707 let mut crc = Crc32c::new();
708 crc.update(&o.meta);
709 crc.update(&blob);
710 let got = crc.finish();
711 if got != want {
712 if staged {
713 forget(root).map_err(|e| internal(o.seq, "dropping the staged entry", e))?;
714 }
715 return Err(ControlError::FailedPrecondition(format!(
716 "seq {}: the {} bytes this box put together do not match the twin's crc ({got:08x}, the twin's {want:08x}); \
717 nothing was applied, the staged bytes are dropped and seq {} starts again from byte 0",
718 o.seq, o.blob_len, o.seq
719 )));
720 }
721 }
722 let e = Entry {
723 seq: o.seq,
724 kind: o.kind,
725 meta: o.meta,
726 blob,
727 };
728 match super::restore_apply(self.stores, &e) {
729 Ok(Applied::Lost(_)) => self.lost += 1,
730 Ok(_) => {}
731 Err(err) => {
732 return Err(ControlError::Internal(format!(
733 "restoring seq {}: {err:#}",
734 e.seq
735 )))
736 }
737 }
738 if staged {
739 tracing::info!(target: gunnar_core::observe::TARGET_TWIN, seq = e.seq, blob_len = e.blob.len(), "a restore entry staged across streams is whole, checked and applied");
740 }
741 if staged || root.join(PARTIAL_DIR).exists() {
743 forget(root)
744 .map_err(|err| internal(e.seq, "removing the staged entry after its apply", err))?;
745 }
746 self.applied += 1;
747 self.next += 1;
748 Ok(())
749 }
750
751 pub fn end_of_stream(&mut self) -> ControlResult<()> {
754 let stores = self.stores;
755 let root = stores.root();
756 if self.snap_touched {
758 if let Some(s) = self.snap.as_mut() {
759 s.commit().map_err(|err| {
760 ControlError::Internal(format!(
761 "committing the twin's snapshot {}: {err:#}",
762 s.id()
763 ))
764 })?;
765 let (items, bytes) = s.position();
766 return Err(ControlError::FailedPrecondition(format!(
767 "the stream ended inside the twin's snapshot {}: this box holds {items} item(s) of it and {bytes} byte(s) of the next, \
768 durably; the next stream resumes there (Bootstrap.Status restore_snapshot_items/bytes, Mirror.SnapshotFetch \
769 from_item/from_offset) — nothing of it is sent twice",
770 s.id()
771 )));
772 }
773 }
774 let Some(mut o) = self.open.take() else {
775 return Ok(());
776 };
777 match &mut o.body {
778 Body::Skip => Err(ControlError::InvalidArgument(format!(
779 "the stream ended inside seq {}, which this box holds already; resend from the box's head + 1 ({})",
780 o.seq, self.next
781 ))),
782 Body::Memory(_) => Err(ControlError::InvalidArgument(format!(
783 "the stream ended inside seq {} ({} of {} bytes, small enough to be gathered in memory, and dropped); \
784 resend from the box's head + 1 ({})",
785 o.seq, o.pos, o.blob_len, self.next
786 ))),
787 Body::Spool { file, rec, written } => {
788 commit(root, file, rec, *written).map_err(|e| internal(o.seq, "committing the staged entry", e))?;
789 if !o.explicit && o.held_before > 0 && *written <= o.held_before {
790 return Err(ControlError::FailedPrecondition(format!(
791 "REFUSED [{TWIN_TOO_OLD}] the twin is too old for a resumed restore; upgrade the twin. Seq {seq} is \
792 {len} bytes and needs more than one call: this box holds {held} of them, durably, but this stream \
793 sent the entry from its first byte (no `offset` on its chunks — a twin, or a restore lane, built \
794 before resumed restores) and ended before reaching byte {held}, so skipping what is held gains \
795 nothing and every resend ends the same way. With the twin upgraded the next stream resumes seq \
796 {seq} at byte {held} (Mirror.LogFetch from_offset); nothing here is lost meanwhile",
797 seq = o.seq,
798 len = o.blob_len,
799 held = o.held_before,
800 )));
801 }
802 Err(ControlError::FailedPrecondition(format!(
803 "the stream ended inside seq {seq}: this box holds {w} of its {len} bytes, durably; the next stream \
804 resumes seq {seq} at byte {w} (Bootstrap.Status restore_partial_bytes, Mirror.LogFetch from_offset) — \
805 nothing of it is sent twice",
806 seq = o.seq,
807 w = written,
808 len = o.blob_len,
809 )))
810 }
811 }
812 }
813
814 pub fn counts(&self) -> (u64, u64) {
816 (self.applied, self.lost)
817 }
818}
819
820impl Drop for Intake<'_> {
821 fn drop(&mut self) {
824 let root = self.stores.root().to_path_buf();
825 if let Some(Open {
826 seq,
827 body: Body::Spool { file, rec, written },
828 ..
829 }) = self.open.as_mut()
830 {
831 if *written > rec.durable {
832 if let Err(err) = commit(&root, file, rec, *written) {
833 tracing::warn!(target: gunnar_core::observe::TARGET_TWIN, seq = *seq, %err, "the staged restore entry could not be committed as its stream ended; the next stream resumes from its last commit");
834 }
835 }
836 }
837 }
838}
839
840impl gunnar_grpc::RestoreIntake for Intake<'_> {
841 fn take(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()> {
842 Intake::take(self, chunk)
843 }
844 fn end_of_stream(&mut self) -> ControlResult<()> {
845 Intake::end_of_stream(self)
846 }
847 fn counts(&self) -> (u64, u64) {
848 Intake::counts(self)
849 }
850}
851
852fn commit(root: &Path, file: &mut File, rec: &mut Staged, written: u64) -> anyhow::Result<()> {
854 file.sync_data().context("fdatasync of the staged entry")?;
855 if rec.durable != written {
856 rec.durable = written;
857 write_record(root, rec)?;
858 }
859 Ok(())
860}
861
862fn start_spool(root: &Path, rec: &Staged) -> anyhow::Result<File> {
864 forget(root)?;
865 write_record(root, rec)?;
866 let path = root.join(PARTIAL_DIR).join(DATA);
867 let file = OpenOptions::new()
868 .read(true)
869 .write(true)
870 .create(true)
871 .truncate(true)
872 .open(&path)
873 .with_context(|| format!("creating {}", path.display()))?;
874 crate::durable::sync_dir(&root.join(PARTIAL_DIR))
876 .with_context(|| format!("flushing {} into place", path.display()))?;
877 Ok(file)
878}
879
880fn internal(seq: u64, what: &str, e: impl Into<anyhow::Error>) -> ControlError {
881 ControlError::Internal(format!("seq {seq}: {what}: {:#}", e.into()))
882}