Skip to main content

gunnar_server/twin/
intake.rs

1//! **The restore's intake: an entry larger than one call, staged** (restore-large-entry,
2//! 2026-10-07).
3//!
4//! # The defect
5//!
6//! A main recharged from its twin through monetize's restore lane (the gunnar guest) is fed by
7//! guest calls that the host cuts: each stream the guest opens is cancelled 30 s after it opens
8//! (the doors' own timeout; the whole call is cut at 60 s). The lane resumed per ENTRY — "a replay
9//! too long for one call resumes on the next tick from head + 1" — and `Bootstrap.Restore`
10//! gathered each entry in memory and dropped it when the stream ended inside it. So ONE entry
11//! that takes longer than a call to stream (a 200 MiB pack over a slow link, MEASURED by the r5
12//! integration test, row R1-recharge-main-lost: 41 calls, every one "the stream ended inside an
13//! entry; resend from the box's head + 1") could never be restored, and the main never came back.
14//!
15//! # What this does
16//!
17//! An entry that spans more than one piece (or more than [`IN_MEMORY_UP_TO`]) is written to
18//! `<data-root>/`[`PARTIAL_DIR`] as it arrives — its bytes in `entry.blob`, and in `entry.rec`
19//! what they are (seq, kind, blob length, meta) and how many of them are DURABLE. The record is
20//! rewritten (atomically, fsynced) after the bytes it counts were `fdatasync`ed: every
21//! [`COMMIT_EVERY`], when the stream ends however it ends (a clean end, a broken one, the sender's
22//! cancel — tonic reads a client's RST_STREAM as a clean end — or a refusal), and when the intake
23//! is dropped. The next stream finds it: `Bootstrap.Status.restore_partial_{seq,bytes}` says
24//! where to resume, the lane asks the twin for `Mirror.LogFetch{from: seq, from_offset: bytes}`,
25//! and a call cut at its deadline loses at most the piece that was in flight — never the entry.
26//!
27//! The entry is applied ONLY when it is whole: its length is the declared one and, when the
28//! sender stamped one (`LogChunk.entry_crc`, the twin's own payload crc, checked before the last
29//! piece left it), the crc32c over `meta ‖ blob` put together here is that crc. Then it is
30//! appended at its number and applied ([`super::restore_apply`]), and only then is the staging
31//! area removed — durably, so nothing leaks onto the volume.
32//!
33//! # Idempotent, crash-safe
34//!
35//! * An entry is applied once: `append_at` takes only `head + 1`, an entry at or below the head
36//!   is checked and dropped, and the staged entry is the one at `head + 1` or it is thrown away
37//!   when the intake opens.
38//! * A crash between a write and its commit leaves bytes past the recorded count; the intake
39//!   truncates the file to the count when it opens. A file SHORTER than its record (a filesystem
40//!   that lied) is not trusted: the entry starts again at byte 0.
41//! * A crash between the apply and the removal leaves a staging area for an entry at or below
42//!   the head; the next intake removes it.
43//!
44//! # Older peers
45//!
46//! * An older TWIN ignores `from_offset` and sends the entry from byte 0 with no `offset` on its
47//!   chunks; the bytes this box holds are skipped as they arrive. Skipping makes the stream no
48//!   shorter, so an entry that needs more than one call STILL cannot pass an older twin: when a
49//!   stream of such a sender ends without one byte past what was held, the refusal says so by
50//!   name ([`TWIN_TOO_OLD`]) instead of asking for the same resend for ever. The estate upgrades
51//!   its twin together with its main.
52//! * An older MAIN ignores the new fields and gathers entries in memory as it always did.
53
54use 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
67/// `<data-root>/txlog-restore-partial/`: the entry a restoring box holds part of. Per box,
68/// never a transaction (`gunnar_core::txlog::is_per_box_path`).
69pub const PARTIAL_DIR: &str = "txlog-restore-partial";
70/// The entry's blob bytes so far.
71const DATA: &str = "entry.blob";
72/// What they are and how many of them are durable.
73const RECORD: &str = "entry.rec";
74/// The first line of [`RECORD`].
75const RECORD_MAGIC: &str = "gunnar-restore-partial v1";
76
77/// Bytes written between two durable commits of the staged entry: what a power cut may cost a
78/// resume, never more.
79pub const COMMIT_EVERY: u64 = 8 << 20;
80
81/// An entry this small that starts at its first byte is gathered in memory, as before: it takes
82/// a few seconds on any link a box is restored over, and staging it would cost two fsyncs per
83/// entry over a log of many small ones. Larger ones (and every resumed one) are staged.
84pub const IN_MEMORY_UP_TO: u64 = 4 << 20;
85
86/// The refusal word for a sender that streams every entry from byte 0 when a staged entry needs
87/// more than one call.
88pub const TWIN_TOO_OLD: &str = "restore-twin-too-old";
89
90/// **What this box holds of an entry it is restoring**, as its record says.
91#[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    /// Bytes of the blob that are on disk AND counted here — the resume point.
98    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
140/// The staged entry's record, or `None` (nothing staged, or a record that does not parse).
141pub 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
148/// **`(seq, bytes held durably)` of the entry a restoring box holds part of** — what
149/// `Bootstrap.Status.restore_partial_{seq,bytes}` says. `None` when nothing is staged.
150pub fn partial(root: &Path) -> Option<(u64, u64)> {
151    staged(root).map(|s| (s.seq, s.durable))
152}
153
154/// **Throw the staging area away, durably**: the files, the directory, then the data root's
155/// entry for it — so a removal after an apply survives a power cut and no staged bytes are left
156/// on the volume.
157pub 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
174/// The record, rewritten atomically and fsynced. The directory is made (and its entry in the
175/// data root made durable) the first time.
176fn 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
186/// **What the box still holds, made trustworthy before a stream uses it**: the staged entry is
187/// the one at `next` (anything else was applied or belongs to another history, and goes), and its
188/// file is cut back to the durable count (a crash may have left a tail nobody counted). A file
189/// shorter than its count is not trusted at all.
190fn 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    // The next byte written is the first one not held.
241    file.seek(SeekFrom::Start(rec.durable))
242        .with_context(|| format!("seeking {}", path.display()))?;
243    Ok(Some((rec, file)))
244}
245
246/// The data roots that have an intake open: ONE stream at a time stages into a box.
247static OPEN: Mutex<Option<HashSet<PathBuf>>> = Mutex::new(None);
248
249/// This root's place in [`OPEN`], given back on drop.
250struct 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
270/// Where the open entry's bytes go.
271enum Body {
272    /// Applied already (`seq` below the next one): its pieces are checked and dropped.
273    Skip,
274    /// Small and starting at byte 0: gathered here.
275    Memory(Vec<u8>),
276    /// Staged on disk: `written` bytes in the file, `rec.durable` of them committed.
277    Spool {
278        file: File,
279        rec: Staged,
280        written: u64,
281    },
282}
283
284/// The entry this stream is receiving.
285struct Open {
286    seq: u64,
287    kind: u16,
288    meta: Vec<u8>,
289    blob_len: u64,
290    /// The byte of the blob this stream's next chunk starts at.
291    pos: u64,
292    /// This stream's chunks name their offsets (a sender that knows resumed restores).
293    explicit: bool,
294    /// What the box held of it before this stream reached it.
295    held_before: u64,
296    body: Body,
297}
298
299/// **One `Bootstrap.Restore` stream's intake on the server** — see the module docs.
300pub struct Intake<'a> {
301    stores: &'a ServerStores,
302    _held: Held,
303    next: u64,
304    /// What the box held when this stream began, until the stream reaches that entry.
305    kept: Option<(Staged, File)>,
306    open: Option<Open>,
307    applied: u64,
308    lost: u64,
309    /// **The twin's base this box holds part of** (restore-after-snapshot, 2026-10-07), staged
310    /// across streams ([`super::snapshot::RestoreSnapshot`]).
311    snap: Option<super::snapshot::RestoreSnapshot>,
312    /// This stream carried pieces of a resumable snapshot.
313    snap_touched: bool,
314}
315
316impl<'a> Intake<'a> {
317    /// **Open the box's intake for one stream**: [`super::restore_begin_by`] (the owner check, the
318    /// next sequence), then what is staged made trustworthy ([`sanitize`]). `Ok(None)`: another
319    /// stream's intake still holds this box.
320    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        // A twin's base staged by earlier streams: cut back to what was made durable — and, when
345        // it is whole (a stream ended, or the box went down, between its last piece and the
346        // install), installed now: the log then follows from its `at + 1`.
347        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    /// **The twin's base is whole: install it** — the box then holds everything up to its `at`,
367    /// and its log follows from `at + 1`. The staging goes with the install.
368    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    /// **One piece of a twin's base, resumable** (`snapshot_id` set): placed by its item and
388    /// offset, the bytes this box holds skipped, a gap refused naming where to resume, the item's
389    /// crc checked at its end, and the base installed at its terminator.
390    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    /// The next sequence this box takes.
459    pub fn next(&self) -> u64 {
460        self.next
461    }
462
463    /// One chunk of the stream.
464    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            // A twin's base, resumable across streams (restore-after-snapshot).
473            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                // Bytes below what the box holds (a sender that started lower) are skipped.
539                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    /// The first chunk of an entry in this stream.
559    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        // What the box already holds of THIS entry: the same number, kind, length and meta.
592        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    /// The open entry's last piece arrived: check it, apply it, forget its staging.
661    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                // Durable before the apply: an apply that fails leaves the whole entry staged,
686                // and the next stream finishes it with one empty last piece.
687                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        // Applied (durable in the log at its number): the staging goes, durably.
742        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    /// The stream ended. `Err` when inside an entry: what this box holds of it, durably, and
752    /// where the next stream resumes — or, for a sender that cannot resume, why it never will.
753    pub fn end_of_stream(&mut self) -> ControlResult<()> {
754        let stores = self.stores;
755        let root = stores.root();
756        // Inside the twin's base: what arrived is made durable, and the box says where it stands.
757        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    /// `(applied, lost)` by this stream.
815    pub fn counts(&self) -> (u64, u64) {
816        (self.applied, self.lost)
817    }
818}
819
820impl Drop for Intake<'_> {
821    /// **A stream that never reached [`Intake::end_of_stream`]** — the handler returned early on a
822    /// broken stream or a refusal, or its future was dropped — still keeps every byte it wrote.
823    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
852/// Make `written` bytes durable and count them.
853fn 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
862/// A fresh staging area for `rec` (anything there before is not this entry).
863fn 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    // The data file's directory entry, durable before any count refers to its bytes.
875    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}