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}