Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf6b80396 | ||
|
|
3e316066c3 |
@@ -1,11 +1,9 @@
|
|||||||
//! SPIKE (throwaway) — RFC 017 "fused" approach.
|
//! The hand-written **expansion target** of the `gen_statem!` macro: the same
|
||||||
|
//! machine as `examples/gen_statem_macro.rs`, written out in full so the
|
||||||
|
//! primitives can be judged standing on their own. The macro generates exactly
|
||||||
|
//! this shape; nothing here needs the macro to be correct or safe.
|
||||||
//!
|
//!
|
||||||
//! This is the hand-written *expansion target* of the eventual `statem!` macro,
|
//! The design:
|
||||||
//! written out in full so the primitives can be judged standing on their own.
|
|
||||||
//! A macro would generate exactly this from a `transitions { … }` block; nothing
|
|
||||||
//! here needs the macro to be correct or safe.
|
|
||||||
//!
|
|
||||||
//! The design, as settled over the prior iterations:
|
|
||||||
//!
|
//!
|
||||||
//! * States and events are real enums. An invalid state is unrepresentable;
|
//! * States and events are real enums. An invalid state is unrepresentable;
|
||||||
//! there are no bitflags, no `u32` superpositions, no unsafe unions.
|
//! there are no bitflags, no `u32` superpositions, no unsafe unions.
|
||||||
@@ -27,18 +25,18 @@
|
|||||||
//! wired in is dead code — and dead_code is denied below, making an orphan
|
//! wired in is dead code — and dead_code is denied below, making an orphan
|
||||||
//! handler a compile error.
|
//! handler a compile error.
|
||||||
//!
|
//!
|
||||||
//! * Drops onto the committed `Machine` / `Resolution` / `Cx` primitives with
|
//! * Drops onto the `Machine` / `Resolution` / `Cx` primitives with no change
|
||||||
//! no change to `src/statem.rs`.
|
//! to `src/gen_statem.rs`.
|
||||||
//!
|
//!
|
||||||
//! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents
|
//! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents
|
||||||
//! how to make each of the four guarantees fire.
|
//! how to make each of the four guarantees fire.
|
||||||
//!
|
//!
|
||||||
//! Run: `cargo run --example statem_fused`
|
//! Run: `cargo run --example gen_statem_fused`
|
||||||
|
|
||||||
#![deny(dead_code, unreachable_patterns)]
|
#![deny(dead_code, unreachable_patterns)]
|
||||||
|
|
||||||
use smarm::run;
|
use smarm::run;
|
||||||
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, StatemRef};
|
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, GenStatemRef};
|
||||||
|
|
||||||
// === user types ============================================================
|
// === user types ============================================================
|
||||||
|
|
||||||
@@ -114,7 +112,7 @@ struct DoorSm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DoorSm {
|
impl DoorSm {
|
||||||
fn start(init: Door) -> StatemRef<DoorSm> {
|
fn start(init: Door) -> GenStatemRef<DoorSm> {
|
||||||
spawn(DoorSm {
|
spawn(DoorSm {
|
||||||
state: init,
|
state: init,
|
||||||
data: Data { enters: 0, pushes: 0 },
|
data: Data { enters: 0, pushes: 0 },
|
||||||
@@ -184,7 +182,7 @@ impl Machine for DoorSm {
|
|||||||
self.state = s; // sole writer of the state cell
|
self.state = s; // sole writer of the state cell
|
||||||
self.enter(cx);
|
self.enter(cx);
|
||||||
}
|
}
|
||||||
Resolution::Postpone => unreachable!("postpone lands in chunk 3"),
|
Resolution::Postpone => unreachable!("postpone is not generated yet"),
|
||||||
Resolution::Unhandled => cx.on_unhandled(),
|
Resolution::Unhandled => cx.on_unhandled(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
//! RFC 017 — the **same** machine as `examples/statem_fused.rs`, written through
|
//! The **same** machine as `examples/gen_statem_fused.rs`, written through the
|
||||||
//! the `gen_statem!` macro. Diff this file against that one to see exactly what
|
//! `gen_statem!` macro. Diff this file against that one to see exactly what the
|
||||||
//! the macro buys: every `// ===` section there that was boilerplate (the `Ev`
|
//! macro buys: every `// ===` section there that was boilerplate (the `Ev`
|
||||||
//! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter`
|
//! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter`
|
||||||
//! dispatch, the stay/transition apply-tail) collapses into the invocation
|
//! dispatch, the stay/transition apply-tail) collapses into the invocation
|
||||||
//! below. What stays hand-written is what carries meaning: the four types, the
|
//! below. What stays hand-written is what carries meaning: the four types, the
|
||||||
//! per-state successor enum, and the handler fns.
|
//! per-state successor enum, and the handler fns.
|
||||||
//!
|
//!
|
||||||
//! The point of the exercise is that the macro is *pure sugar*: the four
|
//! The point of the exercise is that the macro is *pure sugar*: the four
|
||||||
//! compile-time guarantees the fused spike demonstrates are properties of the
|
//! compile-time guarantees the hand-written form demonstrates are properties of
|
||||||
//! emitted code, not of the macro, so they survive expansion unchanged. The
|
//! the emitted code, not of the macro, so they survive expansion unchanged. The
|
||||||
//! BREAK-CASE MENU at the bottom is the same four cases, re-expressed against
|
//! BREAK-CASE MENU at the bottom is the same four cases, re-expressed against
|
||||||
//! the macro surface — flip any one on and the compiler fires identically.
|
//! the macro surface — flip any one on and the compiler fires identically.
|
||||||
//!
|
//!
|
||||||
//! Run: `cargo run --example statem_macro`
|
//! Run: `cargo run --example gen_statem_macro`
|
||||||
|
|
||||||
#![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the
|
#![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the
|
||||||
// dispatch's own unreachable_patterns internally.
|
// dispatch's own unreachable_patterns internally.
|
||||||
@@ -21,7 +21,7 @@ use smarm::gen_statem;
|
|||||||
use smarm::run;
|
use smarm::run;
|
||||||
use smarm::gen_statem::Reply;
|
use smarm::gen_statem::Reply;
|
||||||
|
|
||||||
// === user types (identical to statem_fused.rs) =============================
|
// === user types (identical to gen_statem_fused.rs) =========================
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
enum Door {
|
enum Door {
|
||||||
@@ -91,7 +91,7 @@ gen_statem! {
|
|||||||
|
|
||||||
// You name the bindings the bodies use; the macro can't lend you its own
|
// You name the bindings the bodies use; the macro can't lend you its own
|
||||||
// `self`/`cx` across macro hygiene. `data` = &mut Data, `prev` = current
|
// `self`/`cx` across macro hygiene. `data` = &mut Data, `prev` = current
|
||||||
// state tag, `cx` = context handle (unused in chunk 1).
|
// state tag, `cx` = context handle (unused here).
|
||||||
context(data, prev, cx);
|
context(data, prev, cx);
|
||||||
|
|
||||||
enter {
|
enter {
|
||||||
@@ -145,7 +145,7 @@ fn main() {
|
|||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly
|
// BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly
|
||||||
// as it does in the hand-written statem_fused.rs.
|
// as it does in the hand-written gen_statem_fused.rs.
|
||||||
//
|
//
|
||||||
// 1. ORPHAN HANDLER (dead_code -> error):
|
// 1. ORPHAN HANDLER (dead_code -> error):
|
||||||
// add `fn on_slam(_d: &mut Data) {}` and don't reference it.
|
// add `fn on_slam(_d: &mut Data) {}` and don't reference it.
|
||||||
@@ -157,8 +157,8 @@ fn main() {
|
|||||||
// NOTE: this example is a separate crate from `smarm`, so rustc's
|
// NOTE: this example is a separate crate from `smarm`, so rustc's
|
||||||
// in_external_macro rule SILENCES this lint here even though the macro
|
// in_external_macro rule SILENCES this lint here even though the macro
|
||||||
// denies it — the duplicate compiles. The guarantee is real only for
|
// denies it — the duplicate compiles. The guarantee is real only for
|
||||||
// machines defined inside the smarm crate (see the unit test in
|
// machines defined inside the `smarm` crate itself. This is the documented
|
||||||
// src/statem.rs). This is the documented macro_rules! limitation.
|
// macro_rules! limitation.
|
||||||
//
|
//
|
||||||
// 3. MISSING PAIR (non-exhaustive match, E0004):
|
// 3. MISSING PAIR (non-exhaustive match, E0004):
|
||||||
// delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,`
|
// delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,`
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
//!
|
//!
|
||||||
//! A gen_server is multi-message (call / cast over one inbox), so it is named
|
//! A gen_server is multi-message (call / cast over one inbox), so it is named
|
||||||
//! by the *server* type rather than by a single message type. Registering it
|
//! by the *server* type rather than by a single message type. Registering it
|
||||||
//! under a [`ServerName`] lets clients `call` and `cast` by name, resolving on
|
//! under a [`GenServerName`] lets clients `call` and `cast` by name, resolving on
|
||||||
//! every use — so the address keeps working across a supervised restart, with
|
//! every use — so the address keeps working across a supervised restart, with
|
||||||
//! no stale [`ServerRef`] to refresh.
|
//! no stale [`GenServerRef`] to refresh.
|
||||||
|
|
||||||
use smarm::{call, cast, run, whereis_server, GenServer, ServerBuilder, ServerName, ServerRef};
|
use smarm::{call, cast, run, whereis_server, GenServer, GenServerBuilder, GenServerName, GenServerRef};
|
||||||
|
|
||||||
/// A counter server: synchronous `Get`, asynchronous `Inc` / `Add`.
|
/// A counter server: synchronous `Get`, asynchronous `Inc` / `Add`.
|
||||||
struct Counter {
|
struct Counter {
|
||||||
@@ -41,13 +41,13 @@ impl GenServer for Counter {
|
|||||||
|
|
||||||
/// A durable name typed by the server, so by-name `call` / `cast` check against
|
/// A durable name typed by the server, so by-name `call` / `cast` check against
|
||||||
/// `Counter`'s `Call` / `Cast` / `Reply`.
|
/// `Counter`'s `Call` / `Cast` / `Reply`.
|
||||||
const COUNTER: ServerName<Counter> = ServerName::new("counter");
|
const COUNTER: GenServerName<Counter> = GenServerName::new("counter");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
run(|| {
|
run(|| {
|
||||||
// Start the server and bind its name in one step. A named start is
|
// Start the server and bind its name in one step. A named start is
|
||||||
// fallible: the name may already be held by another live server.
|
// fallible: the name may already be held by another live server.
|
||||||
ServerBuilder::new(Counter { n: 0 })
|
GenServerBuilder::new(Counter { n: 0 })
|
||||||
.named(COUNTER)
|
.named(COUNTER)
|
||||||
.start()
|
.start()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -60,8 +60,8 @@ fn main() {
|
|||||||
assert_eq!(call(COUNTER, Query::Get).unwrap(), 42);
|
assert_eq!(call(COUNTER, Query::Get).unwrap(), 42);
|
||||||
|
|
||||||
// When you want a handle to hold or pass on rather than resolve per
|
// When you want a handle to hold or pass on rather than resolve per
|
||||||
// call, recover a typed `ServerRef` from the name.
|
// call, recover a typed `GenServerRef` from the name.
|
||||||
let svc: Option<ServerRef<Counter>> = whereis_server(COUNTER);
|
let svc: Option<GenServerRef<Counter>> = whereis_server(COUNTER);
|
||||||
if let Some(svc) = svc {
|
if let Some(svc) = svc {
|
||||||
let _ = svc.call(Query::Get);
|
let _ = svc.call(Query::Get);
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-71
@@ -2,9 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! A thin request-reply layer on top of [`channel`](crate::channel()), modelled
|
//! A thin request-reply layer on top of [`channel`](crate::channel()), modelled
|
||||||
//! on Erlang's `gen_server`. A *server* is an actor owning a state value that
|
//! on Erlang's `gen_server`. A *server* is an actor owning a state value that
|
||||||
//! implements [`GenServer`]; clients hold a clonable [`ServerRef`] and issue
|
//! implements [`GenServer`]; clients hold a clonable [`GenServerRef`] and issue
|
||||||
//! [`call`](ServerRef::call) (synchronous, returns a reply) or
|
//! [`call`](GenServerRef::call) (synchronous, returns a reply) or
|
||||||
//! [`cast`](ServerRef::cast) (fire-and-forget).
|
//! [`cast`](GenServerRef::cast) (fire-and-forget).
|
||||||
//!
|
//!
|
||||||
//! ## One inbox, many arms
|
//! ## One inbox, many arms
|
||||||
//!
|
//!
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Out-of-band messages ride *separate* channels composed at the wait via
|
//! Out-of-band messages ride *separate* channels composed at the wait via
|
||||||
//! [`select`](channel::select): info channels handed over at start
|
//! [`select`](channel::select): info channels handed over at start
|
||||||
//! ([`ServerBuilder::with_info`]) are dispatched to
|
//! ([`GenServerBuilder::with_info`]) are dispatched to
|
||||||
//! [`handle_info`](GenServer::handle_info). Arm priority is
|
//! [`handle_info`](GenServer::handle_info). Arm priority is
|
||||||
//! **infos before inbox**, in declaration order — a hot inbox cannot starve
|
//! **infos before inbox**, in declaration order — a hot inbox cannot starve
|
||||||
//! an out-of-band message; conversely a hot info channel CAN starve the
|
//! an out-of-band message; conversely a hot info channel CAN starve the
|
||||||
@@ -45,19 +45,19 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Time (timers and idle)
|
//! ## Time (timers and idle)
|
||||||
//!
|
//!
|
||||||
//! A server arms timers through a [`TimerHandle`] cloned from the [`ServerCtx`]
|
//! A server arms timers through a [`TimerHandle`] cloned from the [`GenServerCtx`]
|
||||||
//! in `init` and stored on the state — the same shape as [`Watcher`] for
|
//! in `init` and stored on the state — the same shape as [`Watcher`] for
|
||||||
//! monitors. [`arm_after`](TimerHandle::arm_after) is a one-shot,
|
//! monitors. [`arm_after`](TimerHandle::arm_after) is a one-shot,
|
||||||
//! [`tick_every`](TimerHandle::tick_every) a periodic; both fire into
|
//! [`tick_every`](TimerHandle::tick_every) a periodic; both fire into
|
||||||
//! [`handle_timer`](GenServer::handle_timer) at *system priority* (above infos
|
//! [`handle_timer`](GenServer::handle_timer) at *system priority* (above infos
|
||||||
//! and the inbox, by arm position), and [`cancel`](TimerHandle::cancel) carries
|
//! and the inbox, by arm position), and [`cancel`](TimerHandle::cancel) carries
|
||||||
//! the substrate's race signal. Separately, [`ServerCtx::idle_after`] sets a
|
//! the substrate's race signal. Separately, [`GenServerCtx::idle_after`] sets a
|
||||||
//! receive-timeout window: quiet for the whole window fires
|
//! receive-timeout window: quiet for the whole window fires
|
||||||
//! [`handle_idle`](GenServer::handle_idle).
|
//! [`handle_idle`](GenServer::handle_idle).
|
||||||
//!
|
//!
|
||||||
//! Two unrelated things share the word *timeout*: the server-side **idle /
|
//! Two unrelated things share the word *timeout*: the server-side **idle /
|
||||||
//! receive timeout** above, and the client-side **call deadline**
|
//! receive timeout** above, and the client-side **call deadline**
|
||||||
//! ([`ServerRef::call_timeout`]) — how long a caller waits for a reply. They sit
|
//! ([`GenServerRef::call_timeout`]) — how long a caller waits for a reply. They sit
|
||||||
//! on different axes and never interact (RFC 015 §7).
|
//! on different axes and never interact (RFC 015 §7).
|
||||||
//!
|
//!
|
||||||
//! ## Not here (yet)
|
//! ## Not here (yet)
|
||||||
@@ -85,15 +85,15 @@ use std::time::{Duration, Instant};
|
|||||||
/// [`terminate`](Self::terminate) are optional lifecycle hooks with no-op
|
/// [`terminate`](Self::terminate) are optional lifecycle hooks with no-op
|
||||||
/// defaults.
|
/// defaults.
|
||||||
pub trait GenServer: Send + 'static {
|
pub trait GenServer: Send + 'static {
|
||||||
/// Synchronous request type (carried by [`ServerRef::call`]).
|
/// Synchronous request type (carried by [`GenServerRef::call`]).
|
||||||
type Call: Send + 'static;
|
type Call: Send + 'static;
|
||||||
/// Reply type returned for a `Call`.
|
/// Reply type returned for a `Call`.
|
||||||
type Reply: Send + 'static;
|
type Reply: Send + 'static;
|
||||||
/// Asynchronous request type (carried by [`ServerRef::cast`]).
|
/// Asynchronous request type (carried by [`GenServerRef::cast`]).
|
||||||
type Cast: Send + 'static;
|
type Cast: Send + 'static;
|
||||||
/// Out-of-band message type, delivered to [`handle_info`](Self::handle_info)
|
/// Out-of-band message type, delivered to [`handle_info`](Self::handle_info)
|
||||||
/// from the info channels registered at start
|
/// from the info channels registered at start
|
||||||
/// ([`ServerBuilder::with_info`]). Servers with several out-of-band
|
/// ([`GenServerBuilder::with_info`]). Servers with several out-of-band
|
||||||
/// sources enum them up into one `Info`. Use `()` if unused.
|
/// sources enum them up into one `Info`. Use `()` if unused.
|
||||||
type Info: Send + 'static;
|
type Info: Send + 'static;
|
||||||
/// The server's own scheduled-timer payload, delivered to
|
/// The server's own scheduled-timer payload, delivered to
|
||||||
@@ -107,10 +107,10 @@ pub trait GenServer: Send + 'static {
|
|||||||
type Timer: Send + 'static;
|
type Timer: Send + 'static;
|
||||||
|
|
||||||
/// Runs once inside the server actor before any message is handled. The
|
/// Runs once inside the server actor before any message is handled. The
|
||||||
/// [`ServerCtx`] is the loop's one runtime hook: clone its [`Watcher`]
|
/// [`GenServerCtx`] is the loop's one runtime hook: clone its [`Watcher`]
|
||||||
/// into the state here to be able to [`watch`](Watcher::watch) monitors
|
/// into the state here to be able to [`watch`](Watcher::watch) monitors
|
||||||
/// from any later handler.
|
/// from any later handler.
|
||||||
fn init(&mut self, _ctx: &ServerCtx<Self>)
|
fn init(&mut self, _ctx: &GenServerCtx<Self>)
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
@@ -138,7 +138,7 @@ pub trait GenServer: Send + 'static {
|
|||||||
fn handle_timer(&mut self, _msg: Self::Timer) {}
|
fn handle_timer(&mut self, _msg: Self::Timer) {}
|
||||||
|
|
||||||
/// Handle a receive/idle timeout: fired when the loop has waited a full
|
/// Handle a receive/idle timeout: fired when the loop has waited a full
|
||||||
/// idle window (set once via [`ServerCtx::idle_after`]) with no message of
|
/// idle window (set once via [`GenServerCtx::idle_after`]) with no message of
|
||||||
/// any kind dispatched. The window resets on every dispatched message and
|
/// any kind dispatched. The window resets on every dispatched message and
|
||||||
/// re-arms after this fires (a steady idle detector); a server wanting
|
/// re-arms after this fires (a steady idle detector); a server wanting
|
||||||
/// one-shot idle-shutdown simply requests its own stop here. Default: no-op
|
/// one-shot idle-shutdown simply requests its own stop here. Default: no-op
|
||||||
@@ -157,27 +157,27 @@ enum Envelope<G: GenServer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A clonable handle to a running server. Cloning yields another sender to the
|
/// A clonable handle to a running server. Cloning yields another sender to the
|
||||||
/// same inbox; the server lives until the last `ServerRef` is dropped, at which
|
/// same inbox; the server lives until the last `GenServerRef` is dropped, at which
|
||||||
/// point its inbox closes and the loop exits normally.
|
/// point its inbox closes and the loop exits normally.
|
||||||
pub struct ServerRef<G: GenServer> {
|
pub struct GenServerRef<G: GenServer> {
|
||||||
tx: Sender<Envelope<G>>,
|
tx: Sender<Envelope<G>>,
|
||||||
pid: Pid,
|
pid: Pid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> Clone for ServerRef<G> {
|
impl<G: GenServer> Clone for GenServerRef<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
ServerRef { tx: self.tx.clone(), pid: self.pid }
|
GenServerRef { tx: self.tx.clone(), pid: self.pid }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by [`ServerRef::call`] when the server is no longer reachable.
|
/// Returned by [`GenServerRef::call`] when the server is no longer reachable.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum CallError {
|
pub enum CallError {
|
||||||
/// The server was already gone, or died before replying.
|
/// The server was already gone, or died before replying.
|
||||||
ServerDown,
|
ServerDown,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by [`ServerRef::call_timeout`].
|
/// Returned by [`GenServerRef::call_timeout`].
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum CallTimeoutError {
|
pub enum CallTimeoutError {
|
||||||
/// The server was already gone, or died before replying.
|
/// The server was already gone, or died before replying.
|
||||||
@@ -190,14 +190,14 @@ pub enum CallTimeoutError {
|
|||||||
Timeout,
|
Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by [`ServerRef::cast`] when the server is no longer reachable.
|
/// Returned by [`GenServerRef::cast`] when the server is no longer reachable.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum CastError {
|
pub enum CastError {
|
||||||
/// The server inbox was closed (the server is gone).
|
/// The server inbox was closed (the server is gone).
|
||||||
ServerDown,
|
ServerDown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> ServerRef<G> {
|
impl<G: GenServer> GenServerRef<G> {
|
||||||
/// The server actor's pid — usable with `monitor`, `request_stop`, `link`.
|
/// The server actor's pid — usable with `monitor`, `request_stop`, `link`.
|
||||||
pub fn pid(&self) -> Pid {
|
pub fn pid(&self) -> Pid {
|
||||||
self.pid
|
self.pid
|
||||||
@@ -219,7 +219,7 @@ impl<G: GenServer> ServerRef<G> {
|
|||||||
///
|
///
|
||||||
/// This `timeout` is a **client-side call deadline** — how long *this caller*
|
/// This `timeout` is a **client-side call deadline** — how long *this caller*
|
||||||
/// waits for a reply — and is wholly separate from the server-side idle /
|
/// waits for a reply — and is wholly separate from the server-side idle /
|
||||||
/// receive timeout ([`ServerCtx::idle_after`] →
|
/// receive timeout ([`GenServerCtx::idle_after`] →
|
||||||
/// [`GenServer::handle_idle`]), which measures quiet on the *server's* inbox.
|
/// [`GenServer::handle_idle`]), which measures quiet on the *server's* inbox.
|
||||||
/// Same word ("timeout"), two different axes (RFC 015 §7); neither touches
|
/// Same word ("timeout"), two different axes (RFC 015 §7); neither touches
|
||||||
/// the other.
|
/// the other.
|
||||||
@@ -264,8 +264,8 @@ impl<G: GenServer> ServerRef<G> {
|
|||||||
/// stop unwind). Returns immediately if the server was already gone.
|
/// stop unwind). Returns immediately if the server was already gone.
|
||||||
///
|
///
|
||||||
/// This is the explicit teardown for a server pinned alive by a registered
|
/// This is the explicit teardown for a server pinned alive by a registered
|
||||||
/// [`ServerName`] (whose stored sender means dropping every external
|
/// [`GenServerName`] (whose stored sender means dropping every external
|
||||||
/// [`ServerRef`] no longer closes the inbox). Best-effort like all
|
/// [`GenServerRef`] no longer closes the inbox). Best-effort like all
|
||||||
/// cooperative cancellation: a server wedged in a tight loop with no
|
/// cooperative cancellation: a server wedged in a tight loop with no
|
||||||
/// observation point cannot be stopped. Panics if called outside
|
/// observation point cannot be stopped. Panics if called outside
|
||||||
/// `Runtime::run()`.
|
/// `Runtime::run()`.
|
||||||
@@ -302,17 +302,17 @@ enum Sys<G: GenServer> {
|
|||||||
/// loop's two clonable intake handles — the [`Watcher`] (monitors) and the
|
/// loop's two clonable intake handles — the [`Watcher`] (monitors) and the
|
||||||
/// [`TimerHandle`] (timers) — plus the one-shot idle-window setter. Opaque, so
|
/// [`TimerHandle`] (timers) — plus the one-shot idle-window setter. Opaque, so
|
||||||
/// fields can grow without breaking.
|
/// fields can grow without breaking.
|
||||||
pub struct ServerCtx<G: GenServer> {
|
pub struct GenServerCtx<G: GenServer> {
|
||||||
sys_tx: Sender<Sys<G>>,
|
sys_tx: Sender<Sys<G>>,
|
||||||
reg: Arc<Mutex<TimerReg<G>>>,
|
reg: Arc<Mutex<TimerReg<G>>>,
|
||||||
/// The idle/receive-timeout window, set once via [`idle_after`](Self::idle_after)
|
/// The idle/receive-timeout window, set once via [`idle_after`](Self::idle_after)
|
||||||
/// during `init` and read by the loop after `init` returns. Interior-mutable
|
/// during `init` and read by the loop after `init` returns. Interior-mutable
|
||||||
/// because `init` holds only `&ctx`; not `Send`, but `ServerCtx` is only ever
|
/// because `init` holds only `&ctx`; not `Send`, but `GenServerCtx` is only ever
|
||||||
/// borrowed on the actor's own stack during `init`, never sent.
|
/// borrowed on the actor's own stack during `init`, never sent.
|
||||||
idle: Cell<Option<Duration>>,
|
idle: Cell<Option<Duration>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> ServerCtx<G> {
|
impl<G: GenServer> GenServerCtx<G> {
|
||||||
/// A clonable handle to the loop's monitor intake. Store it in the state
|
/// A clonable handle to the loop's monitor intake. Store it in the state
|
||||||
/// during `init` to watch monitors from later handlers.
|
/// during `init` to watch monitors from later handlers.
|
||||||
pub fn watcher(&self) -> Watcher<G> {
|
pub fn watcher(&self) -> Watcher<G> {
|
||||||
@@ -341,7 +341,7 @@ impl<G: GenServer> ServerCtx<G> {
|
|||||||
/// detector); a server wanting one-shot idle-shutdown requests its own stop
|
/// detector); a server wanting one-shot idle-shutdown requests its own stop
|
||||||
/// in `handle_idle`.
|
/// in `handle_idle`.
|
||||||
///
|
///
|
||||||
/// Distinct from [`ServerRef::call_timeout`], which is a *client-side* call
|
/// Distinct from [`GenServerRef::call_timeout`], which is a *client-side* call
|
||||||
/// deadline — same word, different axis (§7).
|
/// deadline — same word, different axis (§7).
|
||||||
pub fn idle_after(&self, after: Duration) {
|
pub fn idle_after(&self, after: Duration) {
|
||||||
self.idle.set(Some(after));
|
self.idle.set(Some(after));
|
||||||
@@ -414,7 +414,7 @@ impl<G: GenServer> TimerReg<G> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Arms, cancels, and (chunk 4) periodically ticks server timers, the time-side
|
/// Arms, cancels, and (chunk 4) periodically ticks server timers, the time-side
|
||||||
/// twin of [`Watcher`] (RFC 015 §4.3). Handed out by [`ServerCtx::timer`] in
|
/// twin of [`Watcher`] (RFC 015 §4.3). Handed out by [`GenServerCtx::timer`] in
|
||||||
/// `init` and stored on the state; later handlers arm timers without any change
|
/// `init` and stored on the state; later handlers arm timers without any change
|
||||||
/// to their signatures. Clonable; the arm stays open while any clone (or an
|
/// to their signatures. Clonable; the arm stays open while any clone (or an
|
||||||
/// in-flight fire) lives.
|
/// in-flight fire) lives.
|
||||||
@@ -527,20 +527,20 @@ impl<G: GenServer> Watcher<G> {
|
|||||||
/// [`start`]: info channels, a supervisor. Consumed by [`start`](Self::start).
|
/// [`start`]: info channels, a supervisor. Consumed by [`start`](Self::start).
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let server = ServerBuilder::new(state)
|
/// let server = GenServerBuilder::new(state)
|
||||||
/// .with_info(events_rx)
|
/// .with_info(events_rx)
|
||||||
/// .under(supervisor_pid)
|
/// .under(supervisor_pid)
|
||||||
/// .start();
|
/// .start();
|
||||||
/// ```
|
/// ```
|
||||||
pub struct ServerBuilder<G: GenServer> {
|
pub struct GenServerBuilder<G: GenServer> {
|
||||||
state: G,
|
state: G,
|
||||||
infos: Vec<Receiver<G::Info>>,
|
infos: Vec<Receiver<G::Info>>,
|
||||||
supervisor: Option<Pid>,
|
supervisor: Option<Pid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> ServerBuilder<G> {
|
impl<G: GenServer> GenServerBuilder<G> {
|
||||||
pub fn new(state: G) -> Self {
|
pub fn new(state: G) -> Self {
|
||||||
ServerBuilder { state, infos: Vec::new(), supervisor: None }
|
GenServerBuilder { state, infos: Vec::new(), supervisor: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an out-of-band channel; messages arriving on it are dispatched to
|
/// Add an out-of-band channel; messages arriving on it are dispatched to
|
||||||
@@ -558,33 +558,33 @@ impl<G: GenServer> ServerBuilder<G> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn the server actor and hand back its [`ServerRef`]. The server's
|
/// Spawn the server actor and hand back its [`GenServerRef`]. The server's
|
||||||
/// lifetime is governed by its refs, not by joining, so the backing join
|
/// lifetime is governed by its refs, not by joining, so the backing join
|
||||||
/// handle is dropped.
|
/// handle is dropped.
|
||||||
pub fn start(self) -> ServerRef<G> {
|
pub fn start(self) -> GenServerRef<G> {
|
||||||
self.spawn_server()
|
self.spawn_server()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bind the server to a durable [`ServerName`] as it starts. Switches to the
|
/// Bind the server to a durable [`GenServerName`] as it starts. Switches to the
|
||||||
/// fallible [`NamedServerBuilder::start`] (the name may already be held by a
|
/// fallible [`NamedGenServerBuilder::start`] (the name may already be held by a
|
||||||
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
||||||
/// configuration through.
|
/// configuration through.
|
||||||
pub fn named(self, name: ServerName<G>) -> NamedServerBuilder<G> {
|
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> {
|
||||||
NamedServerBuilder { builder: self, name: name.as_str() }
|
NamedGenServerBuilder { builder: self, name: name.as_str() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The shared spawn body behind [`start`](Self::start) and
|
/// The shared spawn body behind [`start`](Self::start) and
|
||||||
/// [`NamedServerBuilder::start`]: make the inbox, spawn the loop, return the
|
/// [`NamedGenServerBuilder::start`]: make the inbox, spawn the loop, return the
|
||||||
/// ref. (The named path additionally publishes the inbox sender under the
|
/// ref. (The named path additionally publishes the inbox sender under the
|
||||||
/// name before returning.)
|
/// name before returning.)
|
||||||
fn spawn_server(self) -> ServerRef<G> {
|
fn spawn_server(self) -> GenServerRef<G> {
|
||||||
let (tx, rx) = channel::<Envelope<G>>();
|
let (tx, rx) = channel::<Envelope<G>>();
|
||||||
let ServerBuilder { state, infos, supervisor } = self;
|
let GenServerBuilder { state, infos, supervisor } = self;
|
||||||
let handle = match supervisor {
|
let handle = match supervisor {
|
||||||
Some(sup) => spawn_under(sup, move || server_loop::<G>(rx, state, infos)),
|
Some(sup) => spawn_under(sup, move || server_loop::<G>(rx, state, infos)),
|
||||||
None => spawn(move || server_loop::<G>(rx, state, infos)),
|
None => spawn(move || server_loop::<G>(rx, state, infos)),
|
||||||
};
|
};
|
||||||
ServerRef { tx, pid: handle.pid() }
|
GenServerRef { tx, pid: handle.pid() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,19 +594,19 @@ impl<G: GenServer> ServerBuilder<G> {
|
|||||||
/// `G`'s `Call` / `Cast` / `Reply`. Declared as a constant and shared freely:
|
/// `G`'s `Call` / `Cast` / `Reply`. Declared as a constant and shared freely:
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// const COUNTER: ServerName<Counter> = ServerName::new("counter");
|
/// const COUNTER: GenServerName<Counter> = GenServerName::new("counter");
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Under the hood the server's inbox is published into the registry as a
|
/// Under the hood the server's inbox is published into the registry as a
|
||||||
/// `Sender<Envelope<G>>` keyed by its message `TypeId` — the same channel store
|
/// `Sender<Envelope<G>>` keyed by its message `TypeId` — the same channel store
|
||||||
/// every other name uses — so naming needs no separate directory. `Envelope`
|
/// every other name uses — so naming needs no separate directory. `Envelope`
|
||||||
/// stays private: only `ServerName<G>` opens that door.
|
/// stays private: only `GenServerName<G>` opens that door.
|
||||||
pub struct ServerName<G> {
|
pub struct GenServerName<G> {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
_marker: PhantomData<fn() -> G>,
|
_marker: PhantomData<fn() -> G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G> ServerName<G> {
|
impl<G> GenServerName<G> {
|
||||||
/// Bind a static string as a server name. `const`, so names live as
|
/// Bind a static string as a server name. `const`, so names live as
|
||||||
/// associated constants at call sites.
|
/// associated constants at call sites.
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -621,30 +621,30 @@ impl<G> ServerName<G> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G> Copy for ServerName<G> {}
|
impl<G> Copy for GenServerName<G> {}
|
||||||
impl<G> Clone for ServerName<G> {
|
impl<G> Clone for GenServerName<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [`ServerBuilder`] that will bind a [`ServerName`] as it starts. Reached via
|
/// A [`GenServerBuilder`] that will bind a [`GenServerName`] as it starts. Reached via
|
||||||
/// [`ServerBuilder::named`]; its [`start`](Self::start) is fallible because the
|
/// [`GenServerBuilder::named`]; its [`start`](Self::start) is fallible because the
|
||||||
/// name may already be held by a live server. `with_info` / `under` stay
|
/// name may already be held by a live server. `with_info` / `under` stay
|
||||||
/// available so configuration can come before or after `named`.
|
/// available so configuration can come before or after `named`.
|
||||||
pub struct NamedServerBuilder<G: GenServer> {
|
pub struct NamedGenServerBuilder<G: GenServer> {
|
||||||
builder: ServerBuilder<G>,
|
builder: GenServerBuilder<G>,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> NamedServerBuilder<G> {
|
impl<G: GenServer> NamedGenServerBuilder<G> {
|
||||||
/// Add an out-of-band info channel (see [`ServerBuilder::with_info`]).
|
/// Add an out-of-band info channel (see [`GenServerBuilder::with_info`]).
|
||||||
pub fn with_info(mut self, rx: Receiver<G::Info>) -> Self {
|
pub fn with_info(mut self, rx: Receiver<G::Info>) -> Self {
|
||||||
self.builder = self.builder.with_info(rx);
|
self.builder = self.builder.with_info(rx);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn under an explicit supervisor (see [`ServerBuilder::under`]).
|
/// Spawn under an explicit supervisor (see [`GenServerBuilder::under`]).
|
||||||
pub fn under(mut self, supervisor: Pid) -> Self {
|
pub fn under(mut self, supervisor: Pid) -> Self {
|
||||||
self.builder = self.builder.under(supervisor);
|
self.builder = self.builder.under(supervisor);
|
||||||
self
|
self
|
||||||
@@ -659,8 +659,8 @@ impl<G: GenServer> NamedServerBuilder<G> {
|
|||||||
/// `start()` returns — no race with the server body. On a name clash the
|
/// `start()` returns — no race with the server body. On a name clash the
|
||||||
/// just-spawned server is wound down (its only ref is dropped, closing the
|
/// just-spawned server is wound down (its only ref is dropped, closing the
|
||||||
/// inbox), so a failed bind leaks no actor.
|
/// inbox), so a failed bind leaks no actor.
|
||||||
pub fn start(self) -> Result<ServerRef<G>, RegisterError> {
|
pub fn start(self) -> Result<GenServerRef<G>, RegisterError> {
|
||||||
let NamedServerBuilder { builder, name } = self;
|
let NamedGenServerBuilder { builder, name } = self;
|
||||||
let server = builder.spawn_server();
|
let server = builder.spawn_server();
|
||||||
match register_with::<Envelope<G>>(server.pid, name, server.tx.clone()) {
|
match register_with::<Envelope<G>>(server.pid, name, server.tx.clone()) {
|
||||||
Ok(()) => Ok(server),
|
Ok(()) => Ok(server),
|
||||||
@@ -672,13 +672,13 @@ impl<G: GenServer> NamedServerBuilder<G> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a [`ServerName`] to a [`ServerRef`] when you want a handle to hold or
|
/// Resolve a [`GenServerName`] to a [`GenServerRef`] when you want a handle to hold or
|
||||||
/// pass on rather than resolve per call. Rebuilds the ref from the registry's
|
/// pass on rather than resolve per call. Rebuilds the ref from the registry's
|
||||||
/// stored inbox sender; `None` if no live server holds the name.
|
/// stored inbox sender; `None` if no live server holds the name.
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn whereis_server<G: GenServer>(name: ServerName<G>) -> Option<ServerRef<G>> {
|
pub fn whereis_server<G: GenServer>(name: GenServerName<G>) -> Option<GenServerRef<G>> {
|
||||||
resolve_named_sender::<Envelope<G>>(name.as_str()).map(|(pid, tx)| ServerRef { tx, pid })
|
resolve_named_sender::<Envelope<G>>(name.as_str()).map(|(pid, tx)| GenServerRef { tx, pid })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronous request-reply to the server currently registered under `name`,
|
/// Synchronous request-reply to the server currently registered under `name`,
|
||||||
@@ -687,7 +687,7 @@ pub fn whereis_server<G: GenServer>(name: ServerName<G>) -> Option<ServerRef<G>>
|
|||||||
/// server holds the name, or if it dies before replying.
|
/// server holds the name, or if it dies before replying.
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn call<G: GenServer>(name: ServerName<G>, request: G::Call) -> Result<G::Reply, CallError> {
|
pub fn call<G: GenServer>(name: GenServerName<G>, request: G::Call) -> Result<G::Reply, CallError> {
|
||||||
match whereis_server(name) {
|
match whereis_server(name) {
|
||||||
Some(server) => server.call(request),
|
Some(server) => server.call(request),
|
||||||
None => Err(CallError::ServerDown),
|
None => Err(CallError::ServerDown),
|
||||||
@@ -698,7 +698,7 @@ pub fn call<G: GenServer>(name: ServerName<G>, request: G::Call) -> Result<G::Re
|
|||||||
/// [`CastError::ServerDown`] if no live server holds the name.
|
/// [`CastError::ServerDown`] if no live server holds the name.
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn cast<G: GenServer>(name: ServerName<G>, request: G::Cast) -> Result<(), CastError> {
|
pub fn cast<G: GenServer>(name: GenServerName<G>, request: G::Cast) -> Result<(), CastError> {
|
||||||
match whereis_server(name) {
|
match whereis_server(name) {
|
||||||
Some(server) => server.cast(request),
|
Some(server) => server.cast(request),
|
||||||
None => Err(CastError::ServerDown),
|
None => Err(CastError::ServerDown),
|
||||||
@@ -706,25 +706,25 @@ pub fn cast<G: GenServer>(name: ServerName<G>, request: G::Cast) -> Result<(), C
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Terminate the server registered under `name` and block until it is down (see
|
/// Terminate the server registered under `name` and block until it is down (see
|
||||||
/// [`ServerRef::shutdown`]). A no-op if no live server holds the name.
|
/// [`GenServerRef::shutdown`]). A no-op if no live server holds the name.
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn shutdown<G: GenServer>(name: ServerName<G>) {
|
pub fn shutdown<G: GenServer>(name: GenServerName<G>) {
|
||||||
if let Some(server) = whereis_server(name) {
|
if let Some(server) = whereis_server(name) {
|
||||||
server.shutdown();
|
server.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn `state` as a server under the current actor (via [`spawn`]). Returns a
|
/// Spawn `state` as a server under the current actor (via [`spawn`]). Returns a
|
||||||
/// [`ServerRef`]. Shorthand for `ServerBuilder::new(state).start()`.
|
/// [`GenServerRef`]. Shorthand for `GenServerBuilder::new(state).start()`.
|
||||||
pub fn start<G: GenServer>(state: G) -> ServerRef<G> {
|
pub fn start<G: GenServer>(state: G) -> GenServerRef<G> {
|
||||||
ServerBuilder::new(state).start()
|
GenServerBuilder::new(state).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`start`], but spawns the server under an explicit supervisor pid (via
|
/// Like [`start`], but spawns the server under an explicit supervisor pid (via
|
||||||
/// [`spawn_under`]) so it slots into the supervision tree.
|
/// [`spawn_under`]) so it slots into the supervision tree.
|
||||||
pub fn start_under<G: GenServer>(supervisor: Pid, state: G) -> ServerRef<G> {
|
pub fn start_under<G: GenServer>(supervisor: Pid, state: G) -> GenServerRef<G> {
|
||||||
ServerBuilder::new(state).under(supervisor).start()
|
GenServerBuilder::new(state).under(supervisor).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn server_loop<G: GenServer>(
|
fn server_loop<G: GenServer>(
|
||||||
@@ -788,7 +788,7 @@ fn server_loop<G: GenServer>(
|
|||||||
// Bind the ctx so the idle window set during init can be read back, then
|
// Bind the ctx so the idle window set during init can be read back, then
|
||||||
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
||||||
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
||||||
let ctx = ServerCtx { sys_tx, reg: reg.clone(), idle: Cell::new(None) };
|
let ctx = GenServerCtx { sys_tx, reg: reg.clone(), idle: Cell::new(None) };
|
||||||
guard.0.init(&ctx);
|
guard.0.init(&ctx);
|
||||||
let idle = ctx.idle.get();
|
let idle = ctx.idle.get();
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
|
|||||||
+45
-120
@@ -1,50 +1,45 @@
|
|||||||
//! gen_statem — generic finite state machine behaviour (RFC 017).
|
//! gen_statem — generic finite state machine behaviour.
|
||||||
//!
|
//!
|
||||||
//! The sibling of [`gen_server`](crate::gen_server): where a `gen_server`
|
//! The sibling of [`gen_server`](crate::gen_server): where a `gen_server`
|
||||||
//! carries one undifferentiated blob of state and a single `handle` that
|
//! carries one undifferentiated blob of state and a single `handle` that
|
||||||
//! re-derives "what mode am I in" on every message, a `gen_statem` makes the
|
//! re-derives "what mode am I in" on every message, a `gen_statem` makes the
|
||||||
//! state an explicit **tag**, routes event handling by it, and (in later
|
//! state an explicit **tag** and routes event handling by it.
|
||||||
//! chunks) adds the machinery state machines need — state-entry callbacks, a
|
|
||||||
//! timeout taxonomy, and event postponement.
|
|
||||||
//!
|
//!
|
||||||
//! ## What this layer is
|
//! ## What this layer is
|
||||||
//!
|
//!
|
||||||
//! This module is the **runtime support** a state machine runs on, *not* the
|
//! This module is the **runtime support** a state machine runs on. A machine is
|
||||||
//! authoring surface. A machine is any type implementing [`Machine`]: it owns
|
//! any type implementing [`Machine`]: it owns its state tag and data, and its
|
||||||
//! its state tag and data, and its [`handle`](Machine::handle) reduces a
|
//! [`handle`](Machine::handle) reduces a `(state, event)` pair to a
|
||||||
//! `(state, event)` pair to a [`Resolution`]. The loop here drives it — spawn,
|
//! [`Resolution`]. The loop here drives it — spawn,
|
||||||
//! [`on_start`](Machine::on_start), then one [`handle`](Machine::handle) per
|
//! [`on_start`](Machine::on_start), then one [`handle`](Machine::handle) per
|
||||||
//! inbox event — mirroring `gen_server`'s spawn/teardown idioms.
|
//! inbox event — mirroring `gen_server`'s spawn/teardown idioms.
|
||||||
//!
|
//!
|
||||||
//! The RFC's `statem!` macro (deferred) would *generate* a `Machine` impl from
|
//! The [`gen_statem!`](crate::gen_statem) macro is the authoring surface: it
|
||||||
//! a declarative `transitions { … }` graph plus per-state handler blocks, and
|
//! *generates* a `Machine` impl from per-state handler blocks. Edge-validity is
|
||||||
//! add the expansion-time edge-lint. Until then a machine is hand-written
|
//! not a separate check — it falls out of the generated total `match (state,
|
||||||
//! against these primitives; see `examples/statem_switch.rs` for the shape the
|
//! event)` under denied lints (a forgotten or duplicated pair is a compile
|
||||||
//! macro would target.
|
//! error), so no proc-macro is needed. See `examples/gen_statem_macro.rs` for
|
||||||
|
//! the macro form and `examples/gen_statem_fused.rs` for the hand-written shape
|
||||||
|
//! it expands to.
|
||||||
//!
|
//!
|
||||||
//! ## The unified event
|
//! ## The unified event
|
||||||
//!
|
//!
|
||||||
//! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries.
|
//! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries. By
|
||||||
//! By convention (and in the macro's desugaring) it folds the user's `cast`
|
//! convention (and in the macro's desugaring) it folds the user's `cast` and
|
||||||
//! and `call` enums together with the runtime's own internal events:
|
//! `call` enums together:
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```ignore
|
||||||
//! enum Ev { Cast(MyCast), Call(MyCall) /* later: StateTimeout, Timeout(name) */ }
|
//! enum Ev { Cast(MyCast), Call(MyCall) }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! [`StatemRef::send`] pushes any event (a cast is just a `send`);
|
//! [`GenStatemRef::send`] pushes any event (a cast is just a `send`);
|
||||||
//! [`StatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a
|
//! [`GenStatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a
|
||||||
//! `call` variant, and parks until the machine answers — exactly the
|
//! `call` variant, and parks until the machine answers — exactly the
|
||||||
//! `gen_server` call round-trip, but with the reply handle riding *inside* the
|
//! `gen_server` call round-trip, but with the reply handle riding *inside* the
|
||||||
//! user's own event so a handler can answer (or, later, postpone) it.
|
//! user's own event so a handler can answer it.
|
||||||
//!
|
//!
|
||||||
//! ## Chunk status (RFC 017 §Sequencing)
|
//! [`Resolution::Postpone`] and the timeout arming on [`Cx`] are part of the
|
||||||
//!
|
//! type surface but are not yet wired up.
|
||||||
//! This is **chunk 1**: macro-free dispatch against real time — spawn,
|
|
||||||
//! `on_start`, stay/transition, and the `enter` callback (a method on the
|
|
||||||
//! machine, run on entry to a state). [`Resolution::Postpone`] and the timeout
|
|
||||||
//! arming on [`Cx`] are part of the type surface but are not yet acted on; they
|
|
||||||
//! land in chunks 2–3.
|
|
||||||
|
|
||||||
use crate::channel::{channel, Receiver, Sender};
|
use crate::channel::{channel, Receiver, Sender};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
@@ -85,7 +80,7 @@ pub trait Machine: Send + 'static {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// The outcome of handling one event, before the loop/handler acts on it:
|
/// The outcome of handling one event, before the loop/handler acts on it:
|
||||||
/// dispatch reduces to `(state, event) -> Resolution<State>` (RFC §Semantics).
|
/// dispatch reduces to `(state, event) -> Resolution<State>`.
|
||||||
///
|
///
|
||||||
/// [`From<S>`](From) is why a bare state tag works as an arm tail and why
|
/// [`From<S>`](From) is why a bare state tag works as an arm tail and why
|
||||||
/// "stay" needs no keyword — `Tag.into()` is `To(Tag)`, and the handler treats
|
/// "stay" needs no keyword — `Tag.into()` is `To(Tag)`, and the handler treats
|
||||||
@@ -95,7 +90,7 @@ pub enum Resolution<S> {
|
|||||||
/// run `enter`); a **stay** when `s == current` (no `enter`).
|
/// run `enter`); a **stay** when `s == current` (no `enter`).
|
||||||
To(S),
|
To(S),
|
||||||
/// Defer the current event onto the postpone queue, to be replayed after
|
/// Defer the current event onto the postpone queue, to be replayed after
|
||||||
/// the next real transition. Produced by `cx.postpone()` (chunk 3); present
|
/// the next real transition. Produced by `cx.postpone()`; present
|
||||||
/// here for forward-compatibility but not yet generated.
|
/// here for forward-compatibility but not yet generated.
|
||||||
Postpone,
|
Postpone,
|
||||||
/// No arm matched `(state, event)`: log-and-drop via
|
/// No arm matched `(state, event)`: log-and-drop via
|
||||||
@@ -116,11 +111,11 @@ impl<S> From<S> for Resolution<S> {
|
|||||||
|
|
||||||
/// The context handle injected into [`Machine::on_start`] and
|
/// The context handle injected into [`Machine::on_start`] and
|
||||||
/// [`Machine::handle`]. Non-state outcomes (postpone, timeout arming) live here
|
/// [`Machine::handle`]. Non-state outcomes (postpone, timeout arming) live here
|
||||||
/// as method calls rather than keywords (RFC §"Non-state outcomes live on cx").
|
/// as method calls rather than keywords.
|
||||||
///
|
///
|
||||||
/// In chunk 1 it carries only the [`on_unhandled`](Self::on_unhandled) hook;
|
/// For now it carries only the [`on_unhandled`](Self::on_unhandled) hook;
|
||||||
/// `cx.state_timeout(d)` / `cx.timeout(name, d)` (chunk 2) and `cx.postpone()`
|
/// `cx.state_timeout(d)` / `cx.timeout(name, d)` and `cx.postpone()` will attach
|
||||||
/// (chunk 3) attach here as those chunks land. It lives only on the actor's own
|
/// here when implemented. It lives only on the actor's own
|
||||||
/// stack and is never sent.
|
/// stack and is never sent.
|
||||||
pub struct Cx<Ev> {
|
pub struct Cx<Ev> {
|
||||||
_ev: PhantomData<fn() -> Ev>,
|
_ev: PhantomData<fn() -> Ev>,
|
||||||
@@ -132,7 +127,7 @@ impl<Ev> Cx<Ev> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The default for an event no arm matched: **log-and-drop**. Overridable
|
/// The default for an event no arm matched: **log-and-drop**. Overridable
|
||||||
/// hook wiring is a follow-on (RFC Open Q5); for now an unmatched event is
|
/// hook wiring is a follow-on; for now an unmatched event is
|
||||||
/// silently dropped, as `gen_server` does with unexpected messages.
|
/// silently dropped, as `gen_server` does with unexpected messages.
|
||||||
pub fn on_unhandled(&mut self) {}
|
pub fn on_unhandled(&mut self) {}
|
||||||
}
|
}
|
||||||
@@ -143,14 +138,14 @@ impl<Ev> Cx<Ev> {
|
|||||||
|
|
||||||
/// The reply side of a synchronous `call`, carried *inside* the machine's own
|
/// The reply side of a synchronous `call`, carried *inside* the machine's own
|
||||||
/// `call` event variant (`GetCount(Reply<u32>)`). Move-only: answering consumes
|
/// `call` event variant (`GetCount(Reply<u32>)`). Move-only: answering consumes
|
||||||
/// it, so a handler replies at most once. Built by [`StatemRef::call`]; the
|
/// it, so a handler replies at most once. Built by [`GenStatemRef::call`]; the
|
||||||
/// caller parks on the matching receiver until `reply` is invoked (or the
|
/// caller parks on the matching receiver until `reply` is invoked (or the
|
||||||
/// machine dies, closing the channel).
|
/// machine dies, closing the channel).
|
||||||
///
|
///
|
||||||
/// Carrying the handle in the event — rather than the loop owning a reply slot
|
/// Carrying the handle in the event — rather than the loop owning a reply slot
|
||||||
/// — is what lets a later chunk **postpone a call**: the whole event, reply
|
/// — is what lets a later chunk **postpone a call**: the whole event, reply
|
||||||
/// handle included, moves onto the postpone queue and is answered by a later
|
/// handle included, moves onto the postpone queue and is answered by a later
|
||||||
/// state (RFC §Postpone).
|
/// state.
|
||||||
pub struct Reply<T> {
|
pub struct Reply<T> {
|
||||||
tx: Sender<T>,
|
tx: Sender<T>,
|
||||||
}
|
}
|
||||||
@@ -168,14 +163,14 @@ impl<T> Reply<T> {
|
|||||||
// Client handle
|
// Client handle
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Returned by [`StatemRef::call`] when the machine is unreachable.
|
/// Returned by [`GenStatemRef::call`] when the machine is unreachable.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum CallError {
|
pub enum CallError {
|
||||||
/// The machine was already gone, or died before replying.
|
/// The machine was already gone, or died before replying.
|
||||||
Down,
|
Down,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned by [`StatemRef::send`] when the machine's inbox is closed.
|
/// Returned by [`GenStatemRef::send`] when the machine's inbox is closed.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum SendError {
|
pub enum SendError {
|
||||||
/// The machine is gone (its inbox is closed).
|
/// The machine is gone (its inbox is closed).
|
||||||
@@ -183,20 +178,20 @@ pub enum SendError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A clonable handle to a running machine. Cloning yields another sender to the
|
/// A clonable handle to a running machine. Cloning yields another sender to the
|
||||||
/// same inbox; the machine lives until the last `StatemRef` is dropped, at which
|
/// same inbox; the machine lives until the last `GenStatemRef` is dropped, at which
|
||||||
/// point its inbox closes and the loop exits.
|
/// point its inbox closes and the loop exits.
|
||||||
pub struct StatemRef<M: Machine> {
|
pub struct GenStatemRef<M: Machine> {
|
||||||
tx: Sender<M::Ev>,
|
tx: Sender<M::Ev>,
|
||||||
pid: Pid,
|
pid: Pid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Machine> Clone for StatemRef<M> {
|
impl<M: Machine> Clone for GenStatemRef<M> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
StatemRef { tx: self.tx.clone(), pid: self.pid }
|
GenStatemRef { tx: self.tx.clone(), pid: self.pid }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Machine> StatemRef<M> {
|
impl<M: Machine> GenStatemRef<M> {
|
||||||
/// The machine actor's pid — usable with `monitor`, `request_stop`, `link`.
|
/// The machine actor's pid — usable with `monitor`, `request_stop`, `link`.
|
||||||
pub fn pid(&self) -> Pid {
|
pub fn pid(&self) -> Pid {
|
||||||
self.pid
|
self.pid
|
||||||
@@ -235,15 +230,15 @@ impl<M: Machine> StatemRef<M> {
|
|||||||
// Spawn + loop
|
// Spawn + loop
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Spawn `machine` as an actor and hand back its [`StatemRef`]. Shape mirrors
|
/// Spawn `machine` as an actor and hand back its [`GenStatemRef`]. Shape mirrors
|
||||||
/// `gen_server::start`: make the inbox, spawn the loop, return the ref; the
|
/// `gen_server::start`: make the inbox, spawn the loop, return the ref; the
|
||||||
/// backing join handle is dropped (lifetime is governed by refs, not joining).
|
/// backing join handle is dropped (lifetime is governed by refs, not joining).
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn spawn<M: Machine>(machine: M) -> StatemRef<M> {
|
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||||||
let (tx, rx) = channel::<M::Ev>();
|
let (tx, rx) = channel::<M::Ev>();
|
||||||
let handle = spawn_actor(move || statem_loop(rx, machine));
|
let handle = spawn_actor(move || statem_loop(rx, machine));
|
||||||
StatemRef { tx, pid: handle.pid() }
|
GenStatemRef { tx, pid: handle.pid() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The machine actor body: `on_start`, then one `handle` per inbox event until
|
/// The machine actor body: `on_start`, then one `handle` per inbox event until
|
||||||
@@ -266,7 +261,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// gen_statem! — the authoring macro (RFC 017 §Surface, fused variant)
|
// gen_statem! — the authoring macro (fused total-match variant)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Assemble a complete [`Machine`] from hand-written types, a glanceable
|
/// Assemble a complete [`Machine`] from hand-written types, a glanceable
|
||||||
@@ -393,7 +388,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
/// # What it emits
|
/// # What it emits
|
||||||
///
|
///
|
||||||
/// `enum $Ev { Cast($Cast), Call($Call) }`, `struct $Sm { state, data }`,
|
/// `enum $Ev { Cast($Cast), Call($Call) }`, `struct $Sm { state, data }`,
|
||||||
/// `$Sm::start(init, data) -> StatemRef<$Sm>`, the `Machine` impl (`on_start`
|
/// `$Sm::start(init, data) -> GenStatemRef<$Sm>`, the `Machine` impl (`on_start`
|
||||||
/// running the initial `enter`; `handle` = the dispatch match + the
|
/// running the initial `enter`; `handle` = the dispatch match + the
|
||||||
/// stay/transition/unhandled apply-tail, the cell's sole writer), and the
|
/// stay/transition/unhandled apply-tail, the cell's sole writer), and the
|
||||||
/// `enter` dispatch. Chunk 1: real time, no timers, no postpone.
|
/// `enter` dispatch. Chunk 1: real time, no timers, no postpone.
|
||||||
@@ -426,7 +421,7 @@ macro_rules! gen_statem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $sm {
|
impl $sm {
|
||||||
fn start(init: $State, data: $Data) -> $crate::gen_statem::StatemRef<$sm> {
|
fn start(init: $State, data: $Data) -> $crate::gen_statem::GenStatemRef<$sm> {
|
||||||
$crate::gen_statem::spawn($sm { state: init, data })
|
$crate::gen_statem::spawn($sm { state: init, data })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +461,7 @@ macro_rules! gen_statem {
|
|||||||
self.enter(s, $cx);
|
self.enter(s, $cx);
|
||||||
}
|
}
|
||||||
$crate::gen_statem::Resolution::Postpone => {
|
$crate::gen_statem::Resolution::Postpone => {
|
||||||
unreachable!("postpone is unreachable until chunk 3")
|
unreachable!("postpone is not generated yet")
|
||||||
}
|
}
|
||||||
$crate::gen_statem::Resolution::Unhandled => $cx.on_unhandled(),
|
$crate::gen_statem::Resolution::Unhandled => $cx.on_unhandled(),
|
||||||
}
|
}
|
||||||
@@ -527,73 +522,3 @@ macro_rules! gen_statem {
|
|||||||
$crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] $($more)*)
|
$crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] $($more)*)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod gen_statem_tests {
|
|
||||||
use crate::gen_statem::Reply;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
enum Switch {
|
|
||||||
Off,
|
|
||||||
On,
|
|
||||||
}
|
|
||||||
struct Counts {
|
|
||||||
flips: u32,
|
|
||||||
enters: u32,
|
|
||||||
}
|
|
||||||
enum Cast {
|
|
||||||
Flip,
|
|
||||||
Nudge,
|
|
||||||
}
|
|
||||||
enum Call {
|
|
||||||
GetFlips(Reply<u32>),
|
|
||||||
GetEnters(Reply<u32>),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_flip(data: &mut Counts) {
|
|
||||||
data.flips += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::gen_statem! {
|
|
||||||
machine: SwitchSm { state: Switch, data: Counts };
|
|
||||||
event: Ev { cast: Cast, call: Call };
|
|
||||||
context(data, prev, cx);
|
|
||||||
|
|
||||||
enter {
|
|
||||||
_ => data.enters += 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
on Switch::Off => {
|
|
||||||
cast Cast::Flip => { on_flip(data); Switch::On },
|
|
||||||
cast Cast::Nudge => unhandled,
|
|
||||||
}
|
|
||||||
on Switch::On => {
|
|
||||||
cast Cast::Flip => { on_flip(data); Switch::Off },
|
|
||||||
cast Cast::Nudge => prev, // explicit stay
|
|
||||||
}
|
|
||||||
on _ => {
|
|
||||||
call Call::GetFlips(r) => { r.reply(data.flips); prev },
|
|
||||||
call Call::GetEnters(r) => { r.reply(data.enters); prev },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE (guarantee #2): because this machine lives in the SAME crate as
|
|
||||||
// `gen_statem!`, adding a duplicate row here — e.g. a second
|
|
||||||
// `cast Cast::Nudge => unhandled,` under `on Switch::Off` — is a hard
|
|
||||||
// `unreachable pattern` error. (From a downstream crate it is silently
|
|
||||||
// suppressed by rustc's in_external_macro rule; see the macro docs.)
|
|
||||||
#[test]
|
|
||||||
fn macro_machine_drives_and_counts() {
|
|
||||||
crate::run(|| {
|
|
||||||
let sm = SwitchSm::start(Switch::Off, Counts { flips: 0, enters: 0 });
|
|
||||||
sm.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On (flip=1, enter)
|
|
||||||
sm.send(Ev::Cast(Cast::Nudge)).unwrap(); // On: stay (no enter)
|
|
||||||
sm.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off (flip=2, enter)
|
|
||||||
|
|
||||||
let flips = sm.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
|
|
||||||
let enters = sm.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
|
||||||
assert_eq!(flips, 2);
|
|
||||||
assert_eq!(enters, 3); // Off(start) + On + Off
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-3
@@ -56,11 +56,11 @@ pub use channel::{
|
|||||||
};
|
};
|
||||||
pub use gen_server::{
|
pub use gen_server::{
|
||||||
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
||||||
NamedServerBuilder, ServerBuilder, ServerCtx, ServerName, ServerRef, TimerHandle, Watcher,
|
NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher,
|
||||||
};
|
};
|
||||||
pub use gen_statem::{
|
pub use gen_statem::{
|
||||||
CallError as StatemCallError, Cx, Machine, Reply, Resolution, SendError as StatemSendError,
|
CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError,
|
||||||
StatemRef,
|
GenStatemRef,
|
||||||
};
|
};
|
||||||
pub use introspect::{
|
pub use introspect::{
|
||||||
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
||||||
|
|||||||
+5
-5
@@ -29,7 +29,7 @@
|
|||||||
//! channel by value; that is intended, it is exactly what a remote observer
|
//! channel by value; that is intended, it is exactly what a remote observer
|
||||||
//! (RFC 011) will serialize across a node boundary.
|
//! (RFC 011) will serialize across a node boundary.
|
||||||
|
|
||||||
use crate::gen_server::{GenServer, ServerBuilder, ServerRef};
|
use crate::gen_server::{GenServer, GenServerBuilder, GenServerRef};
|
||||||
use crate::introspect::{actor_info, snapshot, tree};
|
use crate::introspect::{actor_info, snapshot, tree};
|
||||||
use crate::introspect::{ActorInfo, RuntimeSnapshot, RuntimeTree};
|
use crate::introspect::{ActorInfo, RuntimeSnapshot, RuntimeTree};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
@@ -89,8 +89,8 @@ impl GenServer for Observer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn the observer under the current actor and hand back its [`ServerRef`].
|
/// Spawn the observer under the current actor and hand back its [`GenServerRef`].
|
||||||
/// Shorthand for `ServerBuilder::new(Observer).start()`; use the builder
|
/// Shorthand for `GenServerBuilder::new(Observer).start()`; use the builder
|
||||||
/// directly (e.g. `.under(sup)`) to slot it into a supervision tree.
|
/// directly (e.g. `.under(sup)`) to slot it into a supervision tree.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -109,6 +109,6 @@ impl GenServer for Observer {
|
|||||||
/// assert!(snap.actors.iter().any(|a| a.pid == obs.pid()));
|
/// assert!(snap.actors.iter().any(|a| a.pid == obs.pid()));
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn start() -> ServerRef<Observer> {
|
pub fn start() -> GenServerRef<Observer> {
|
||||||
ServerBuilder::new(Observer).start()
|
GenServerBuilder::new(Observer).start()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -53,7 +53,7 @@ impl std::fmt::Display for RawPid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Phantom actor type for a pid that has no single message type: raw `spawn`
|
/// Phantom actor type for a pid that has no single message type: raw `spawn`
|
||||||
/// actors, gen_servers (intrinsically multi-message, addressed via `ServerRef`),
|
/// actors, gen_servers (intrinsically multi-message, addressed via `GenServerRef`),
|
||||||
/// and every identity-only context. Deliberately **not** [`Addressable`], so a
|
/// and every identity-only context. Deliberately **not** [`Addressable`], so a
|
||||||
/// typed `send` to a `Pid<Erased>` does not compile; the runtime-checked
|
/// typed `send` to a `Pid<Erased>` does not compile; the runtime-checked
|
||||||
/// `send_dyn` escape hatch (RFC 014 §4.6) is the sanctioned bare-pid path.
|
/// `send_dyn` escape hatch (RFC 014 §4.6) is the sanctioned bare-pid path.
|
||||||
@@ -166,7 +166,7 @@ impl<A> std::fmt::Display for Pid<A> {
|
|||||||
/// An actor type with a single associated message type, so a [`Pid<Self>`] is a
|
/// An actor type with a single associated message type, so a [`Pid<Self>`] is a
|
||||||
/// typed address. The raw channel layer has no such trait (actors are closures
|
/// typed address. The raw channel layer has no such trait (actors are closures
|
||||||
/// over channels) and `GenServer` is intrinsically multi-message (addressed via
|
/// over channels) and `GenServer` is intrinsically multi-message (addressed via
|
||||||
/// its own `ServerRef`); this is the minimal hook that lets the single-message
|
/// its own `GenServerRef`); this is the minimal hook that lets the single-message
|
||||||
/// actors carry their message type in their pid. (RFC 014 §4.2.)
|
/// actors carry their message type in their pid. (RFC 014 §4.2.)
|
||||||
pub trait Addressable: 'static {
|
pub trait Addressable: 'static {
|
||||||
/// The message this actor receives. A `Pid<Self>` delivers `Self::Msg`.
|
/// The message this actor receives. A `Pid<Self>` delivers `Self::Msg`.
|
||||||
|
|||||||
+1
-1
@@ -425,7 +425,7 @@ pub fn lookup_as<A: Addressable>(name: &str) -> Option<Pid<A>> {
|
|||||||
/// lock (clone-under-lock, then release). The crate-internal building block for
|
/// lock (clone-under-lock, then release). The crate-internal building block for
|
||||||
/// `gen_server`'s by-name addressing: a named server publishes its inbox as a
|
/// `gen_server`'s by-name addressing: a named server publishes its inbox as a
|
||||||
/// `Sender<Envelope<G>>` (via [`register_with`]), and `whereis_server` / `call`
|
/// `Sender<Envelope<G>>` (via [`register_with`]), and `whereis_server` / `call`
|
||||||
/// / `cast` recover that exact typed sender here to rebuild a `ServerRef<G>`.
|
/// / `cast` recover that exact typed sender here to rebuild a `GenServerRef<G>`.
|
||||||
/// `None` if unbound, dead (pruned on the way out), or holding no `M` channel.
|
/// `None` if unbound, dead (pruned on the way out), or holding no `M` channel.
|
||||||
pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid, Sender<M>)> {
|
pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid, Sender<M>)> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
|
|||||||
+1
-1
@@ -206,7 +206,7 @@ pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) ->
|
|||||||
/// [`send_to`](crate::send_to) always resolves, never racing the body's first
|
/// [`send_to`](crate::send_to) always resolves, never racing the body's first
|
||||||
/// instruction. The actor is detached — its lifetime is governed by its own
|
/// instruction. The actor is detached — its lifetime is governed by its own
|
||||||
/// logic (an explicit stop message, or returning), like
|
/// logic (an explicit stop message, or returning), like
|
||||||
/// [`ServerBuilder::start`](crate::ServerBuilder::start) — so the backing join
|
/// [`GenServerBuilder::start`](crate::GenServerBuilder::start) — so the backing join
|
||||||
/// handle is dropped. Spawns under the current actor (via [`spawn`]).
|
/// handle is dropped. Spawns under the current actor (via [`spawn`]).
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
|
|||||||
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
//! gen_server tests: call round-trip, cast, lifecycle callbacks, and the two
|
//! gen_server tests: call round-trip, cast, lifecycle callbacks, and the two
|
||||||
//! server-down detection paths (reply-channel close vs. inbox-send failure).
|
//! server-down detection paths (reply-channel close vs. inbox-send failure).
|
||||||
|
|
||||||
use smarm::gen_server::{start, CallError, GenServer, ServerBuilder};
|
use smarm::gen_server::{start, CallError, GenServer, GenServerBuilder};
|
||||||
use smarm::run;
|
use smarm::run;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ impl GenServer for Lifecycle {
|
|||||||
type Info = ();
|
type Info = ();
|
||||||
type Timer = ();
|
type Timer = ();
|
||||||
|
|
||||||
fn init(&mut self, _ctx: &smarm::gen_server::ServerCtx<Self>) {
|
fn init(&mut self, _ctx: &smarm::gen_server::GenServerCtx<Self>) {
|
||||||
self.log.lock().unwrap().push("init");
|
self.log.lock().unwrap().push("init");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ fn info_is_dispatched() {
|
|||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let (info_tx, info_rx) = smarm::channel::<&'static str>();
|
let (info_tx, info_rx) = smarm::channel::<&'static str>();
|
||||||
let server = ServerBuilder::new(Logger { log: Vec::new() })
|
let server = GenServerBuilder::new(Logger { log: Vec::new() })
|
||||||
.with_info(info_rx)
|
.with_info(info_rx)
|
||||||
.start();
|
.start();
|
||||||
info_tx.send("info").unwrap();
|
info_tx.send("info").unwrap();
|
||||||
@@ -293,7 +293,7 @@ fn info_outranks_inbox() {
|
|||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let (info_tx, info_rx) = smarm::channel::<&'static str>();
|
let (info_tx, info_rx) = smarm::channel::<&'static str>();
|
||||||
let server = ServerBuilder::new(Logger { log: Vec::new() })
|
let server = GenServerBuilder::new(Logger { log: Vec::new() })
|
||||||
.with_info(info_rx)
|
.with_info(info_rx)
|
||||||
.start();
|
.start();
|
||||||
// The server actor hasn't run yet: both messages are queued before
|
// The server actor hasn't run yet: both messages are queued before
|
||||||
@@ -314,7 +314,7 @@ fn info_arms_keep_declaration_priority() {
|
|||||||
run(move || {
|
run(move || {
|
||||||
let (hi_tx, hi_rx) = smarm::channel::<&'static str>();
|
let (hi_tx, hi_rx) = smarm::channel::<&'static str>();
|
||||||
let (lo_tx, lo_rx) = smarm::channel::<&'static str>();
|
let (lo_tx, lo_rx) = smarm::channel::<&'static str>();
|
||||||
let server = ServerBuilder::new(Logger { log: Vec::new() })
|
let server = GenServerBuilder::new(Logger { log: Vec::new() })
|
||||||
.with_info(hi_rx)
|
.with_info(hi_rx)
|
||||||
.with_info(lo_rx)
|
.with_info(lo_rx)
|
||||||
.start();
|
.start();
|
||||||
@@ -335,7 +335,7 @@ fn closed_info_arm_is_dropped_silently() {
|
|||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let (info_tx, info_rx) = smarm::channel::<&'static str>();
|
let (info_tx, info_rx) = smarm::channel::<&'static str>();
|
||||||
let server = ServerBuilder::new(Logger { log: Vec::new() })
|
let server = GenServerBuilder::new(Logger { log: Vec::new() })
|
||||||
.with_info(info_rx)
|
.with_info(info_rx)
|
||||||
.start();
|
.start();
|
||||||
drop(info_tx); // closed before the server's first select
|
drop(info_tx); // closed before the server's first select
|
||||||
@@ -371,7 +371,7 @@ impl GenServer for Pool {
|
|||||||
type Info = ();
|
type Info = ();
|
||||||
type Timer = ();
|
type Timer = ();
|
||||||
|
|
||||||
fn init(&mut self, ctx: &smarm::gen_server::ServerCtx<Self>) {
|
fn init(&mut self, ctx: &smarm::gen_server::GenServerCtx<Self>) {
|
||||||
self.watcher = Some(ctx.watcher());
|
self.watcher = Some(ctx.watcher());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +448,7 @@ fn unused_ctx_closes_control_arm_silently() {
|
|||||||
// RFC 015 — gen_server timers. A server that arms one-shot timers from a cast
|
// RFC 015 — gen_server timers. A server that arms one-shot timers from a cast
|
||||||
// and records each fire's payload, plus the cancel race signal.
|
// and records each fire's payload, plus the cancel race signal.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
use smarm::gen_server::{ServerCtx, TimerHandle};
|
use smarm::gen_server::{GenServerCtx, TimerHandle};
|
||||||
use smarm::TimerId;
|
use smarm::TimerId;
|
||||||
|
|
||||||
enum TkCast {
|
enum TkCast {
|
||||||
@@ -471,7 +471,7 @@ impl GenServer for Timed {
|
|||||||
type Info = ();
|
type Info = ();
|
||||||
type Timer = u32;
|
type Timer = u32;
|
||||||
|
|
||||||
fn init(&mut self, ctx: &ServerCtx<Self>) {
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
self.timer = Some(ctx.timer());
|
self.timer = Some(ctx.timer());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +597,7 @@ impl GenServer for Idler {
|
|||||||
type Info = ();
|
type Info = ();
|
||||||
type Timer = ();
|
type Timer = ();
|
||||||
|
|
||||||
fn init(&mut self, ctx: &ServerCtx<Self>) {
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
ctx.idle_after(self.window);
|
ctx.idle_after(self.window);
|
||||||
}
|
}
|
||||||
fn handle_call(&mut self, _: ()) -> u32 {
|
fn handle_call(&mut self, _: ()) -> u32 {
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
//! gen_statem behaviour tests, driven through the `gen_statem!` macro: cast/call
|
||||||
|
//! round-trip, `enter` firing on start and on every real transition (but not on
|
||||||
|
//! a stay), and the machine-down path when a handler panics.
|
||||||
|
|
||||||
|
use smarm::gen_statem;
|
||||||
|
use smarm::gen_statem::{CallError, Reply};
|
||||||
|
use smarm::run;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
// A two-state machine: Flip toggles, calls read counters, Boom panics.
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum Switch {
|
||||||
|
Off,
|
||||||
|
On,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Counts {
|
||||||
|
flips: u32,
|
||||||
|
enters: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Cast {
|
||||||
|
Flip,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Call {
|
||||||
|
GetFlips(Reply<u32>),
|
||||||
|
GetEnters(Reply<u32>),
|
||||||
|
Boom(Reply<u32>),
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_statem! {
|
||||||
|
machine: Sm { state: Switch, data: Counts };
|
||||||
|
event: Ev { cast: Cast, call: Call };
|
||||||
|
context(data, prev, cx);
|
||||||
|
|
||||||
|
enter {
|
||||||
|
_ => data.enters += 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
on Switch::Off => {
|
||||||
|
cast Cast::Flip => { data.flips += 1; Switch::On },
|
||||||
|
}
|
||||||
|
on Switch::On => {
|
||||||
|
cast Cast::Flip => Switch::Off,
|
||||||
|
}
|
||||||
|
|
||||||
|
// State-independent queries: reply, then stay via `prev`. Boom panics
|
||||||
|
// (`boom()` is typed as a state tag so the arm stays well-formed).
|
||||||
|
on _ => {
|
||||||
|
call Call::GetFlips(r) => { r.reply(data.flips); prev },
|
||||||
|
call Call::GetEnters(r) => { r.reply(data.enters); prev },
|
||||||
|
call Call::Boom(_r) => boom(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boom() -> Switch {
|
||||||
|
panic!("boom")
|
||||||
|
}
|
||||||
|
// Casts are applied in order and a later call observes the accumulated data.
|
||||||
|
#[test]
|
||||||
|
fn cast_then_call_roundtrip() {
|
||||||
|
let got = Arc::new(Mutex::new(0u32));
|
||||||
|
let got2 = got.clone();
|
||||||
|
run(move || {
|
||||||
|
let sw = Sm::start(Switch::Off, Counts { flips: 0, enters: 0 });
|
||||||
|
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On (flips = 1)
|
||||||
|
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off (no flip count)
|
||||||
|
let flips = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
|
||||||
|
*got2.lock().unwrap() = flips;
|
||||||
|
});
|
||||||
|
assert_eq!(*got.lock().unwrap(), 1, "turned On once across the two flips");
|
||||||
|
}
|
||||||
|
|
||||||
|
// `enter` fires once on start and once per *real* transition; a stay (a call
|
||||||
|
// that returns the current tag) does not re-enter.
|
||||||
|
#[test]
|
||||||
|
fn enter_on_start_and_each_transition_but_not_stay() {
|
||||||
|
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
||||||
|
let got2 = got.clone();
|
||||||
|
run(move || {
|
||||||
|
let sw = Sm::start(Switch::Off, Counts { flips: 0, enters: 0 }); // enter -> 1
|
||||||
|
let after_start = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
||||||
|
// Two stays (the reads) must not bump enters.
|
||||||
|
let _ = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
|
||||||
|
let still = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
||||||
|
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On -> enter -> 2
|
||||||
|
let after_flip = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
||||||
|
*got2.lock().unwrap() = (after_start, still, after_flip);
|
||||||
|
});
|
||||||
|
assert_eq!(*got.lock().unwrap(), (1, 1, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handler that panics tears the loop down; the in-flight call's reply channel
|
||||||
|
// closes as the stack unwinds, so the parked caller wakes with Down (mirrors
|
||||||
|
// gen_server's panicking-handler path).
|
||||||
|
#[test]
|
||||||
|
fn call_to_panicking_handler_is_down() {
|
||||||
|
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
||||||
|
let got2 = got.clone();
|
||||||
|
run(move || {
|
||||||
|
let sw = Sm::start(Switch::Off, Counts { flips: 0, enters: 0 });
|
||||||
|
let r = sw.call(|rep| Ev::Call(Call::Boom(rep)));
|
||||||
|
*got2.lock().unwrap() = Some(r);
|
||||||
|
});
|
||||||
|
assert_eq!(*got.lock().unwrap(), Some(Err(CallError::Down)));
|
||||||
|
}
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
//! gen_statem (RFC 017) chunk-1 tests: call/cast round-trip against a
|
|
||||||
//! hand-written machine, `enter` firing on start and on every real transition
|
|
||||||
//! (but not on a stay), and the machine-down path when a handler panics.
|
|
||||||
//!
|
|
||||||
//! These drive the `smarm::gen_statem` primitives directly, the same way a
|
|
||||||
//! `statem!`-generated machine eventually will.
|
|
||||||
|
|
||||||
use smarm::run;
|
|
||||||
use smarm::gen_statem::{self, CallError, Cx, Machine, Reply, Resolution, StatemRef};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// A hand-written two-state machine: Flip toggles, calls read, Boom panics.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
enum Switch {
|
|
||||||
Off,
|
|
||||||
On,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Counts {
|
|
||||||
flips: u32,
|
|
||||||
enters: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Cast {
|
|
||||||
Flip,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Call {
|
|
||||||
GetFlips(Reply<u32>),
|
|
||||||
GetEnters(Reply<u32>),
|
|
||||||
Boom(Reply<u32>),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Ev {
|
|
||||||
Cast(Cast),
|
|
||||||
Call(Call),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Sm {
|
|
||||||
state: Switch,
|
|
||||||
data: Counts,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sm {
|
|
||||||
fn start(init: Switch) -> StatemRef<Sm> {
|
|
||||||
gen_statem::spawn(Sm { state: init, data: Counts { flips: 0, enters: 0 } })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enter(&mut self, _s: Switch, _cx: &mut Cx<Ev>) {
|
|
||||||
self.data.enters += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Machine for Sm {
|
|
||||||
type Ev = Ev;
|
|
||||||
|
|
||||||
fn on_start(&mut self, cx: &mut Cx<Ev>) {
|
|
||||||
let s = self.state;
|
|
||||||
self.enter(s, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle(&mut self, ev: Ev, cx: &mut Cx<Ev>) {
|
|
||||||
let prev = self.state;
|
|
||||||
let next: Resolution<Switch> = match (self.state, ev) {
|
|
||||||
(Switch::Off, Ev::Cast(Cast::Flip)) => {
|
|
||||||
self.data.flips += 1;
|
|
||||||
Switch::On.into()
|
|
||||||
}
|
|
||||||
(Switch::On, Ev::Cast(Cast::Flip)) => Switch::Off.into(),
|
|
||||||
(s, Ev::Call(Call::GetFlips(r))) => {
|
|
||||||
r.reply(self.data.flips);
|
|
||||||
s.into() // stay
|
|
||||||
}
|
|
||||||
(s, Ev::Call(Call::GetEnters(r))) => {
|
|
||||||
r.reply(self.data.enters);
|
|
||||||
s.into() // stay
|
|
||||||
}
|
|
||||||
(_, Ev::Call(Call::Boom(_r))) => panic!("boom"),
|
|
||||||
};
|
|
||||||
match next {
|
|
||||||
Resolution::To(s) if s == prev => {}
|
|
||||||
Resolution::To(s) => {
|
|
||||||
self.state = s;
|
|
||||||
self.enter(s, cx);
|
|
||||||
}
|
|
||||||
Resolution::Postpone => {}
|
|
||||||
Resolution::Unhandled => cx.on_unhandled(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Casts are applied in order and a later call observes the accumulated data.
|
|
||||||
#[test]
|
|
||||||
fn cast_then_call_roundtrip() {
|
|
||||||
let got = Arc::new(Mutex::new(0u32));
|
|
||||||
let got2 = got.clone();
|
|
||||||
run(move || {
|
|
||||||
let sw = Sm::start(Switch::Off);
|
|
||||||
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On
|
|
||||||
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off
|
|
||||||
let flips = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
|
|
||||||
*got2.lock().unwrap() = flips;
|
|
||||||
});
|
|
||||||
assert_eq!(*got.lock().unwrap(), 1, "turned On once across the two flips");
|
|
||||||
}
|
|
||||||
|
|
||||||
// `enter` fires once on start and once per *real* transition; a stay (a call
|
|
||||||
// that returns the current tag) does not re-enter.
|
|
||||||
#[test]
|
|
||||||
fn enter_on_start_and_each_transition_but_not_stay() {
|
|
||||||
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
|
||||||
let got2 = got.clone();
|
|
||||||
run(move || {
|
|
||||||
let sw = Sm::start(Switch::Off); // on_start enter -> enters = 1
|
|
||||||
let after_start = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
|
||||||
// Two stays (the reads above + below) must not bump enters.
|
|
||||||
let _ = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap();
|
|
||||||
let still = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
|
||||||
sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On -> enter -> enters = 2
|
|
||||||
let after_flip = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap();
|
|
||||||
*got2.lock().unwrap() = (after_start, still, after_flip);
|
|
||||||
});
|
|
||||||
assert_eq!(*got.lock().unwrap(), (1, 1, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A handler that panics tears the loop down; the in-flight call's reply channel
|
|
||||||
// closes as the stack unwinds, so the parked caller wakes with Down (mirrors
|
|
||||||
// gen_server's panicking-handler path).
|
|
||||||
#[test]
|
|
||||||
fn call_to_panicking_handler_is_down() {
|
|
||||||
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
|
||||||
let got2 = got.clone();
|
|
||||||
run(move || {
|
|
||||||
let sw = Sm::start(Switch::Off);
|
|
||||||
let r = sw.call(|rep| Ev::Call(Call::Boom(rep)));
|
|
||||||
*got2.lock().unwrap() = Some(r);
|
|
||||||
});
|
|
||||||
assert_eq!(*got.lock().unwrap(), Some(Err(CallError::Down)));
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user