crates/gunnar-grpc/src/service/usage.rs
//! `Usage`. Per-namespace meters, read-only.
//!
//! **Open source, always on.** Counting what a tenant holds is not the metered
//! appliance's capability — it is every appliance's, because a self-hoster
//! reading their own disk wants the same numbers a service provider bills
//! from. What is behind the `metered` feature is *acting* on them
//! (`Entitlement`); this service only reports.
//!
//! The grant split is `Admin.StorageInfo`'s: one namespace's meters are that
//! namespace's business (`Read` on the namespace, so an owner can see their own
//! bill), a server-wide read spans tenants and takes a server-wide `Read`.
//!
//! `Read` also carries the namespace's **allowance** — per meter, the cap this
//! box holds for it, absent when there is none (see `usage.proto`). On the
//! metered appliance that is read off the entitlement seam for the one
//! namespace the caller may already read; on the community appliance every
//! cap is absent, because there is nothing to read it from.
use gunnar_proto::usage_server::Usage;
use gunnar_proto::{
ActivityRequest, ActivityResponse, AdmitPlanRequest, AdmitPlanResponse, Allowance, GrantLevel,
NamespaceUsage, ReadAllUsageRequest, ReadAllUsageResponse, ReadUsageRequest, ReadUsageResponse,
Remaining,
};
use tonic::{Request, Response, Status};
use crate::auth::Access;
use crate::service::UsageService;
/// **A signed fact's `caps`, as the allowance `Usage.Read` reports.**
///
/// One rule, the push door's (`gunnar_server::entitlement`, "0 is
/// unlimited"): a meter the fact does not name and a meter it names as `0`
/// are both ABSENT — never `Some(0)`, which a reader would take for "may hold
/// nothing". Keyed by the meter names `NamespaceUsage` reports, which are the
/// keys monetize writes.
///
/// **Every cap goes out labelled** with what this box does at it
/// (`crate::service::meters`), because four of these seven are a reading and
/// three are a door, and reporting a reading in a message called `Allowance`
/// with no other word for it is how `caps["repos"]=5` came to be shown to a
/// tenant as a limit that nothing on the box refuses. See
/// [`crate::service::meters`] for the table and for the two candidate fixes
/// that were not taken.
#[cfg(feature = "metered")]
pub fn allowance_of(caps: &std::collections::BTreeMap<String, u64>) -> Allowance {
let cap = |meter: &str| caps.get(meter).copied().filter(|cap| *cap > 0);
Allowance {
enforcement: crate::service::meters::enforcement(),
pack_bytes: cap("pack_bytes"),
cache_bytes: cap("cache_bytes"),
cold_bytes: cap("cold_bytes"),
lfs_bytes: cap("lfs_bytes"),
tombstoned_bytes: cap("tombstoned_bytes"),
repos: cap("repos"),
objects: cap("objects"),
}
}
/// **No cap on anything, and the same seven words about what a cap WOULD do.**
///
/// The answer for a Free namespace and for every namespace on the community
/// appliance. `Allowance::default()` would leave `enforcement` empty, and an
/// empty map is not "nothing is enforced" — it is "this build did not say",
/// which is the one thing a reader must never have to guess. The community
/// appliance holds no caps for anyone, and its doors are the same three doors:
/// both halves of that are true at once and the message says both.
fn uncapped() -> Allowance {
Allowance {
enforcement: crate::service::meters::enforcement(),
..Allowance::default()
}
}
/// **What is left under each cap**: cap − held, clamped at 0, ABSENT exactly
/// where the cap is absent (2026-10-04, the thin layer: gunnar's answer, so a
/// console stops computing its own four).
pub fn remaining_of(usage: &NamespaceUsage, allowance: &Allowance) -> Remaining {
let left = |cap: Option<u64>, held: u64| cap.map(|cap| cap.saturating_sub(held));
Remaining {
pack_bytes: left(allowance.pack_bytes, usage.pack_bytes),
cache_bytes: left(allowance.cache_bytes, usage.cache_bytes),
cold_bytes: left(allowance.cold_bytes, usage.cold_bytes),
lfs_bytes: left(allowance.lfs_bytes, usage.lfs_bytes),
tombstoned_bytes: left(allowance.tombstoned_bytes, usage.tombstoned_bytes),
repos: left(allowance.repos, usage.repos),
objects: left(allowance.objects, usage.objects),
}
}
impl UsageService {
/// The namespace's allowance: every cap absent, unless this is the
/// metered appliance AND it holds a fact for the namespace naming the
/// meter. The community appliance has no entitlement store to ask, so
/// the answer is the same [`uncapped`] for every namespace — not a stub,
/// the truth: nothing on that box caps anyone.
///
/// The caller already holds `Read` on the namespace; nothing here is a
/// second grant check, and nothing here reaches `Entitlement.Get`'s
/// server-wide gate — the seam is asked directly, for this one namespace.
fn allowance(&self, namespace: &str) -> Result<Allowance, Status> {
#[cfg(feature = "metered")]
{
use crate::seam::ControlError;
// **Clans** (2026-10-05): a clan's room is the sum of what its
// members give — the number the door holds it to, 0 included (a
// clan nobody gave room to holds nothing; no fact is signed for a
// clan) — and a member's own room is its cap less what it gives.
//
// Round 2: the room IN FORCE (a lapsed member's share dropped),
// and ONE room for packs and LFS objects together — so each meter
// is allowed the room less what the other holds, and what is left
// under either is what is left of the one room.
let clans = self.ctx.control.clans();
if let Some(clan) = clans.map(|p| p.clan_get(namespace)).transpose()?.flatten() {
let room = clan.room_in_force_bytes;
let (pack, lfs) = self
.ctx
.control
.usage(Some(namespace))
.ok()
.and_then(|rows| rows.into_iter().find(|row| row.namespace == namespace))
.map_or((0, 0), |row| (row.pack_bytes, row.lfs_bytes));
return Ok(Allowance {
pack_bytes: Some(room.saturating_sub(lfs)),
lfs_bytes: Some(room.saturating_sub(pack)),
..uncapped()
});
}
match self.ctx.control.entitlement_get(namespace) {
Ok(Some(fact)) => {
let mut allowance = allowance_of(&fact.caps);
if let (Some(cap), Some(plane)) = (allowance.pack_bytes, clans) {
allowance.pack_bytes =
Some(cap.saturating_sub(plane.clan_given(namespace)?));
}
return Ok(allowance);
}
// No fact = Free = uncapped.
Ok(None) => {}
// A metered server started without `--monetize-pubkey` holds
// no facts at all (`CoreControlPlane::entitlement` says so by
// name); every namespace is Free there, and a usage read must
// not fail on it.
Err(ControlError::FailedPrecondition(_)) => {}
Err(err) => return Err(err.into()),
}
}
#[cfg(not(feature = "metered"))]
let _ = namespace;
Ok(uncapped())
}
}
#[tonic::async_trait]
impl Usage for UsageService {
async fn read(
&self,
request: Request<ReadUsageRequest>,
) -> Result<Response<ReadUsageResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let namespace = request.into_inner().namespace;
if namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&namespace),
GrantLevel::Read,
"Usage.Read",
)?;
let mut rows = self.ctx.control.usage(Some(&namespace))?;
let usage = rows.pop().ok_or_else(|| {
Status::not_found(format!("namespace {namespace} is not in the catalog"))
})?;
let allowance = self.allowance(&namespace)?;
let remaining = remaining_of(&usage, &allowance);
// Where the room went, on the same `Read`. A plane that measures the
// meters but not the rows answers the meters alone rather than nothing.
let repos = match self.ctx.control.repo_usage(&namespace) {
Ok(rows) => rows,
Err(crate::seam::ControlError::Unimplemented(_)) => Vec::new(),
Err(err) => return Err(err.into()),
};
Ok(Response::new(ReadUsageResponse {
usage: Some(usage),
allowance: Some(allowance),
remaining: Some(remaining),
repos,
}))
}
async fn read_all(
&self,
request: Request<ReadAllUsageRequest>,
) -> Result<Response<ReadAllUsageResponse>, Status> {
let caller =
self.ctx
.authorize(&request, Access::Server, GrantLevel::Read, "Usage.ReadAll")?;
let usages = self.ctx.control.usage(None)?;
// **An account's size, never its insides** (2026-10-05): the row of a
// customer's account itself is the operator's aggregate (what it
// holds, what it was sold); a namespace NESTED in it names the
// account's structure and is not shown to a server grant that does
// not reach inside.
let usages = self.ctx.retain_unshielded(&caller, usages, |row| {
row.namespace.contains('/').then(|| row.namespace.clone())
})?;
Ok(Response::new(ReadAllUsageResponse { usages }))
}
/// **The flow series for one namespace.**
///
/// The grant is `Read` on the NAMESPACE, deliberately the same one
/// [`Usage::read`] takes and not the server-wide grant `ReadAll` takes:
/// what a tenant pushed and fetched is that tenant's own business, and a
/// tenant who can read their meters can read how they got them. Making it
/// server-wide would have put a tenant's own traffic behind an operator
/// credential, which is the shape of gate that ends with a console reading
/// everyone's numbers to show one person theirs.
///
/// Nothing here re-derives the window: the control plane resolves `0` to
/// "the whole kept window" / "now" and clamps to whole UTC days, because it
/// is the half that knows what this server actually keeps. A second
/// arithmetic here would be a second answer to the same question.
async fn activity(
&self,
request: Request<ActivityRequest>,
) -> Result<Response<ActivityResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
if request.namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&request.namespace),
GrantLevel::Read,
"Usage.Activity",
)?;
let (days, retained_days) = self.ctx.control.activity(
&request.namespace,
request.since_unix_ms,
request.until_unix_ms,
)?;
Ok(Response::new(ActivityResponse {
days,
retained_days,
}))
}
/// `Usage.AdmitPlan` — the SAME grant as `Activity`: `Read` on the
/// namespace. A tenant may read their own budget and what it buys.
async fn admit_plan(
&self,
request: Request<AdmitPlanRequest>,
) -> Result<Response<AdmitPlanResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
if request.namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&request.namespace),
GrantLevel::Read,
"Usage.AdmitPlan",
)?;
Ok(Response::new(
self.ctx.control.admit_plan(&request.namespace)?,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn held() -> NamespaceUsage {
NamespaceUsage {
namespace: "acme".into(),
pack_bytes: 700,
cache_bytes: 50,
lfs_bytes: 0,
repos: 3,
..Default::default()
}
}
/// ★ cap − held under the cap; 0 — never a wrapped huge number — over it;
/// absent where the cap is absent, and a 0 held under a cap is the whole cap.
#[test]
fn what_is_left_is_cap_minus_held_clamped_at_zero_and_absent_where_uncapped() {
let allowance = Allowance {
pack_bytes: Some(1_000),
cache_bytes: Some(40),
lfs_bytes: Some(9),
repos: None,
..Allowance::default()
};
let left = remaining_of(&held(), &allowance);
assert_eq!(left.pack_bytes, Some(300), "under the cap");
assert_eq!(left.cache_bytes, Some(0), "over the cap is 0, not a wrap");
assert_eq!(left.lfs_bytes, Some(9), "nothing held: the whole cap");
assert_eq!(left.repos, None, "uncapped is absent, never a number");
assert_eq!(left.cold_bytes, None);
assert_eq!(left.tombstoned_bytes, None);
assert_eq!(left.objects, None);
}
}