Skip to main content

gunnar_server/
mirror_queue.rs

1//! **One mirror sync at a time, for the whole box.**
2//!
3//! `Mirror.Sync` exists so a console button can pull a repository off another
4//! forge. The moment it exists, ten owners can press ten buttons inside one
5//! second, and a sync is the most expensive button on the box: it is a whole
6//! `git clone` over somebody else's network, into this box's storage engine,
7//! with the pack streaming through the whole time.
8//!
9//! # Why this is the third queue and not a third caller of one queue
10//!
11//! [`crate::job_queue`] argues it in full and this is the mirror's half of that
12//! argument: **the line a job waits in is part of what the job costs.** A gc
13//! waits holding nothing. A purge waits holding a total lockout on an account.
14//! A mirror waits holding a repository that is *not yet usable* — somebody
15//! asked for a copy of an upstream and until the sync runs there is nothing
16//! there to clone. Putting it behind a compaction that is only holding the disk
17//! would be the same mistake in a third colour, so it gets its own worker and
18//! its own line.
19//!
20//! # What is here and what is not
21//!
22//! Not the fetch. That is [`crate::mirror_fetch`], reached through a runner
23//! handed in at construction — the same shape [`crate::gc_queue`] uses, and for
24//! the same two reasons: one writer for what a sync *is* (one implementation), and a queue
25//! that can be tested with no network and no store under it.
26//!
27//! What is here is the mirror's answers to [`JobKind`]'s questions: a sync's
28//! subject is a repository path, a sync's order carries the upstream as well
29//! (so the worker does not have to re-read a registration that may have been
30//! edited while the job waited), and a sync's report is objects, bytes and the
31//! references that moved.
32//!
33//! # `progress`, and the honest shape of it
34//!
35//! A gc and a purge report none — each is one indivisible act as far as a
36//! caller is concerned, and a fabricated `0/1` would be a progress bar that
37//! lies. A mirror cannot do without one: a fetch of a large repository is
38//! minutes of network with a console watching.
39//!
40//! **Both halves are real and live, and they are published from opposite sides
41//! of the storage seam.** `gunnar-wire`'s `FetchProgress` publishes
42//! `objects_total` the instant the packfile header arrives — twelve bytes, read
43//! before an object is stored — so `(0, N)` is known at the *start* of the
44//! transfer. The numerator is the ingest's, because the only code that can
45//! count objects without inflating them a second time is the code that is
46//! already splitting the pack: gunnar has exactly one pack decoder (one implementation), and
47//! `fetch_pack`'s module doc refuses to become the second one for this exact
48//! reason.
49//!
50//! # 🔴 What this used to say, and what closed it
51//!
52//! Until 2026-09-08 this paragraph read *"the numerator has exactly two
53//! values"*, and the pair really did go `(0, N) → (N, N)` — one publish at the
54//! header and one at the finish, with nothing in between. The reason given was
55//! that znippy "takes a pack in one call and reports what it indexed when it
56//! returns", which was true of the *seam* and never true of the engine: znippy
57//! splits the pack entry by entry in `pack_walk`, and has known the running
58//! count all along. It simply had nobody to tell.
59//!
60//! It has now. [`GitOps::put_pack_observed`](git_storage_trait::GitOps::put_pack_observed)
61//! is one defaulted method on the storage contract — no existing implementer
62//! changed, and an engine that has no entry loop keeps publishing nothing and
63//! says so by name through `publishes_pack_progress()`. znippy overrides it and
64//! publishes **once per object** from the walk it was already doing; nothing
65//! new is decompressed and no second decoder exists.
66//! [`crate::mirror_fetch`] passes the sink straight through to the same
67//! `Reporter` the wire's two figures go to.
68//!
69//! So the pair now steps: `(0, N)`, then a value per object as the engine takes
70//! them in, ending at `(N, N)` — and `fetch_pack`'s header-versus-receipt
71//! cross-check at the end is now also the proof that the live count was honest.
72//! [`MirrorReport::bytes`] is still the figure that moves while the *network*
73//! is the bottleneck, and the object count is the one that moves while
74//! *storage* is; between them a console has something live for both halves of a
75//! sync, and neither is fabricated.
76
77use std::sync::Arc;
78use std::time::Duration;
79
80use crate::job_queue::{Job, JobKind, JobQueue, JobState, Reporter};
81
82/// How long a finished sync stays readable. The shared default: ten minutes is
83/// far longer than any console poll interval and far shorter than a session.
84pub const FINISHED_RETENTION: Duration = <Mirror as JobKind>::FINISHED_RETENTION;
85
86/// Finished syncs kept regardless of age, most recent first.
87pub const FINISHED_KEPT: usize = <Mirror as JobKind>::FINISHED_KEPT;
88
89/// Where a sync is. Mirrors `gunnar.v1.MirrorJobState`; the mapping to the wire
90/// lives in `gunnar-grpc` so this crate does not depend on the protos.
91///
92/// The four words are [`JobState`]'s, shared with the gc and the purge, so
93/// three status RPCs cannot drift apart about what `Done` means.
94pub type MirrorState = JobState;
95
96/// One sync, queued or completed.
97///
98/// A [`Job`] of kind [`Mirror`]: the queue's own fields (`job_id`, `state`,
99/// `queue_position`, `progress`, `error`, the timestamps) plus a
100/// [`MirrorReport`], which dereferences flat. The repository is `job.key`,
101/// exactly as the queue keys on it.
102pub type MirrorJob = Job<Mirror>;
103
104/// **What a sync was asked for.**
105///
106/// The upstream travels on the order rather than being re-read by the worker,
107/// and that is the rule [`JobKind::Order`] states: a registration can be
108/// changed while its job waits, and a job that quietly fetched from a different
109/// remote than the one the caller asked about would be a mirror nobody could
110/// audit.
111#[derive(Debug, Clone)]
112pub struct MirrorOrder {
113    /// `<namespace>/<name>` — the subject, and therefore the dedup key.
114    pub path: String,
115    /// Where the upstream is, as the registration held it when `Sync` was
116    /// called.
117    pub upstream: String,
118    /// Delete local mirrored references the upstream no longer advertises.
119    pub prune: bool,
120}
121
122/// **What one sync produced.**
123///
124/// The kind's own fields; the runner never reads them. Nothing here is
125/// restated from the order — every number is measured by the fetch.
126#[derive(Debug, Clone, Default)]
127pub struct MirrorReport {
128    /// Objects the packfile carried, as both the header declared and the ingest
129    /// indexed — `fetch_pack` refuses the transfer if those two disagree, so
130    /// there is one number here and not two.
131    ///
132    /// 0 on a mirror that was already up to date: no want, no pack, no walk
133    /// charged to the remote. That is the ordinary answer for a poll.
134    pub objects: u64,
135    /// Packfile bytes taken off the wire, header and trailer included. **This
136    /// is the figure that moves while the job runs.**
137    pub bytes: u64,
138    /// Every reference this sync moved, as `(name, old, new)` in hex, with an
139    /// all-zeroes `new` for a deletion and an all-zeroes `old` for a creation —
140    /// the `gunnar.v1.RefUpdate` convention, so the seam does no second
141    /// encoding.
142    pub updates: Vec<(String, String, String)>,
143    /// **Why the sync was refused by the namespace's own plan**, when it was
144    /// (2026-10-04) — `MirrorJob.refused_reason` on the wire, so a console
145    /// reads a field instead of parsing the sentence in `error`.
146    pub refused: Refusal,
147}
148
149/// Why a sync was REFUSED by the namespace's plan, as against failing.
150#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
151pub enum Refusal {
152    /// Not refused by the plan: it succeeded, or it failed for another reason.
153    #[default]
154    None,
155    /// The plan refuses writes at all (suspended, lapsed).
156    Plan,
157    /// The namespace is at its pack room, or the fetch was cut at the ceiling.
158    PackCap,
159}
160
161/// **What a sync actually is**, handed in so this module does not have to know.
162///
163/// Takes the order and the job's reporter and either succeeds or returns the
164/// sentence a console will show. The real one is
165/// [`crate::mirror_fetch::sync_once`].
166pub type MirrorRunner =
167    Arc<dyn Fn(&MirrorOrder, &Reporter<Mirror>) -> Result<(), String> + Send + Sync>;
168
169/// **The mirror kind.** Everything the shared runner needs to know about a
170/// sync, and nothing else.
171pub struct Mirror {
172    run: MirrorRunner,
173}
174
175impl JobKind for Mirror {
176    const NAME: &'static str = "mirror";
177
178    type Order = MirrorOrder;
179    type Report = MirrorReport;
180
181    /// The repository. A second registration for the same repository replaces
182    /// the first, so a repository has at most one upstream and the path is the
183    /// whole subject.
184    fn key(order: &MirrorOrder) -> &str {
185        &order.path
186    }
187
188    fn run(&self, order: &MirrorOrder, report: &Reporter<Mirror>) -> Result<(), String> {
189        (self.run)(order, report)
190    }
191}
192
193/// The queue: one worker, one sync at a time, FIFO, deduplicated by repository.
194///
195/// A named type over [`JobQueue<Mirror>`] rather than an alias, so `Mirror.Sync`'s
196/// door keeps speaking in orders instead of making every caller reach for the
197/// generic.
198#[derive(Clone)]
199pub struct MirrorQueue(JobQueue<Mirror>);
200
201impl MirrorQueue {
202    /// A queue over `run`. No thread is started until something is enqueued.
203    pub fn new(run: MirrorRunner) -> Self {
204        MirrorQueue(JobQueue::new(Mirror { run }))
205    }
206
207    /// **Ask for `order.path` to be synced, and return without syncing it.**
208    ///
209    /// Returns the job now responsible for the repository: a new one, or the
210    /// one that was already queued or running. Never enqueues a second sync for
211    /// a repository that has one live — a double-clicked button would otherwise
212    /// pull the same pack over somebody else's network twice.
213    pub fn enqueue(&self, order: MirrorOrder) -> MirrorJob {
214        self.0.enqueue(order)
215    }
216
217    /// The job the queue is holding for `path`, if it still has one.
218    pub fn status(&self, path: &str) -> Option<MirrorJob> {
219        self.0.status(path)
220    }
221
222    /// The highest number of syncs that have ever been running together.
223    ///
224    /// **Must be 1 after any amount of traffic.** 0 before anything has run.
225    pub fn peak_in_flight(&self) -> usize {
226        self.0.peak_in_flight()
227    }
228
229    /// How many times the runner has been called — i.e. how many syncs actually
230    /// happened. The applied output the dedup gate asserts on.
231    pub fn runs(&self) -> u64 {
232        self.0.runs()
233    }
234
235    /// Stop the worker after the job it is on.
236    pub fn stop(&self) {
237        self.0.stop()
238    }
239}