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))
}
}