gunnar-grpc 3.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! `UsageBatch`. Many namespaces' meters in one call (2026-10-06).
//!
//! **Why.** monetize's poller asked `Usage.Read` once per tenant every 300 s;
//! the load test on the UpCloud mockup measured ~25–30 s of 1.4 cores on main
//! per period at 1,000 tenants, and a per-call cost that itself grew with the
//! tenant count. `ReadMany` answers a page of namespaces out of the server's
//! usage ledger (gunnar-server `usage_ledger`), which re-measures only what
//! changed, and with `changed_since` it answers only the namespaces whose row
//! moved — so a poll is O(pages) calls and a quiet estate costs almost nothing.
//!
//! **The grant is `Usage.ReadAll`'s, and so is the shield**: server-wide `Read`
//! (it spans tenants), and an account's NESTED namespaces are kept from a grant
//! that does not reach inside the account.

use gunnar_proto::usage_batch_server::UsageBatch;
use gunnar_proto::{GrantLevel, ReadManyUsageRequest, ReadManyUsageResponse};
use tonic::{Request, Response, Status};

use crate::auth::Access;
use crate::service::UsageBatchService;

/// The page size when the caller names none.
pub const DEFAULT_PAGE_SIZE: u32 = 500;
/// The most rows one page carries, whatever the caller asked for.
pub const MAX_PAGE_SIZE: u32 = 5_000;

/// The page size actually served for an asked one.
pub fn page_size(asked: u32) -> usize {
    match asked {
        0 => DEFAULT_PAGE_SIZE as usize,
        n => n.min(MAX_PAGE_SIZE) as usize,
    }
}

#[tonic::async_trait]
impl UsageBatch for UsageBatchService {
    async fn read_many(
        &self,
        request: Request<ReadManyUsageRequest>,
    ) -> Result<Response<ReadManyUsageResponse>, Status> {
        let caller = self.ctx.authorize(
            &request,
            Access::Server,
            GrantLevel::Read,
            "UsageBatch.ReadMany",
        )?;
        let request = request.into_inner();
        let mut page = self.ctx.control.usage_page(
            &request.page_token,
            page_size(request.page_size),
            &request.changed_since,
        )?;
        // As `Usage.ReadAll`: an account's size is the operator's aggregate; a
        // namespace NESTED in it names the account's structure.
        page.usages = self.ctx.retain_unshielded(&caller, page.usages, |row| {
            row.namespace.contains('/').then(|| row.namespace.clone())
        })?;
        Ok(Response::new(page))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_page_size_defaults_and_is_clamped() {
        assert_eq!(page_size(0), 500);
        assert_eq!(page_size(7), 7);
        assert_eq!(page_size(1_000_000), 5_000);
    }
}