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/callback_admin.rs

//! **`CallbackAdmin.ListRefusals`** — the operator's read of the refused
//! vendor callbacks.
//!
//! One rpc, no write side. The store is [`crate::refusals::RefusalLog`]: a
//! bounded in-memory ring that [`crate::callback`] is the only writer of, and
//! that nothing in this server reads back as authority.
//!
//! # Why a list as well as the bus
//!
//! Every refusal is also published on `Events.Watch`, which is what makes a
//! console that is already open light up the moment one lands. That is not
//! enough on its own and this rpc is not a duplicate of it: the bus is a
//! bounded ring with no log and no replay from a cold cursor, so a console
//! opened ten minutes into an outage — which is the console in the 2026-09-14
//! incident — was not subscribed when anything happened and would show an empty
//! page. "Tell me as it happens" and "tell me what happened" are two questions.
//!
//! # There is nothing here to page through
//!
//! `limit` cuts from the newest end and the ceiling is the whole ring. A
//! cursor would imply a history behind the list, and there is none: past
//! [`crate::refusals::CAPACITY`] the oldest refusals are gone, and
//! `total_since_start` is how a reader learns that rather than by asking for a
//! page that cannot be served.

use std::sync::Arc;

use monetize_proto::callback_admin_server::CallbackAdmin;
use monetize_proto::{self as pb};
use tonic::{Request, Response, Status};

use super::blocking;
use crate::state::AppState;

pub struct CallbackAdminService(pub Arc<AppState>);

#[tonic::async_trait]
impl CallbackAdmin for CallbackAdminService {
    async fn list_refusals(
        &self,
        req: Request<pb::ListRefusalsRequest>,
    ) -> Result<Response<pb::ListRefusalsResponse>, Status> {
        let limit = req.into_inner().limit as usize;
        // On the blocking pool like every other read here, though this one takes
        // a mutex rather than a redb transaction: one shape for all of them
        // beats an exception nobody can see the reason for from the call site.
        let out = blocking(&self.0, move |s| {
            let stats = s.refusals.stats();
            Ok(pb::ListRefusalsResponse {
                refusals: s
                    .refusals
                    .recent(limit)
                    .iter()
                    .map(crate::refusals::Refused::to_pb)
                    .collect(),
                total_since_start: stats.total,
                kept: stats.kept,
                capacity: stats.capacity,
                first_at_unix_ms: stats.first_at_unix_ms,
                last_at_unix_ms: stats.last_at_unix_ms,
            })
        })
        .await?;
        Ok(Response::new(out))
    }
}