//! 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` — 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), } /// 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 { GenServerBuilder::new(Observer).start() }