115 lines
4.8 KiB
Rust
115 lines
4.8 KiB
Rust
//! RFC 016 — runtime observability (Chunk 4: the observer gen_server).
|
||
//!
|
||
//! A thin [`GenServer`] that consumes the Chunk-1 read primitive
|
||
//! ([`snapshot`](crate::snapshot) / [`tree`](crate::tree) /
|
||
//! [`actor_info`](crate::actor_info)) over a message interface — the live
|
||
//! `observer` process, in the OTP sense. It is a *transport*, not the
|
||
//! mechanism: the synchronous internal read stays the primitive, and the
|
||
//! observer is just one more consumer of it alongside the test suite. This is
|
||
//! also the read half of the future RFC 003 control plane — the same actor
|
||
//! gains write verbs there rather than a second consumer being spun up
|
||
//! (DECISION D12).
|
||
//!
|
||
//! ## Why it is feature-gated (DECISION D10)
|
||
//!
|
||
//! The read primitive (Chunks 1–3) is always present and unflagged: it is pure
|
||
//! reads and the test suite leans on it. The *gen_server* sits behind the
|
||
//! `observer` Cargo feature, off by default, matching RFC 003's dev-only
|
||
//! feature-flag stance — a release build pays nothing for a live observer it
|
||
//! never starts.
|
||
//!
|
||
//! ## The protocol is the contract (DECISION D11)
|
||
//!
|
||
//! [`ObserverRequest`] / [`ObserverReply`] *are* the wire contract. They carry
|
||
//! no version field of their own because the payloads already do:
|
||
//! [`RuntimeSnapshot`](crate::RuntimeSnapshot) and
|
||
//! [`RuntimeTree`](crate::RuntimeTree) each carry
|
||
//! [`SNAPSHOT_FORMAT_VERSION`](crate::SNAPSHOT_FORMAT_VERSION) (D1). The owned
|
||
//! snapshot — a potentially large `Vec<ActorInfo>` — travels over the call
|
||
//! channel by value; that is intended, it is exactly what a remote observer
|
||
//! (RFC 011) will serialize across a node boundary.
|
||
|
||
use crate::gen_server::{GenServer, GenServerBuilder, GenServerRef};
|
||
use crate::introspect::{actor_info, snapshot, tree};
|
||
use crate::introspect::{ActorInfo, RuntimeSnapshot, RuntimeTree};
|
||
use crate::pid::Pid;
|
||
|
||
/// A read-only request to the observer. Each verb maps one-to-one onto a
|
||
/// Chunk-1 read; there are deliberately no mutating verbs here (those are RFC
|
||
/// 003, D12).
|
||
#[derive(Debug, Clone)]
|
||
pub enum ObserverRequest {
|
||
/// Whole-runtime [`snapshot`].
|
||
Snapshot,
|
||
/// Parentage forest, folded from a snapshot ([`tree`]).
|
||
Tree,
|
||
/// Coherent view of one actor ([`actor_info`]); `None` reply if the pid is
|
||
/// stale, forged, or names a vacant slot.
|
||
ActorInfo(Pid),
|
||
}
|
||
|
||
/// The observer's reply, tagged to match the [`ObserverRequest`] verb. Each
|
||
/// variant wraps the owned Chunk-1 read result unchanged — the observer adds no
|
||
/// interpretation, it is pure transport.
|
||
#[derive(Debug, Clone)]
|
||
pub enum ObserverReply {
|
||
Snapshot(RuntimeSnapshot),
|
||
Tree(RuntimeTree),
|
||
ActorInfo(Option<ActorInfo>),
|
||
}
|
||
|
||
/// The observer server. Stateless by construction (a ZST): every reply is
|
||
/// derived freshly from the live runtime on each call, so there is nothing to
|
||
/// keep between requests.
|
||
pub struct Observer;
|
||
|
||
impl GenServer for Observer {
|
||
type Call = ObserverRequest;
|
||
type Reply = ObserverReply;
|
||
/// No async verbs: the observer is request/reply only. `Infallible` is
|
||
/// uninhabited, so a `cast` can never be constructed and
|
||
/// [`handle_cast`](GenServer::handle_cast) is statically unreachable.
|
||
type Cast = core::convert::Infallible;
|
||
type Info = ();
|
||
type Timer = ();
|
||
|
||
fn handle_call(&mut self, request: ObserverRequest) -> ObserverReply {
|
||
match request {
|
||
ObserverRequest::Snapshot => ObserverReply::Snapshot(snapshot()),
|
||
ObserverRequest::Tree => ObserverReply::Tree(tree()),
|
||
ObserverRequest::ActorInfo(pid) => ObserverReply::ActorInfo(actor_info(pid)),
|
||
}
|
||
}
|
||
|
||
fn handle_cast(&mut self, request: core::convert::Infallible) {
|
||
// Uninhabited: this match has no arms because `Cast` cannot be
|
||
// constructed. It documents at the type level that the observer takes
|
||
// no fire-and-forget traffic.
|
||
match request {}
|
||
}
|
||
}
|
||
|
||
/// Spawn the observer under the current actor and hand back its [`GenServerRef`].
|
||
/// Shorthand for `GenServerBuilder::new(Observer).start()`; use the builder
|
||
/// directly (e.g. `.under(sup)`) to slot it into a supervision tree.
|
||
///
|
||
/// ```
|
||
/// use smarm::run;
|
||
/// use smarm::observer::{self, ObserverRequest, ObserverReply};
|
||
///
|
||
/// run(|| {
|
||
/// let obs = observer::start();
|
||
///
|
||
/// // Ask for a whole-runtime snapshot over the call channel.
|
||
/// let ObserverReply::Snapshot(snap) = obs.call(ObserverRequest::Snapshot).unwrap()
|
||
/// else { panic!("snapshot verb must reply with a snapshot") };
|
||
///
|
||
/// // The observer is itself a scheduled actor, so it appears in the very
|
||
/// // snapshot it produced — transport over the same read every consumer sees.
|
||
/// assert!(snap.actors.iter().any(|a| a.pid == obs.pid()));
|
||
/// });
|
||
/// ```
|
||
pub fn start() -> GenServerRef<Observer> {
|
||
GenServerBuilder::new(Observer).start()
|
||
}
|