Skip to main content

gunnar_core/
flow.rs

1//! **The flow ledger: what MOVED through a namespace, per UTC day.**
2//!
3//! # The hole this fills
4//!
5//! Every meter gunnar had before this file was a **stock** meter: `pack_bytes`,
6//! `cache_bytes`, `cold_bytes`, `lfs_bytes`, `tombstoned_bytes`, `objects` and
7//! `repos` all answer *how much is sitting on the disk right now*. They are the
8//! right numbers to bill from and the wrong ones to run a service on, because
9//! every one of them reads the same for a tenant who pushed this morning and a
10//! tenant who last touched the server in March. A silent customer and a busy
11//! one are indistinguishable in a stock meter, and so are a customer who is
12//! working and a customer who is being refused at the door.
13//!
14//! This module counts the other kind: **flow** — bytes in, bytes out, pushes,
15//! fetches, refusals, and when the tenant was last seen. Nothing here is ever a
16//! wall. Every counter is `Enforcement::Measured` in monetize's vocabulary
17//! (`monetize_product::Enforcement`, which gunnar took onto the wire in
18//! `usage.proto` on purpose): they are counted and reported, and nothing on this
19//! box refuses because of one. Promoting any of them to `Enforced` would need a
20//! refusal to exist first, and there is none.
21//!
22//! # Two halves, and why
23//!
24//! * [`FlowLedger`] is the LIVE half: pre-sized atomics, one slot per namespace
25//!   that has been touched since boot. A transfer bumps a handful of
26//!   `fetch_add`s under a read lock and allocates nothing. This is what the
27//!   transfer paths call.
28//! * [`FlowStore`] is the KEPT half: one document per namespace in the control
29//!   store (redb, the same store family the catalog, the auth table and the
30//!   entitlement facts live in), holding up to [`RETENTION_DAYS`] days of
31//!   [`FlowDay`] rows.
32//!
33//! A periodic flush copies the live slot into the store. **It copies, it does
34//! not add**: the live slot holds the whole of today, and the stored row for
35//! today is a snapshot of it, so a flush that runs twice writes the same number
36//! twice rather than doubling it. That is the property that makes a restart
37//! safe in both directions — nothing is counted twice, and nothing older than
38//! one flush interval is lost, because a slot opened after a restart is SEEDED
39//! from the stored row for today rather than from zero.
40//!
41//! # The day boundary
42//!
43//! Days are UTC, numbered as whole days since the Unix epoch ([`day_of`]). A
44//! transfer is charged to the day its *record* is taken, which is the day it
45//! finished. A transfer that starts at 23:59 and ends at 00:01 lands wholly in
46//! the second day; splitting one transfer across two days would need the byte
47//! counters to be sampled on a clock, and a meter that is sampled is a meter
48//! that can be read while it is being written.
49//!
50//! # What is NOT here
51//!
52//! **A key that resolves to nobody has no namespace**, and this ledger is per
53//! namespace. `ssh.auth.refused` with `reason = "unknown-key"` happens before
54//! any repository is named — there is no exec line yet — so it cannot be
55//! charged to a tenant and is not counted here. It is counted server-wide by
56//! `ServerMetrics::add_connections_refused`, where it belongs.
57//! [`FlowDay::auth_failures`] is the OTHER half of the same sentence: a
58//! principal that authenticated and then held no grant on the repository it
59//! asked for. That one names a namespace, so that one is a tenant's number.
60
61use std::collections::{BTreeMap, BTreeSet, HashMap};
62use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
63use std::sync::{Arc, Mutex, RwLock};
64
65use crate::control::ControlStore;
66use crate::doc::{impl_doc, DocRef, ShardedMap};
67use crate::error::Result;
68
69/// **The name the flow document is stored under** in the control store.
70pub const FLOW_REF: &str = "refs/gunnar/flow";
71
72/// Milliseconds in a day. UTC, no leap seconds: the wire carries Unix
73/// milliseconds and a Unix millisecond has never counted one.
74pub const DAY_MS: i64 = 86_400_000;
75
76/// **How many days of flow are kept per namespace: 400.**
77///
78/// The same window `monetize_server::state::UsageStore::RETENTION_MS` keeps its
79/// readings for, and for the same reason: a shade over a year, so a question
80/// about "this month last year" can still be answered, and bounded, so a table
81/// written on every transfer is not a disk-fill bug with a schedule.
82pub const RETENTION_DAYS: i64 = 400;
83
84/// **The most principals one namespace-day counts by name.**
85///
86/// [`FlowDay::principals`] is an EXACT set rather than a sketch. A HyperLogLog
87/// would answer "roughly how many" in a fixed 1–2 KiB; a `BTreeSet` of
88/// fingerprints answers "exactly how many, and which" in about 50 B each. At
89/// the scale a git server has — a namespace is a team, and a team with ten
90/// thousand distinct keys pushing in one day is not a team — exact costs less
91/// than the sketch and answers a strictly harder question, which is the whole
92/// argument. The ceiling exists so that the pathological case is bounded rather
93/// than unbounded, and when it is hit [`FlowDay::principals_capped`] says so,
94/// because a count that stopped growing must never be read as a total.
95pub const PRINCIPAL_CEILING: usize = 10_000;
96
97/// The UTC day a millisecond timestamp falls in, as whole days since the epoch.
98/// Euclidean, so a timestamp before 1970 floors rather than truncating toward
99/// zero and cannot share a day number with one after it.
100pub fn day_of(unix_ms: i64) -> i64 {
101    unix_ms.div_euclid(DAY_MS)
102}
103
104/// Midnight UTC of a day number, in Unix milliseconds — the value the wire
105/// carries as `DayActivity.day_unix_ms`.
106pub fn midnight_of(day: i64) -> i64 {
107    day.saturating_mul(DAY_MS)
108}
109
110/// **Unix milliseconds → `YYYY-MM-DDTHH:MM:SSZ`, in std alone.**
111///
112/// Eleven lines of arithmetic rather than a date crate in every consumer's
113/// graph, and the ONE runtime copy of it: `gunnar control usage` and `gunnar
114/// control activity` both print timestamps off this, so the two cannot render
115/// the same instant two ways. (`gunnar-common/build.rs` has the same
116/// arithmetic and must: a build script cannot link the crate it is building.)
117///
118/// The civil date is Howard Hinnant's `civil_from_days`, exact for every
119/// representable day and needing no table. Pre-1970 falls out correctly because
120/// the era arithmetic floors.
121pub fn iso8601_utc(unix_ms: i64) -> String {
122    let secs = unix_ms.div_euclid(1000);
123    let days = secs.div_euclid(86_400);
124    let rem = secs.rem_euclid(86_400);
125    let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60);
126    // Shift the epoch to 0000-03-01 so leap days land at the end of the cycle.
127    let z = days + 719_468;
128    let era = z.div_euclid(146_097);
129    let doe = z.rem_euclid(146_097);
130    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
131    let y = yoe + era * 400;
132    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
133    let mp = (5 * doy + 2) / 153;
134    let d = doy - (153 * mp + 2) / 5 + 1;
135    let m = if mp < 10 { mp + 3 } else { mp - 9 };
136    let y = if m <= 2 { y + 1 } else { y };
137    format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}Z")
138}
139
140/// **Why a transfer was turned away**, in the three words an operator can act
141/// on differently.
142///
143/// Deliberately three and not the gate's six. `gunnar_ssh::Refusal` has six
144/// variants because the admission gate has six ceilings, and which of them was
145/// hit is an operator's question about the BOX. A tenant's question is a
146/// different one — *is the server busy, am I over my plan, or am I not allowed*
147/// — and those are three answers. The six-way detail stays where it is, on the
148/// gate's own `AdmissionStats` and in the `admission-*` reason on the log line.
149#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
150pub enum Refusal {
151    /// **The box was full.** Every `gunnar_ssh::Refusal` variant: the gate is at
152    /// capacity, the principal's share is spent, a lane is full, the anonymous
153    /// ceiling is reached, the keyed reserve is held, the memory budget is
154    /// spent. Nothing about the tenant is wrong; the server said "not now".
155    Admission,
156    /// **The plan said no before a byte.** `PlanGate::pack_quota` refusing a
157    /// namespace already at its `pack_bytes` cap, and the LFS allowance
158    /// refusing an upload past `lfs_bytes`. This is the one an invoice can fix.
159    Cap,
160    /// **The push gate said no.** A protected ref, a required review, a
161    /// signature that did not verify — `gunnar_policy`'s verdict. Nothing an
162    /// invoice can fix and nothing the operator should page about.
163    Policy,
164}
165
166impl Refusal {
167    /// `admission` | `cap` | `policy` — the wire spelling, one writer. The same
168    /// idiom as `Scope::name` and `Growth::name` on monetize's side: a word,
169    /// never an enum's ordinal, because an ordinal is an encoding of the
170    /// meaning rather than the meaning.
171    pub fn name(self) -> &'static str {
172        match self {
173            Refusal::Admission => "admission",
174            Refusal::Cap => "cap",
175            Refusal::Policy => "policy",
176        }
177    }
178
179    /// Every reason, in the order a breakdown is rendered in.
180    pub const ALL: [Refusal; 3] = [Refusal::Admission, Refusal::Cap, Refusal::Policy];
181}
182
183/// **One namespace's flow for one UTC day.**
184///
185/// Every field is a count of something that HAPPENED. There is no field here
186/// whose absence could be mistaken for a reading that was never taken: a day
187/// with no row is a day with no traffic, and a row of zeroes is the same thing
188/// said twice. That is the opposite of the stock meters, where a missing
189/// reading and a zero mean different things — and the difference is that a
190/// transfer either occurred or did not.
191#[derive(Clone, Default, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
192#[serde(deny_unknown_fields)]
193pub struct FlowDay {
194    /// Bytes the server ACCEPTED off the wire: receive-pack, and the client
195    /// half of every other conversation. Measured by the transport's own
196    /// `Metered` reader, which is the one place a byte arriving is counted.
197    #[serde(default)]
198    pub bytes_in: u64,
199    /// Bytes the server SERVED: upload-pack, the advertisement, and the archive
200    /// stream. Measured by the transport's own `Metered` writer.
201    #[serde(default)]
202    pub bytes_out: u64,
203    /// receive-pack sessions that finished without an error.
204    #[serde(default)]
205    pub pushes: u64,
206    /// upload-pack sessions, clones included. The bare advertisement
207    /// (`GET /info/refs`, `git ls-remote`) is NOT one: it reads the ref list and
208    /// asks for no objects, and counting it would make a monitoring probe look
209    /// like a customer.
210    #[serde(default)]
211    pub fetches: u64,
212    /// LFS object bytes received. A subset of nothing: LFS rides its own
213    /// conversation, so these bytes are also in [`FlowDay::bytes_in`] as wire
214    /// bytes, and this is the applied figure the session itself counted.
215    #[serde(default)]
216    pub lfs_bytes_in: u64,
217    /// LFS object bytes sent. See [`FlowDay::lfs_bytes_in`].
218    #[serde(default)]
219    pub lfs_bytes_out: u64,
220    /// [`Refusal::Admission`] — the box was full.
221    #[serde(default)]
222    pub refused_admission: u64,
223    /// [`Refusal::Cap`] — the plan refused.
224    #[serde(default)]
225    pub refused_cap: u64,
226    /// [`Refusal::Policy`] — the push gate refused.
227    #[serde(default)]
228    pub refused_policy: u64,
229    /// **A principal that authenticated and then held no grant here.** See the
230    /// module doc for the half that is deliberately absent: a key that resolves
231    /// to nobody names no namespace and is not a tenant's number.
232    #[serde(default)]
233    pub auth_failures: u64,
234    /// The last transfer that put bytes IN, in Unix milliseconds; `0` for a day
235    /// with no push. Kept per day rather than only in the latest row so a
236    /// series answers *when in that week did they stop* and not only *have they
237    /// stopped*.
238    #[serde(default)]
239    pub last_push_unix_ms: i64,
240    /// The last transfer that took bytes OUT. `0` for a day with no fetch.
241    #[serde(default)]
242    pub last_fetch_unix_ms: i64,
243    /// **Which principals were seen**, by the fingerprint the auth table knows
244    /// them as. Exact, bounded by [`PRINCIPAL_CEILING`]; see there for why a
245    /// set and not a sketch.
246    #[serde(default)]
247    pub principals: BTreeSet<String>,
248    /// **The principal set stopped growing at [`PRINCIPAL_CEILING`]**, so
249    /// `principals.len()` is a FLOOR and not a total. It has never been true on
250    /// any real namespace; it exists so that if it ever is, the number says so
251    /// instead of quietly flattening.
252    #[serde(default)]
253    pub principals_capped: bool,
254}
255
256impl FlowDay {
257    /// Every refusal, whatever the reason.
258    pub fn refused(&self) -> u64 {
259        self.refused_admission
260            .saturating_add(self.refused_cap)
261            .saturating_add(self.refused_policy)
262    }
263
264    /// The count for one reason.
265    pub fn refused_for(&self, reason: Refusal) -> u64 {
266        match reason {
267            Refusal::Admission => self.refused_admission,
268            Refusal::Cap => self.refused_cap,
269            Refusal::Policy => self.refused_policy,
270        }
271    }
272
273    /// **Distinct principals seen that day** — a floor when
274    /// [`FlowDay::principals_capped`].
275    pub fn distinct_principals(&self) -> u64 {
276        self.principals.len() as u64
277    }
278
279    /// **Nothing happened**: no bytes, no sessions, no refusals, nobody seen. A
280    /// row like this is never written — a day with no traffic has no row — and
281    /// this is what the flush asks before writing one.
282    pub fn is_empty(&self) -> bool {
283        *self == FlowDay::default()
284    }
285
286    /// The last time anything moved in either direction, or `0`.
287    pub fn last_seen_unix_ms(&self) -> i64 {
288        self.last_push_unix_ms.max(self.last_fetch_unix_ms)
289    }
290
291    /// **Add `other` into this day.** Used to fold a series into a window
292    /// total, never by the flush — see the module doc for why the flush copies
293    /// rather than adds.
294    pub fn add(&mut self, other: &FlowDay) {
295        self.bytes_in = self.bytes_in.saturating_add(other.bytes_in);
296        self.bytes_out = self.bytes_out.saturating_add(other.bytes_out);
297        self.pushes = self.pushes.saturating_add(other.pushes);
298        self.fetches = self.fetches.saturating_add(other.fetches);
299        self.lfs_bytes_in = self.lfs_bytes_in.saturating_add(other.lfs_bytes_in);
300        self.lfs_bytes_out = self.lfs_bytes_out.saturating_add(other.lfs_bytes_out);
301        self.refused_admission = self
302            .refused_admission
303            .saturating_add(other.refused_admission);
304        self.refused_cap = self.refused_cap.saturating_add(other.refused_cap);
305        self.refused_policy = self.refused_policy.saturating_add(other.refused_policy);
306        self.auth_failures = self.auth_failures.saturating_add(other.auth_failures);
307        self.last_push_unix_ms = self.last_push_unix_ms.max(other.last_push_unix_ms);
308        self.last_fetch_unix_ms = self.last_fetch_unix_ms.max(other.last_fetch_unix_ms);
309        for who in &other.principals {
310            if self.principals.len() >= PRINCIPAL_CEILING {
311                self.principals_capped = true;
312                break;
313            }
314            self.principals.insert(who.clone());
315        }
316        self.principals_capped |= other.principals_capped;
317    }
318}
319
320/// One namespace's kept days, newest last. Absent days are days with no
321/// traffic.
322#[derive(Clone, Default, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct NamespaceFlow {
325    /// Keyed by the day number [`day_of`] returns.
326    #[serde(default)]
327    pub days: BTreeMap<i64, FlowDay>,
328}
329
330/// The document as it is stored.
331#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
332#[serde(deny_unknown_fields)]
333struct FlowDoc {
334    /// Keyed by namespace name. One entry per namespace, holding at most
335    /// [`RETENTION_DAYS`] rows — a few tens of kilobytes at the top end, which
336    /// is one blob and one read rather than a scan over a day-keyed table.
337    #[serde(default)]
338    flows: ShardedMap<NamespaceFlow>,
339}
340
341impl_doc!(FlowDoc { flows });
342
343/// **The kept half**: read and write a namespace's days. Cheap to clone; every
344/// clone reads and writes the same document.
345#[derive(Clone, Debug)]
346pub struct FlowStore {
347    doc: DocRef<FlowDoc>,
348}
349
350impl FlowStore {
351    /// Open the flow document held in `store` (the server's control store). A
352    /// store that has never held one reads as empty, which is the truth for
353    /// every server that predates this file: no flow was counted, so no flow is
354    /// reported — never a zero that would read as *nothing happened*.
355    pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
356        Ok(FlowStore {
357            doc: DocRef::new(store, FLOW_REF)?,
358        })
359    }
360
361    /// The name the document is stored under.
362    pub fn ref_name(&self) -> &str {
363        self.doc.ref_name()
364    }
365
366    /// One namespace's row for one day, or `None` when that day has no traffic.
367    pub fn day(&self, namespace: &str, day: i64) -> Result<Option<FlowDay>> {
368        Ok(self
369            .doc
370            .load()?
371            .flows
372            .get(namespace)?
373            .and_then(|flow| flow.days.get(&day).cloned()))
374    }
375
376    /// **The series for one namespace, oldest first**, over the inclusive day
377    /// range. Days with no traffic are absent rather than zero-filled: the
378    /// caller knows the range it asked for, and a row that was never written is
379    /// not a row of zeroes that was.
380    pub fn series(
381        &self,
382        namespace: &str,
383        from_day: i64,
384        to_day: i64,
385    ) -> Result<Vec<(i64, FlowDay)>> {
386        let Some(flow) = self.doc.load()?.flows.get(namespace)? else {
387            return Ok(Vec::new());
388        };
389        Ok(flow
390            .days
391            .range(from_day..=to_day)
392            .map(|(day, row)| (*day, row.clone()))
393            .collect())
394    }
395
396    /// Every namespace that has ever had a row. Whole-map, O(entries) — the
397    /// fleet view and the flush, never a transfer.
398    pub fn namespaces(&self) -> Result<Vec<String>> {
399        let mut names: Vec<String> = self.doc.load()?.flows.keys()?;
400        names.sort();
401        Ok(names)
402    }
403
404    /// **Write these rows, replacing whatever each day held, and drop anything
405    /// older than [`RETENTION_DAYS`] before `today`.**
406    ///
407    /// Replace and not add, because the live slot holds the whole of the day it
408    /// is writing: see the module doc. An empty row REMOVES the day rather than
409    /// storing zeroes — a namespace that had a row and then had a day of
410    /// silence must not grow a row saying so.
411    ///
412    /// One document update for the whole batch, so a flush touching forty
413    /// namespaces is one compare-and-swap and not forty.
414    pub fn put(&self, rows: &[(String, i64, FlowDay)], today: i64) -> Result<()> {
415        if rows.is_empty() {
416            return Ok(());
417        }
418        let oldest = today.saturating_sub(RETENTION_DAYS);
419        let rows = rows.to_vec();
420        self.doc.update(|doc| {
421            for (namespace, day, row) in &rows {
422                let mut flow = doc.flows.get(namespace)?.unwrap_or_default();
423                if row.is_empty() {
424                    flow.days.remove(day);
425                } else {
426                    flow.days.insert(*day, row.clone());
427                }
428                flow.days.retain(|day, _| *day > oldest);
429                if flow.days.is_empty() {
430                    doc.flows.remove(namespace)?;
431                } else {
432                    doc.flows.insert(namespace.clone(), flow)?;
433                }
434            }
435            Ok(())
436        })
437    }
438
439    /// Forget one namespace's whole series, reporting whether there was one.
440    /// The flow half of deleting a namespace — a tenant that is gone must not
441    /// leave a record of who pushed to it.
442    pub fn forget(&self, namespace: &str) -> Result<bool> {
443        let key = namespace.to_owned();
444        self.doc.update(|doc| Ok(doc.flows.remove(&key)?.is_some()))
445    }
446}
447
448/// **One namespace's flow at a glance**: today, and when it was last seen.
449///
450/// The shape `Usage.Read` carries. `last_*` deliberately look back over the
451/// whole kept window rather than over `today` — see [`FlowLedger::glance`].
452#[derive(Clone, Default, PartialEq, Eq, Debug)]
453pub struct Glance {
454    /// Today's counts. All zero for a day with no traffic, which for a FLOW
455    /// meter is a fact and not a gap: a transfer either occurred or it did not.
456    pub today: FlowDay,
457    /// The last push anywhere in the kept window, or `0` for a namespace that
458    /// has never been pushed to inside it.
459    pub last_push_unix_ms: i64,
460    /// The last fetch anywhere in the kept window, or `0`.
461    pub last_fetch_unix_ms: i64,
462}
463
464impl Glance {
465    /// The last time anything moved in either direction, or `0`.
466    pub fn last_seen_unix_ms(&self) -> i64 {
467        self.last_push_unix_ms.max(self.last_fetch_unix_ms)
468    }
469}
470
471/// **The whole box's flow at a glance**, folded over every namespace. See
472/// [`FlowLedger::fleet`].
473#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
474pub struct Fleet {
475    /// Wire bytes accepted today, across every namespace.
476    pub bytes_in_today: u64,
477    /// Wire bytes served today, across every namespace.
478    pub bytes_out_today: u64,
479    /// Transfers turned away today, across every namespace.
480    pub refused_today: u64,
481    /// How many namespaces this fold looked at.
482    pub namespaces: u64,
483    /// **How many have neither pushed nor fetched inside the caller's window.**
484    /// A tenant that has never been seen at all counts here: it is the quietest
485    /// one there is, and rounding it to "not silent" would hide exactly the
486    /// account an operator is looking for.
487    pub silent: u64,
488}
489
490/// The live counters for one namespace's current day. Every field is an atomic
491/// so the hot path takes no lock; `roll` is taken only when the day turns and
492/// when the flush snapshots.
493#[derive(Debug)]
494struct Slot {
495    day: AtomicI64,
496    bytes_in: AtomicU64,
497    bytes_out: AtomicU64,
498    pushes: AtomicU64,
499    fetches: AtomicU64,
500    lfs_bytes_in: AtomicU64,
501    lfs_bytes_out: AtomicU64,
502    refused_admission: AtomicU64,
503    refused_cap: AtomicU64,
504    refused_policy: AtomicU64,
505    auth_failures: AtomicU64,
506    last_push_unix_ms: AtomicI64,
507    last_fetch_unix_ms: AtomicI64,
508    /// The one thing that is not an atomic. A `BTreeSet` insert per transfer —
509    /// once per SESSION, never per byte — is not on the byte path.
510    principals: Mutex<(BTreeSet<String>, bool)>,
511    /// Held while the day turns and while the flush reads, so the two cannot
512    /// interleave and a completed day cannot be written twice.
513    roll: Mutex<()>,
514}
515
516impl Slot {
517    fn seeded(day: i64, from: FlowDay) -> Slot {
518        Slot {
519            day: AtomicI64::new(day),
520            bytes_in: AtomicU64::new(from.bytes_in),
521            bytes_out: AtomicU64::new(from.bytes_out),
522            pushes: AtomicU64::new(from.pushes),
523            fetches: AtomicU64::new(from.fetches),
524            lfs_bytes_in: AtomicU64::new(from.lfs_bytes_in),
525            lfs_bytes_out: AtomicU64::new(from.lfs_bytes_out),
526            refused_admission: AtomicU64::new(from.refused_admission),
527            refused_cap: AtomicU64::new(from.refused_cap),
528            refused_policy: AtomicU64::new(from.refused_policy),
529            auth_failures: AtomicU64::new(from.auth_failures),
530            last_push_unix_ms: AtomicI64::new(from.last_push_unix_ms),
531            last_fetch_unix_ms: AtomicI64::new(from.last_fetch_unix_ms),
532            principals: Mutex::new((from.principals, from.principals_capped)),
533            roll: Mutex::new(()),
534        }
535    }
536
537    /// A snapshot, without disturbing the counters.
538    fn read(&self) -> FlowDay {
539        let (principals, principals_capped) = {
540            let held = self.principals.lock().expect("flow principals");
541            (held.0.clone(), held.1)
542        };
543        FlowDay {
544            bytes_in: self.bytes_in.load(Ordering::Relaxed),
545            bytes_out: self.bytes_out.load(Ordering::Relaxed),
546            pushes: self.pushes.load(Ordering::Relaxed),
547            fetches: self.fetches.load(Ordering::Relaxed),
548            lfs_bytes_in: self.lfs_bytes_in.load(Ordering::Relaxed),
549            lfs_bytes_out: self.lfs_bytes_out.load(Ordering::Relaxed),
550            refused_admission: self.refused_admission.load(Ordering::Relaxed),
551            refused_cap: self.refused_cap.load(Ordering::Relaxed),
552            refused_policy: self.refused_policy.load(Ordering::Relaxed),
553            auth_failures: self.auth_failures.load(Ordering::Relaxed),
554            last_push_unix_ms: self.last_push_unix_ms.load(Ordering::Relaxed),
555            last_fetch_unix_ms: self.last_fetch_unix_ms.load(Ordering::Relaxed),
556            principals,
557            principals_capped,
558        }
559    }
560
561    /// Zero everything and open a new day.
562    fn reset_to(&self, day: i64) {
563        for counter in [
564            &self.bytes_in,
565            &self.bytes_out,
566            &self.pushes,
567            &self.fetches,
568            &self.lfs_bytes_in,
569            &self.lfs_bytes_out,
570            &self.refused_admission,
571            &self.refused_cap,
572            &self.refused_policy,
573            &self.auth_failures,
574        ] {
575            counter.store(0, Ordering::Relaxed);
576        }
577        self.last_push_unix_ms.store(0, Ordering::Relaxed);
578        self.last_fetch_unix_ms.store(0, Ordering::Relaxed);
579        {
580            let mut held = self.principals.lock().expect("flow principals");
581            held.0.clear();
582            held.1 = false;
583        }
584        self.day.store(day, Ordering::Release);
585    }
586
587    fn saw(&self, principal: &str) {
588        if principal.is_empty() {
589            return;
590        }
591        let mut held = self.principals.lock().expect("flow principals");
592        if held.0.contains(principal) {
593            return;
594        }
595        if held.0.len() >= PRINCIPAL_CEILING {
596            held.1 = true;
597            return;
598        }
599        held.0.insert(principal.to_owned());
600    }
601}
602
603/// **What one finished transfer moved.** Built by the counting point, applied
604/// once.
605///
606/// One struct rather than eight `record_*` methods, because a transfer is ONE
607/// event and its numbers must land on one day together: eight calls could
608/// straddle midnight and split a single push across two rows.
609#[derive(Clone, Default, PartialEq, Eq, Debug)]
610pub struct Transfer {
611    /// Wire bytes accepted.
612    pub bytes_in: u64,
613    /// Wire bytes served.
614    pub bytes_out: u64,
615    /// A receive-pack session that finished.
616    pub pushed: bool,
617    /// An upload-pack session that was asked for objects — never the bare
618    /// advertisement.
619    pub fetched: bool,
620    /// LFS object bytes received, as the LFS session counted them.
621    pub lfs_bytes_in: u64,
622    /// LFS object bytes sent.
623    pub lfs_bytes_out: u64,
624    /// The principal, by the fingerprint the auth table knows. Empty for a
625    /// transfer with no identity, which is counted in the bytes and in nobody's
626    /// name.
627    pub principal: String,
628}
629
630impl Transfer {
631    /// Did anything move at all? A transfer that moved no byte in either
632    /// direction and neither pushed nor fetched is not written.
633    pub fn is_empty(&self) -> bool {
634        self.bytes_in == 0
635            && self.bytes_out == 0
636            && !self.pushed
637            && !self.fetched
638            && self.lfs_bytes_in == 0
639            && self.lfs_bytes_out == 0
640    }
641}
642
643/// **The live half: the counters every transfer path bumps.**
644///
645/// Cheap to clone through an `Arc`. A record is: one read lock, one hash
646/// lookup, a handful of relaxed `fetch_add`s, and one `BTreeSet` insert per
647/// session. Nothing on that path allocates once the namespace has a slot, and a
648/// slot is made once per namespace per process.
649#[derive(Debug)]
650pub struct FlowLedger {
651    store: FlowStore,
652    /// One slot per namespace touched since boot. The write lock is taken only
653    /// to make a slot.
654    live: RwLock<HashMap<String, Arc<Slot>>>,
655    /// Days that turned over and have not been written yet. The flush drains
656    /// this before it snapshots today.
657    completed: Mutex<Vec<(String, i64, FlowDay)>>,
658    /// **Who is told a namespace's flow moved** (2026-10-06): the server's
659    /// usage ledger, so a batched usage read (`UsageBatch.ReadMany` with a
660    /// cursor) answers the namespaces whose `*_today` meters changed — and
661    /// re-measures their bytes, since a transfer is also what grows a
662    /// repository, its LFS store and its exploded read cache. One call per
663    /// recorded transfer, refusal or auth failure; never per object.
664    observer: RwLock<Option<FlowObserver>>,
665}
666
667/// The callback [`FlowLedger::observe`] installs, with a `Debug` that says
668/// only that it is there.
669#[derive(Clone)]
670pub struct FlowObserver(pub Arc<dyn Fn(&str) + Send + Sync>);
671
672impl std::fmt::Debug for FlowObserver {
673    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
674        f.write_str("FlowObserver(..)")
675    }
676}
677
678impl FlowLedger {
679    /// Open the ledger over a control store.
680    pub fn new(store: FlowStore) -> FlowLedger {
681        FlowLedger {
682            store,
683            live: RwLock::new(HashMap::new()),
684            completed: Mutex::new(Vec::new()),
685            observer: RwLock::new(None),
686        }
687    }
688
689    /// **Tell `observer` the name of every namespace whose flow moves** — a
690    /// recorded transfer, a refusal, an auth failure. Replaces any earlier
691    /// observer. Called on the transfer path, so it must be cheap: the
692    /// server's usage ledger inserts the name into a set and returns.
693    pub fn observe(&self, observer: FlowObserver) {
694        *self.observer.write().expect("flow observer") = Some(observer);
695    }
696
697    fn tell_observer(&self, namespace: &str) {
698        if let Some(observer) = self.observer.read().expect("flow observer").as_ref() {
699            (observer.0)(namespace);
700        }
701    }
702
703    /// The kept half, for the readers that want a series rather than today.
704    pub fn store(&self) -> &FlowStore {
705        &self.store
706    }
707
708    /// The slot for a namespace, made and seeded from the store on first touch.
709    ///
710    /// **Seeded, not zeroed.** A server that restarts at noon must go on
711    /// counting today from where the last flush left it; starting from zero
712    /// would make every restart look like a quiet morning.
713    fn slot(&self, namespace: &str, day: i64) -> Arc<Slot> {
714        if let Some(slot) = self
715            .live
716            .read()
717            .expect("flow slots")
718            .get(namespace)
719            .map(Arc::clone)
720        {
721            self.roll_to(namespace, &slot, day);
722            return slot;
723        }
724        // The store read happens outside the write lock: it is I/O, and a
725        // second namespace's first transfer must not wait behind it.
726        let seed = self
727            .store
728            .day(namespace, day)
729            .unwrap_or_default()
730            .unwrap_or_default();
731        let mut live = self.live.write().expect("flow slots");
732        let slot = Arc::clone(
733            live.entry(namespace.to_owned())
734                .or_insert_with(|| Arc::new(Slot::seeded(day, seed))),
735        );
736        drop(live);
737        self.roll_to(namespace, &slot, day);
738        slot
739    }
740
741    /// Turn the day if it has turned, parking the completed row for the flush.
742    fn roll_to(&self, namespace: &str, slot: &Slot, day: i64) {
743        if slot.day.load(Ordering::Acquire) == day {
744            return;
745        }
746        let _held = slot.roll.lock().expect("flow roll");
747        let was = slot.day.load(Ordering::Acquire);
748        if was == day {
749            return;
750        }
751        let finished = slot.read();
752        if !finished.is_empty() {
753            self.completed.lock().expect("flow completed").push((
754                namespace.to_owned(),
755                was,
756                finished,
757            ));
758        }
759        slot.reset_to(day);
760    }
761
762    /// **The counting point for a finished transfer.** One call, one day.
763    pub fn record(&self, namespace: &str, at_unix_ms: i64, transfer: &Transfer) {
764        if namespace.is_empty() || transfer.is_empty() {
765            return;
766        }
767        let slot = self.slot(namespace, day_of(at_unix_ms));
768        add(&slot.bytes_in, transfer.bytes_in);
769        add(&slot.bytes_out, transfer.bytes_out);
770        add(&slot.lfs_bytes_in, transfer.lfs_bytes_in);
771        add(&slot.lfs_bytes_out, transfer.lfs_bytes_out);
772        if transfer.pushed {
773            slot.pushes.fetch_add(1, Ordering::Relaxed);
774            slot.last_push_unix_ms.store(at_unix_ms, Ordering::Relaxed);
775        }
776        if transfer.fetched {
777            slot.fetches.fetch_add(1, Ordering::Relaxed);
778            slot.last_fetch_unix_ms.store(at_unix_ms, Ordering::Relaxed);
779        }
780        slot.saw(&transfer.principal);
781        self.tell_observer(namespace);
782    }
783
784    /// **The counting point for a refusal.** No bytes moved; the tenant met a
785    /// door.
786    pub fn refuse(&self, namespace: &str, at_unix_ms: i64, reason: Refusal) {
787        if namespace.is_empty() {
788            return;
789        }
790        let slot = self.slot(namespace, day_of(at_unix_ms));
791        let counter = match reason {
792            Refusal::Admission => &slot.refused_admission,
793            Refusal::Cap => &slot.refused_cap,
794            Refusal::Policy => &slot.refused_policy,
795        };
796        counter.fetch_add(1, Ordering::Relaxed);
797        self.tell_observer(namespace);
798    }
799
800    /// **The counting point for a principal with no grant here.** See the
801    /// module doc for the half that is not counted.
802    pub fn auth_failure(&self, namespace: &str, at_unix_ms: i64) {
803        if namespace.is_empty() {
804            return;
805        }
806        let slot = self.slot(namespace, day_of(at_unix_ms));
807        slot.auth_failures.fetch_add(1, Ordering::Relaxed);
808        self.tell_observer(namespace);
809    }
810
811    /// **Today's live figure for one namespace**, whether or not it has been
812    /// flushed. Read straight off the atomics — the store is not consulted,
813    /// because the slot holds the whole day and the stored row is a copy of it.
814    ///
815    /// `None` when this process has not seen the namespace today AND the store
816    /// holds no row for it: nothing happened, which is a fact and not a gap.
817    pub fn today(&self, namespace: &str, now_unix_ms: i64) -> Option<FlowDay> {
818        let day = day_of(now_unix_ms);
819        if let Some(slot) = self
820            .live
821            .read()
822            .expect("flow slots")
823            .get(namespace)
824            .map(Arc::clone)
825        {
826            if slot.day.load(Ordering::Acquire) == day {
827                let row = slot.read();
828                return (!row.is_empty()).then_some(row);
829            }
830        }
831        self.store.day(namespace, day).ok().flatten()
832    }
833
834    /// **What `Usage.Read` puts beside the stock meters**: today's flow, and
835    /// when the namespace was last seen over the WHOLE kept window.
836    ///
837    /// The two halves answer different questions and must not be folded into
838    /// one. *How much moved today* is a rate; *when did anything last move* is
839    /// what an operator reads to decide a tenant has gone quiet, and a tenant
840    /// who has done nothing for six weeks has a today of zeroes and a last-seen
841    /// six weeks ago. Reading the second out of the first is exactly the
842    /// mistake this pair exists to prevent.
843    pub fn glance(&self, namespace: &str, now_unix_ms: i64) -> Glance {
844        let today_number = day_of(now_unix_ms);
845        let today = self.today(namespace, now_unix_ms).unwrap_or_default();
846        let mut glance = Glance {
847            last_push_unix_ms: today.last_push_unix_ms,
848            last_fetch_unix_ms: today.last_fetch_unix_ms,
849            today,
850        };
851        // Backwards from yesterday, and stop as soon as both are known: a
852        // namespace that pushed yesterday costs one row, not four hundred.
853        let kept = self
854            .store
855            .series(namespace, today_number - RETENTION_DAYS, today_number - 1)
856            .unwrap_or_default();
857        for (_, row) in kept.iter().rev() {
858            if glance.last_push_unix_ms == 0 {
859                glance.last_push_unix_ms = row.last_push_unix_ms;
860            }
861            if glance.last_fetch_unix_ms == 0 {
862                glance.last_fetch_unix_ms = row.last_fetch_unix_ms;
863            }
864            if glance.last_push_unix_ms != 0 && glance.last_fetch_unix_ms != 0 {
865                break;
866            }
867        }
868        glance
869    }
870
871    /// **The whole box's flow, folded** — what moved today across every
872    /// namespace, and how many tenants have gone quiet.
873    ///
874    /// O(namespaces), and it reads the control document. It is for the
875    /// dashboard band and for an operator's fleet view, NEVER for a transfer:
876    /// the flush tick computes it once a minute and the screen reads the
877    /// result, because the dashboard's own two-second tick is documented as
878    /// reading a few atomics and nothing else.
879    ///
880    /// `silent_after_days` is the caller's policy, not this module's. Fourteen
881    /// days is what the console uses; a different deployment may reasonably
882    /// call a tenant quiet sooner, and a constant here would make that a fork.
883    pub fn fleet(&self, now_unix_ms: i64, silent_after_days: i64) -> Fleet {
884        let mut names: BTreeSet<String> = self
885            .store
886            .namespaces()
887            .unwrap_or_default()
888            .into_iter()
889            .collect();
890        names.extend(self.live.read().expect("flow slots").keys().cloned());
891        let quiet_before = now_unix_ms.saturating_sub(silent_after_days.saturating_mul(DAY_MS));
892        let mut fleet = Fleet {
893            namespaces: names.len() as u64,
894            ..Fleet::default()
895        };
896        for name in &names {
897            let glance = self.glance(name, now_unix_ms);
898            fleet.bytes_in_today = fleet.bytes_in_today.saturating_add(glance.today.bytes_in);
899            fleet.bytes_out_today = fleet.bytes_out_today.saturating_add(glance.today.bytes_out);
900            fleet.refused_today = fleet.refused_today.saturating_add(glance.today.refused());
901            // A namespace that has NEVER been seen inside the kept window is
902            // silent too — `last_seen_unix_ms()` is 0 there, which is before
903            // any cutoff. That is the right answer: a tenant nobody has ever
904            // pushed to is the quietest one there is.
905            if glance.last_seen_unix_ms() < quiet_before {
906                fleet.silent = fleet.silent.saturating_add(1);
907            }
908        }
909        fleet
910    }
911
912    /// **Copy every live slot into the store, and drop what has aged out.**
913    ///
914    /// The flush. Runs on a tick; touches redb once for the whole batch. It
915    /// COPIES rather than adds, so running it twice in a row writes the same
916    /// rows twice rather than doubling them — which is what makes it safe to
917    /// run it again on shutdown, and safe for a restart to seed from it.
918    ///
919    /// Returns how many rows it wrote, for the caller's log line.
920    pub fn flush(&self, now_unix_ms: i64) -> Result<usize> {
921        let today = day_of(now_unix_ms);
922        let mut rows: Vec<(String, i64, FlowDay)> =
923            std::mem::take(&mut *self.completed.lock().expect("flow completed"));
924        let slots: Vec<(String, Arc<Slot>)> = self
925            .live
926            .read()
927            .expect("flow slots")
928            .iter()
929            .map(|(name, slot)| (name.clone(), Arc::clone(slot)))
930            .collect();
931        for (namespace, slot) in slots {
932            // Roll first: a slot nobody has touched since yesterday still holds
933            // yesterday, and the tick is what notices on an idle server.
934            self.roll_to(&namespace, &slot, today);
935            let _held = slot.roll.lock().expect("flow roll");
936            let row = slot.read();
937            if !row.is_empty() {
938                rows.push((namespace, slot.day.load(Ordering::Acquire), row));
939            }
940        }
941        // A roll during the loop above parks rows in `completed`; take them too
942        // rather than leaving them for the next tick.
943        rows.extend(std::mem::take(
944            &mut *self.completed.lock().expect("flow completed"),
945        ));
946        let written = rows.len();
947        if written > 0 {
948            if let Err(err) = self.store.put(&rows, today) {
949                // A refused write (the transaction log released while the data set is
950                // grown, a full disk) must not LOSE the completed days taken out of
951                // `completed` above: a live slot is still in memory, a rolled day is
952                // only here. Parked again, the next flush writes them. A live-slot row
953                // parked beside them is harmless: `put` copies, it does not add.
954                self.completed.lock().expect("flow completed").extend(rows);
955                return Err(err);
956            }
957        }
958        Ok(written)
959    }
960
961    /// **Bytes moved — in plus out — per UTC day, over the last `days` days,
962    /// today included**: `(day, bytes)` for each day that moved anything,
963    /// oldest first. The durable half of admission's usage: `edda_admit`'s
964    /// table SEEDS a namespace from this once, on first sight, and is the
965    /// cache from then on — so this is read once per namespace per process,
966    /// never per transfer (it takes the ledger's locks and reads the store).
967    ///
968    /// Wire bytes only. LFS bytes are already inside them (see
969    /// [`FlowDay::lfs_bytes_in`]), so adding the LFS pair would count one
970    /// transfer twice.
971    ///
972    /// * **today** is the live slot, read off its atomics;
973    /// * **the closed days** are the store's rows, plus any closed day still
974    ///   parked for the flush — a parked row is the FINAL figure for its day
975    ///   and replaces the store's last-tick copy of it, never adds to it.
976    ///
977    /// A slot still holding yesterday is rolled first, so a read just after
978    /// midnight does not take yesterday's live bytes for today's.
979    pub fn moved(&self, namespace: &str, now_unix_ms: i64, days: u32) -> Vec<(i64, u64)> {
980        let today = day_of(now_unix_ms);
981        let live_slot = self
982            .live
983            .read()
984            .expect("flow slots")
985            .get(namespace)
986            .map(Arc::clone);
987        if let Some(slot) = live_slot {
988            self.roll_to(namespace, &slot, today);
989        }
990        let from = today - i64::from(days.max(1)) + 1;
991        let mut rows: BTreeMap<i64, u64> = if from < today {
992            self.store
993                .series(namespace, from, today - 1)
994                .unwrap_or_default()
995                .into_iter()
996                .map(|(day, row)| (day, row.bytes_in.saturating_add(row.bytes_out)))
997                .collect()
998        } else {
999            BTreeMap::new()
1000        };
1001        rows.extend(
1002            self.completed
1003                .lock()
1004                .expect("flow completed")
1005                .iter()
1006                .filter(|(name, day, _)| name == namespace && *day >= from && *day < today)
1007                .map(|(_, day, row)| (*day, row.bytes_in.saturating_add(row.bytes_out))),
1008        );
1009        if let Some(t) = self.today(namespace, now_unix_ms) {
1010            rows.insert(today, t.bytes_in.saturating_add(t.bytes_out));
1011        }
1012        rows.into_iter().filter(|(_, b)| *b > 0).collect()
1013    }
1014
1015    /// [`FlowLedger::moved`], summed: bytes in + out over the window.
1016    pub fn window_bytes(&self, namespace: &str, now_unix_ms: i64, days: u32) -> u64 {
1017        self.moved(namespace, now_unix_ms, days)
1018            .iter()
1019            .fold(0u64, |acc, (_, b)| acc.saturating_add(*b))
1020    }
1021
1022    /// Drop a namespace's live slot and its whole kept series. The flow half of
1023    /// deleting a namespace.
1024    pub fn forget(&self, namespace: &str) -> Result<bool> {
1025        self.live.write().expect("flow slots").remove(namespace);
1026        self.completed
1027            .lock()
1028            .expect("flow completed")
1029            .retain(|(name, _, _)| name != namespace);
1030        self.store.forget(namespace)
1031    }
1032}
1033
1034/// `fetch_add` that skips the bus traffic for the common zero.
1035fn add(counter: &AtomicU64, by: u64) {
1036    if by > 0 {
1037        counter.fetch_add(by, Ordering::Relaxed);
1038    }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use super::*;
1044    use crate::control::RedbControlStore;
1045
1046    fn ledger() -> FlowLedger {
1047        let store = Arc::new(RedbControlStore::in_memory().unwrap()) as Arc<dyn ControlStore>;
1048        FlowLedger::new(FlowStore::new(store).unwrap())
1049    }
1050
1051    fn push(bytes: u64, who: &str) -> Transfer {
1052        Transfer {
1053            bytes_in: bytes,
1054            pushed: true,
1055            principal: who.into(),
1056            ..Transfer::default()
1057        }
1058    }
1059
1060    fn fetch(bytes: u64, who: &str) -> Transfer {
1061        Transfer {
1062            bytes_out: bytes,
1063            fetched: true,
1064            principal: who.into(),
1065            ..Transfer::default()
1066        }
1067    }
1068
1069    /// ★ **The admission window is today plus the closed days before it**,
1070    /// in + out, from the live slot, the parked rows and the store — each day
1071    /// counted once, whichever of those three it sits in.
1072    #[test]
1073    fn the_window_sums_thirty_days_counting_each_day_once() {
1074        let flow = ledger();
1075        let day0 = 20_000 * DAY_MS + 3_600_000;
1076        // 31 days ago: outside a 30-day window.
1077        flow.record("team", day0, &push(1_000_000, "a"));
1078        // 29 days ago: the oldest day inside it.
1079        flow.record("team", day0 + DAY_MS, &push(1000, "a"));
1080        flow.flush(day0 + DAY_MS).unwrap();
1081        // Yesterday: rolled but NOT flushed — parked, and the store holds nothing of it.
1082        flow.record("team", day0 + 29 * DAY_MS, &fetch(200, "a"));
1083        flow.record("team", day0 + 30 * DAY_MS, &push(30, "a"));
1084        flow.record("team", day0 + 30 * DAY_MS, &fetch(7, "b"));
1085        let now = day0 + 30 * DAY_MS;
1086        assert_eq!(flow.window_bytes("team", now, 30), 1000 + 200 + 30 + 7);
1087        let days = flow.moved("team", now, 30);
1088        let today = day_of(now);
1089        assert_eq!(
1090            days,
1091            vec![(today - 29, 1000), (today - 1, 200), (today, 37)],
1092            "oldest first, per day"
1093        );
1094        assert_eq!(
1095            flow.window_bytes("team", now, 1),
1096            37,
1097            "a one-day window is today"
1098        );
1099        assert_eq!(flow.window_bytes("other", now, 30), 0);
1100        // After the flush the parked row is in the store: the same total, not double.
1101        flow.flush(now).unwrap();
1102        assert_eq!(flow.window_bytes("team", now, 30), 1237);
1103        // Today is live: a new transfer today shows at once.
1104        flow.record("team", now + 1, &push(3, "a"));
1105        assert_eq!(flow.window_bytes("team", now + 1, 30), 1240);
1106        // The next day the oldest day falls out.
1107        assert_eq!(flow.window_bytes("team", now + DAY_MS, 30), 200 + 40);
1108    }
1109
1110    /// A slot still holding yesterday is rolled before it is read: the first
1111    /// read after midnight does not take yesterday's live bytes for today's,
1112    /// nor lose them.
1113    #[test]
1114    fn the_window_rolls_a_stale_slot_before_reading_it() {
1115        let flow = ledger();
1116        let day0 = 20_000 * DAY_MS;
1117        flow.flush(day0).unwrap();
1118        flow.record("team", day0 + 10, &push(500, "a"));
1119        flow.flush(day0 + 20).unwrap(); // the store holds a copy of day0 = 500
1120        flow.record("team", day0 + 30, &push(5, "a")); // live day0 = 505, store 500
1121        let next = day0 + DAY_MS + 1;
1122        assert_eq!(flow.window_bytes("team", next, 30), 505);
1123        assert_eq!(
1124            flow.window_bytes("team", next, 1),
1125            0,
1126            "nothing moved today yet"
1127        );
1128    }
1129
1130    /// ★ **A push charges the namespace it was pushed to, and nobody else.**
1131    ///
1132    /// The first thing that has to be true of a per-tenant meter, and the one
1133    /// that a global counter — which is all gunnar had — cannot be.
1134    #[test]
1135    fn a_push_lands_on_its_own_namespace_and_no_other() {
1136        let flow = ledger();
1137        let now = 1_757_000_000_000;
1138        flow.record("team", now, &push(4096, "SHA256:aaa"));
1139
1140        let mine = flow.today("team", now).expect("the push was counted");
1141        assert_eq!(mine.bytes_in, 4096);
1142        assert_eq!(mine.pushes, 1);
1143        assert_eq!(mine.bytes_out, 0, "a push serves no pack");
1144        assert_eq!(mine.last_push_unix_ms, now);
1145        assert_eq!(mine.distinct_principals(), 1);
1146
1147        assert_eq!(
1148            flow.today("other", now),
1149            None,
1150            "a namespace with no traffic has no row, not a row of zeroes"
1151        );
1152    }
1153
1154    /// A fetch is bytes OUT and a fetch count; a bare advertisement is neither.
1155    #[test]
1156    fn a_fetch_is_bytes_out_and_an_advertisement_is_not_a_fetch() {
1157        let flow = ledger();
1158        let now = 1_757_000_000_000;
1159        flow.record("team", now, &fetch(9000, "SHA256:bbb"));
1160        // The advertisement: bytes out, no session.
1161        flow.record(
1162            "team",
1163            now,
1164            &Transfer {
1165                bytes_out: 120,
1166                ..Transfer::default()
1167            },
1168        );
1169
1170        let row = flow.today("team", now).unwrap();
1171        assert_eq!(row.bytes_out, 9120, "both wrote bytes");
1172        assert_eq!(row.fetches, 1, "only one of them asked for objects");
1173        assert_eq!(row.last_fetch_unix_ms, now);
1174    }
1175
1176    /// ★ **A refusal is counted, by reason, and moves no bytes.**
1177    #[test]
1178    fn a_refusal_is_counted_by_reason_and_leaves_the_bytes_alone() {
1179        let flow = ledger();
1180        let now = 1_757_000_000_000;
1181        flow.record("team", now, &push(4096, "SHA256:aaa"));
1182        flow.refuse("team", now, Refusal::Cap);
1183        flow.refuse("team", now, Refusal::Cap);
1184        flow.refuse("team", now, Refusal::Admission);
1185
1186        let row = flow.today("team", now).unwrap();
1187        assert_eq!(row.bytes_in, 4096, "a refused push moved nothing");
1188        assert_eq!(row.pushes, 1);
1189        assert_eq!(row.refused(), 3);
1190        assert_eq!(row.refused_for(Refusal::Cap), 2);
1191        assert_eq!(row.refused_for(Refusal::Admission), 1);
1192        assert_eq!(row.refused_for(Refusal::Policy), 0);
1193    }
1194
1195    /// ★ **A day rollover splits the counts**, and yesterday keeps its own.
1196    #[test]
1197    fn the_day_boundary_splits_the_counts() {
1198        let flow = ledger();
1199        let day_one = day_of(1_757_000_000_000);
1200        let late = midnight_of(day_one) + DAY_MS - 1;
1201        let early = midnight_of(day_one + 1) + 1;
1202
1203        flow.record("team", late, &push(1000, "SHA256:aaa"));
1204        flow.record("team", early, &push(7, "SHA256:aaa"));
1205
1206        let today = flow.today("team", early).unwrap();
1207        assert_eq!(today.bytes_in, 7, "the new day starts from nothing");
1208        assert_eq!(today.pushes, 1);
1209
1210        flow.flush(early).unwrap();
1211        let yesterday = flow
1212            .store()
1213            .day("team", day_one)
1214            .unwrap()
1215            .expect("yesterday was written");
1216        assert_eq!(yesterday.bytes_in, 1000);
1217        assert_eq!(yesterday.pushes, 1);
1218    }
1219
1220    /// ★ **A restart keeps yesterday and drops nothing of today.**
1221    ///
1222    /// The whole reason the flush copies rather than adds, and the reason a new
1223    /// slot is seeded from the store: a second ledger over the same control
1224    /// store must go on counting today from where the first left it.
1225    #[test]
1226    fn a_restart_keeps_yesterday_and_resumes_today() {
1227        let store = Arc::new(RedbControlStore::in_memory().unwrap()) as Arc<dyn ControlStore>;
1228        let day_one = day_of(1_757_000_000_000);
1229        let yesterday_at = midnight_of(day_one) + 3_600_000;
1230        let today_at = midnight_of(day_one + 1) + 3_600_000;
1231        let later_today = today_at + 60_000;
1232
1233        {
1234            let flow = FlowLedger::new(FlowStore::new(Arc::clone(&store)).unwrap());
1235            flow.record("team", yesterday_at, &push(1000, "SHA256:aaa"));
1236            flow.flush(yesterday_at).unwrap();
1237            flow.record("team", today_at, &push(500, "SHA256:aaa"));
1238            flow.flush(today_at).unwrap();
1239        }
1240        // The server restarts.
1241        let flow = FlowLedger::new(FlowStore::new(Arc::clone(&store)).unwrap());
1242        assert_eq!(
1243            flow.today("team", today_at).unwrap().bytes_in,
1244            500,
1245            "today survived the restart"
1246        );
1247        flow.record("team", later_today, &push(1, "SHA256:aaa"));
1248        assert_eq!(
1249            flow.today("team", later_today).unwrap().bytes_in,
1250            501,
1251            "counting resumed from the stored figure, not from zero"
1252        );
1253        assert_eq!(flow.today("team", later_today).unwrap().pushes, 2);
1254        assert_eq!(
1255            flow.store().day("team", day_one).unwrap().unwrap().bytes_in,
1256            1000,
1257            "yesterday is untouched"
1258        );
1259    }
1260
1261    /// **Flushing twice writes the same numbers twice, not double them.** The
1262    /// property that makes the flush safe to run on a tick AND on shutdown.
1263    #[test]
1264    fn a_second_flush_is_not_a_second_count() {
1265        let flow = ledger();
1266        let now = 1_757_000_000_000;
1267        flow.record("team", now, &push(4096, "SHA256:aaa"));
1268        flow.flush(now).unwrap();
1269        flow.flush(now).unwrap();
1270        flow.flush(now).unwrap();
1271        assert_eq!(
1272            flow.store()
1273                .day("team", day_of(now))
1274                .unwrap()
1275                .unwrap()
1276                .bytes_in,
1277            4096
1278        );
1279    }
1280
1281    /// ★ **The series comes back oldest first**, and days with no traffic are
1282    /// absent rather than zero-filled.
1283    #[test]
1284    fn the_series_is_oldest_first_and_silent_days_are_absent() {
1285        let flow = ledger();
1286        let first = day_of(1_757_000_000_000);
1287        for (offset, bytes) in [(0i64, 100u64), (1, 200), (3, 400)] {
1288            let at = midnight_of(first + offset) + 1000;
1289            flow.record("team", at, &push(bytes, "SHA256:aaa"));
1290            flow.flush(at).unwrap();
1291        }
1292        let series = flow.store().series("team", first, first + 3).unwrap();
1293        assert_eq!(
1294            series.iter().map(|(day, _)| *day).collect::<Vec<_>>(),
1295            vec![first, first + 1, first + 3],
1296            "oldest first, and the silent day is not a row"
1297        );
1298        assert_eq!(
1299            series.iter().map(|(_, r)| r.bytes_in).collect::<Vec<_>>(),
1300            vec![100, 200, 400]
1301        );
1302        // A window that starts after the first day starts there.
1303        let narrow = flow.store().series("team", first + 1, first + 3).unwrap();
1304        assert_eq!(narrow.len(), 2);
1305    }
1306
1307    /// **Retention is 400 days, and the writer prunes.** A row older than the
1308    /// window is gone the next time its namespace is written.
1309    #[test]
1310    fn nothing_older_than_the_window_survives_a_write() {
1311        let flow = ledger();
1312        let ancient = day_of(1_757_000_000_000);
1313        let at = midnight_of(ancient) + 1000;
1314        flow.record("team", at, &push(100, "SHA256:aaa"));
1315        flow.flush(at).unwrap();
1316        assert!(flow.store().day("team", ancient).unwrap().is_some());
1317
1318        let much_later = midnight_of(ancient + RETENTION_DAYS + 1) + 1000;
1319        flow.record("team", much_later, &push(1, "SHA256:aaa"));
1320        flow.flush(much_later).unwrap();
1321        assert_eq!(flow.store().day("team", ancient).unwrap(), None, "aged out");
1322        assert!(flow
1323            .store()
1324            .day("team", day_of(much_later))
1325            .unwrap()
1326            .is_some());
1327    }
1328
1329    /// Distinct principals are exact and de-duplicated; the ceiling says so
1330    /// when it is hit rather than flattening quietly.
1331    #[test]
1332    fn principals_are_exact_and_the_ceiling_announces_itself() {
1333        let flow = ledger();
1334        let now = 1_757_000_000_000;
1335        for _ in 0..5 {
1336            flow.record("team", now, &push(1, "SHA256:aaa"));
1337            flow.record("team", now, &fetch(1, "SHA256:bbb"));
1338        }
1339        let row = flow.today("team", now).unwrap();
1340        assert_eq!(row.distinct_principals(), 2, "ten transfers, two people");
1341        assert!(!row.principals_capped);
1342
1343        let mut wide = FlowDay::default();
1344        for n in 0..PRINCIPAL_CEILING + 10 {
1345            wide.principals.insert(format!("SHA256:{n}"));
1346        }
1347        let mut folded = FlowDay::default();
1348        folded.add(&wide);
1349        assert!(
1350            folded.principals_capped,
1351            "a count that stopped growing must say so"
1352        );
1353        assert_eq!(folded.distinct_principals(), PRINCIPAL_CEILING as u64);
1354    }
1355
1356    /// Forgetting a namespace forgets its whole series and its live slot.
1357    #[test]
1358    fn forgetting_a_namespace_forgets_its_flow() {
1359        let flow = ledger();
1360        let now = 1_757_000_000_000;
1361        flow.record("team", now, &push(4096, "SHA256:aaa"));
1362        flow.record("other", now, &push(1, "SHA256:aaa"));
1363        flow.flush(now).unwrap();
1364        assert!(flow.forget("team").unwrap());
1365        assert_eq!(flow.today("team", now), None);
1366        assert!(
1367            flow.today("other", now).is_some(),
1368            "the other tenant is untouched"
1369        );
1370    }
1371
1372    /// A transfer that moved nothing is not a row, and an empty namespace name
1373    /// is refused rather than charged to `""`.
1374    #[test]
1375    fn nothing_is_never_written_as_something() {
1376        let flow = ledger();
1377        let now = 1_757_000_000_000;
1378        flow.record("team", now, &Transfer::default());
1379        flow.record("", now, &push(4096, "SHA256:aaa"));
1380        assert_eq!(flow.today("team", now), None);
1381        assert_eq!(flow.today("", now), None);
1382        assert_eq!(flow.flush(now).unwrap(), 0, "nothing to write");
1383    }
1384
1385    /// The date renderer, against the two dates in this file's fixtures and the
1386    /// epoch itself.
1387    #[test]
1388    fn the_date_renderer_is_utc_and_floors_before_the_epoch() {
1389        assert_eq!(iso8601_utc(0), "1970-01-01T00:00:00Z");
1390        assert_eq!(iso8601_utc(1_757_000_000_000), "2025-09-04T15:33:20Z");
1391        assert_eq!(iso8601_utc(1_789_041_600_000), "2026-09-10T12:00:00Z");
1392        assert_eq!(
1393            iso8601_utc(-1),
1394            "1969-12-31T23:59:59Z",
1395            "one millisecond before the epoch"
1396        );
1397        // A leap day, which is where a table-free formula goes wrong if it can.
1398        assert_eq!(iso8601_utc(1_709_164_800_000), "2024-02-29T00:00:00Z");
1399    }
1400
1401    /// The day arithmetic, including before the epoch, where truncation toward
1402    /// zero would put two different days in one bucket.
1403    #[test]
1404    fn the_day_arithmetic_floors() {
1405        assert_eq!(day_of(0), 0);
1406        assert_eq!(day_of(DAY_MS - 1), 0);
1407        assert_eq!(day_of(DAY_MS), 1);
1408        assert_eq!(day_of(-1), -1, "the last millisecond of 1969 is not day 0");
1409        assert_eq!(midnight_of(day_of(1_757_000_000_000)), 1_756_944_000_000);
1410    }
1411}