Skip to main content

znippy_plugin_git/
replicate.rs

1//! Replication — sending a repository's stored bytes out to another gunnar.
2//!
3//! # THIS MODULE IS DELIBERATELY EMPTY
4//!
5//! [`replicate_out`] is a signature, a contract and a `todo!()`. It is a
6//! placeholder to be filled in **tomorrow (from 2026-08-07)** and it is not a
7//! forgotten stub: the shape below is the decision that was made today, and the
8//! wire is the part that was not. [`tests::replicate_out_is_deliberately_empty`]
9//! asserts the emptiness, so that if someone half-implements this the test that
10//! says "still empty" fails and has to be removed on purpose.
11//!
12//! **Nothing here opens a socket, and no protocol is designed here.** Do not add
13//! one to this file as a convenience — a peer transport is its own decision.
14//!
15//! ## What was decided, and is therefore in the signature
16//!
17//! * **STORE-side.** Replication is a command on the store that holds the bytes.
18//!   It is not a read path and not a plugin hook; the receiving gunnar is a peer,
19//!   not a client of a lookup.
20//! * **No index travels with it.** Not the Arrow object index, not the stree oid
21//!   section, not the redb tail. The receiver has its own
22//!   [`ObjectReadStack`](crate::read_stack::ObjectReadStack) and rebuilds its own
23//!   projection from what it receives — an index is a derivation, and shipping a
24//!   derivation is how two copies of a repository come to disagree about the
25//!   same bytes. This is the reason [`ReplicationReceipt`] reports objects and
26//!   bytes and says nothing about rows or generations of any index.
27//! * **One transaction, per repository.** The unit is one repository's archive
28//!   between two generation marks: it either lands whole at the peer or it does
29//!   not land. There is no partial-repository state to reconcile, because a
30//!   half-replicated object set is exactly the thing whose repair costs more
31//!   than the resend.
32//! * **Append-only makes it resumable, later.** Because the archive is
33//!   append-only, `from_generation` is a watermark and not a diff: everything
34//!   above it is new and nothing below it has changed. A resumed transaction is
35//!   therefore a fresh one with a higher watermark. That property is why the
36//!   parameter is a single `u64` and not a manifest.
37
38use std::path::Path;
39
40use anyhow::Result;
41
42/// Another gunnar. Deliberately just a name and an address: what an endpoint
43/// string means is the transport's business, and the transport is not decided.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Peer {
46    /// Stable identity of the peer, for logs and for the receipt.
47    pub name: String,
48    /// Where to reach it. Opaque to this crate.
49    pub endpoint: String,
50}
51
52/// What a completed replication transaction reports.
53///
54/// Counts of things that actually moved — objects and bytes — and the watermark
55/// the next transaction should start from. Nothing about indexes: none travelled.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ReplicationReceipt {
58    pub peer: String,
59    /// Objects sent. Zero is a legitimate, successful outcome: the peer was
60    /// already current.
61    pub objects_sent: u64,
62    /// Stored bytes sent, as stored — a delta chunk counts as its delta.
63    pub bytes_sent: u64,
64    /// The watermark this transaction started from.
65    pub from_generation: u64,
66    /// The watermark to pass as `from_generation` next time.
67    pub to_generation: u64,
68}
69
70/// **Send one repository's new objects to `peer`. NOT IMPLEMENTED — see the
71/// module documentation.**
72///
73/// A store-side, all-or-nothing transaction over the bytes of `archive` above
74/// `from_generation`. No index of any kind is transmitted; the peer derives its
75/// own.
76///
77/// # Panics
78///
79/// Always. This is a placeholder with a settled signature and no body yet.
80pub fn replicate_out(
81    _archive: &Path,
82    _peer: &Peer,
83    _from_generation: u64,
84) -> Result<ReplicationReceipt> {
85    todo!(
86        "replication transaction: deliberately empty, to be implemented tomorrow. \
87         The signature is settled (store-side, per repository, watermarked, no index \
88         travels); the transport is not. Do not open a socket from this file without \
89         deciding the protocol first."
90    )
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    /// **The emptiness is asserted, so it cannot be mistaken for an oversight.**
98    ///
99    /// If this test starts failing, `replicate_out` grew a body — which is the
100    /// intended future. Delete this test in the same commit that does it, on
101    /// purpose.
102    ///
103    /// Seen RED by replacing the `todo!()` with `Ok(ReplicationReceipt { … })`:
104    /// "the replication placeholder no longer panics — if it was implemented,
105    /// delete this test in the same commit: ()". Restored.
106    #[test]
107    fn replicate_out_is_deliberately_empty() {
108        let peer = Peer {
109            name: "gunnar-2".into(),
110            endpoint: "unspecified".into(),
111        };
112        let outcome = std::panic::catch_unwind(|| {
113            let _ = replicate_out(Path::new("/nonexistent.znippy"), &peer, 0);
114        });
115        let payload = outcome.expect_err(
116            "the replication placeholder no longer panics — if it was implemented, delete this \
117             test in the same commit",
118        );
119        let msg = payload
120            .downcast_ref::<String>()
121            .cloned()
122            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
123            .unwrap_or_default();
124        assert!(
125            msg.contains("deliberately empty"),
126            "it panics, but not as the placeholder: {msg}"
127        );
128    }
129}