RFC 016 Chunk 4: observer gen_server (feature-gated)
A thin GenServer consumer of the Chunk-1 read primitive — the live observer process (D12). ObserverRequest/ObserverReply are the wire contract (D11); the version rides along on the snapshot/tree payloads, which already carry SNAPSHOT_FORMAT_VERSION (D1). Behind the new `observer` Cargo feature, off by default (D10): the primitive stays always-on, only the transport is gated. Cast is Infallible, so the server takes no async traffic and handle_cast is statically unreachable. Gated integration test proves each verb relays exactly what the corresponding primitive returns (snapshot/tree/actor_info), incl. a forged-pid None and a live Parked classification.
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
//! 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, ServerBuilder, ServerRef};
|
||||
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 [`ServerRef`].
|
||||
/// Shorthand for `ServerBuilder::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() -> ServerRef<Observer> {
|
||||
ServerBuilder::new(Observer).start()
|
||||
}
|
||||
Reference in New Issue
Block a user