Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/services/estate.rs

//! **`EstateAdmin.Draw`** — the estate, drawn, over the wire.
//!
//! The renderer is [`crate::estate_svg`]; this is the door onto it, and the
//! whole of its job is to put the two halves in front of it honestly:
//!
//! * the DECLARED half is [`AppState::config`] — `[machines.<id>]`, the clouds,
//!   and which cloud a purchase spends at. It is the file this server started
//!   from, not a re-read: a picture drawn from a config the running process
//!   never loaded would show machines it has never heard of.
//! * the MEASURED half is [`Measured::from_product`], which asks the product's
//!   plugin what it already knows. Nothing here measures anything, and nothing
//!   here turns a refusal into a number — that rule lives in the renderer and
//!   in the plugin, and this file must not be the place it is quietly undone.
//!
//! # Two refusals, on purpose
//!
//! **A product this server does not serve is `NOT_FOUND`.** `estate_svg::from_config`
//! would cheerfully draw an empty board for a misspelt product, and an empty
//! board is already what an undeclared estate looks like — so a typo would be
//! indistinguishable from the truth. [`product_or_not_found`] names the products
//! that ARE linked in, which is the answer the operator needs.
//!
//! **A product that declares no machines is not asked anything.** With an empty
//! roll call there is nothing on the board for a reading to land on, so
//! [`Measured::default`] is the complete and honest answer and the plugin's box
//! is not disturbed. This is not an optimisation: it is what keeps a picture of
//! an estate that is not written down from hanging on a box that is not going to
//! be drawn.
//!
//! # What is deliberately NOT filled in
//!
//! [`Measured::tenants`] stays `None`, so the board's subtitle carries no tenant
//! count. Counting them means `Product::list_tenants` — a second live call to
//! the same box, for a number that already has a screen of its own
//! (`Products` → the product's tenants). A picture of the IRON should not hang
//! on the tenant list.

use std::sync::Arc;

use monetize_proto as pb;
use monetize_proto::estate_admin_server::EstateAdmin;
use tonic::{Request, Response, Status};

use crate::estate_svg::{estate_svg, Measured};
use crate::services::{blocking, product_or_not_found};
use crate::state::AppState;

pub struct EstateAdminService(pub Arc<AppState>);

#[tonic::async_trait]
impl EstateAdmin for EstateAdminService {
    async fn draw(
        &self,
        request: Request<pb::DrawEstateRequest>,
    ) -> Result<Response<pb::DrawEstateResponse>, Status> {
        let r = request.into_inner();
        let (product, estate) = (r.product, r.estate);
        // On the blocking pool like every other handler that touches a
        // provider: `Product::servable` and `Product::twins` are sync calls to
        // another box.
        let (svg, machines) = blocking(&self.0, move |state| {
            Ok(if estate.is_empty() {
                draw_counted(state, &product)
            } else {
                draw_site(state, &product, &estate)
            })
        })
        .await??;
        Ok(Response::new(pb::DrawEstateResponse {
            svg,
            machines: Some(machines),
        }))
    }
}

/// **A site's board from its live fleet reports** ([`crate::estate_svg::from_fleet`]);
/// the product's `[machines]` board when no machine of the site has reported yet.
pub fn draw_site(state: &AppState, product: &str, estate: &str) -> Result<(String, u32), Status> {
    product_or_not_found(state, product)?;
    let Some(g) = state.fleet.guardian() else {
        return draw_counted(state, product);
    };
    let boxes: Vec<crate::estate_svg::Reported> = g
        .store
        .roles_of(estate)
        .into_iter()
        .map(|role| {
            let rec = g.store.box_record(estate, &role).unwrap_or_default();
            crate::estate_svg::Reported {
                serves_at: rec
                    .report
                    .health
                    .get("serves_at")
                    .cloned()
                    .unwrap_or_default(),
                heard_unix_ms: rec.heard_unix_ms,
                role,
            }
        })
        .collect();
    let e = crate::estate_svg::from_fleet(product, &boxes, state.now(), g.cfg.silent_after_ms);
    if e.machines.is_empty() {
        return draw_counted(state, product);
    }
    let n = e.machines.len() as u32;
    Ok((estate_svg(&e), n))
}

/// The whole of the drawing, on the blocking pool. Separate from the trait
/// method so a test can hold the picture to account without a wire.
pub fn draw(state: &AppState, product: &str) -> Result<String, Status> {
    draw_counted(state, product).map(|(svg, _)| svg)
}

/// [`draw`], and how many machines the board carries — counted off the SAME
/// roll call the picture was drawn from, so the number a console prints
/// beside the board ("this estate names no machines yet") cannot disagree
/// with the board.
pub fn draw_counted(state: &AppState, product: &str) -> Result<(String, u32), Status> {
    let plugin = product_or_not_found(state, product)?;
    let config = state
        .config
        .read()
        .unwrap_or_else(|e| e.into_inner())
        .clone();
    let roll = crate::estate_svg::roll_call(&config, product);
    // See the module's header: an empty roll call is asked nothing.
    let live = if roll.is_empty() {
        Measured::default()
    } else {
        Measured::from_product(plugin, &roll)
    };
    let estate = crate::estate_svg::from_config(&config, product, &live);
    Ok((estate_svg(&estate), roll.len() as u32))
}