Skip to main content

behavior/protocol/
mod.rs

1//! Neutral typed vocabulary for interpreter-originated event and service lanes.
2//!
3//! Concrete behavior transformations define the closed sum types that add
4//! these lanes. Keeping their values and construction capabilities here avoids
5//! dependencies between otherwise independent transformations.
6
7pub(crate) mod forward;
8
9use std::time::Duration;
10
11use std::time::Instant;
12
13use crate::behavior::Address;
14use crate::{Crash, CreationKind, Exit};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct TimerId(pub u64);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct TimerGeneration(pub u64);
21
22impl From<u64> for TimerId {
23    fn from(value: u64) -> Self {
24        Self(value)
25    }
26}
27
28impl From<TimerId> for u64 {
29    fn from(value: TimerId) -> Self {
30        value.0
31    }
32}
33
34impl From<u64> for TimerGeneration {
35    fn from(value: u64) -> Self {
36        Self(value)
37    }
38}
39
40impl From<TimerGeneration> for u64 {
41    fn from(value: TimerGeneration) -> Self {
42        value.0
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct ScheduleAt {
48    pub id: TimerId,
49    pub generation: TimerGeneration,
50    pub at: Instant,
51}
52
53impl ScheduleAt {
54    #[must_use]
55    pub const fn new(id: TimerId, generation: TimerGeneration, at: Instant) -> Self {
56        Self { id, generation, at }
57    }
58}
59
60impl From<(TimerId, TimerGeneration, Instant)> for ScheduleAt {
61    fn from((id, generation, at): (TimerId, TimerGeneration, Instant)) -> Self {
62        Self::new(id, generation, at)
63    }
64}
65
66/// Request scheduling relative to the interpreter's clock.
67///
68/// Constructing this value does not observe a clock. The interpreter resolves
69/// `after` only when it interprets the successful transition that emitted the
70/// request.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct ScheduleAfter {
73    pub id: TimerId,
74    pub generation: TimerGeneration,
75    pub after: Duration,
76}
77
78impl ScheduleAfter {
79    #[must_use]
80    pub const fn new(id: TimerId, generation: TimerGeneration, after: Duration) -> Self {
81        Self {
82            id,
83            generation,
84            after,
85        }
86    }
87}
88
89impl From<(TimerId, TimerGeneration, Duration)> for ScheduleAfter {
90    fn from((id, generation, after): (TimerId, TimerGeneration, Duration)) -> Self {
91        Self::new(id, generation, after)
92    }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct TimerElapsed {
97    pub id: TimerId,
98    pub generation: TimerGeneration,
99}
100
101impl TimerElapsed {
102    #[must_use]
103    pub const fn new(id: TimerId, generation: TimerGeneration) -> Self {
104        Self { id, generation }
105    }
106}
107
108impl From<(TimerId, TimerGeneration)> for TimerElapsed {
109    fn from((id, generation): (TimerId, TimerGeneration)) -> Self {
110        Self::new(id, generation)
111    }
112}
113
114/// Ask the local interpreter to observe the exact peer incarnation selected at
115/// `peer` when this request is interpreted.
116///
117/// [`PeerStopped`] is the pure result protocol. It arrives eventually if a
118/// selected live incarnation later terminates, or may arrive immediately when
119/// the interpreter has authoritative retained termination for the requested
120/// incarnation. Absence from a live-address table is not such authority: an
121/// interpreter that can select neither a live incarnation nor retained
122/// terminal history must return an interpreter error rather than fabricate a
123/// stop result.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct ObservePeer<A> {
126    pub peer: A,
127}
128
129impl<A> From<A> for ObservePeer<A> {
130    fn from(peer: A) -> Self {
131        Self { peer }
132    }
133}
134
135impl<A> ObservePeer<A> {
136    #[must_use]
137    pub const fn new(peer: A) -> Self {
138        Self { peer }
139    }
140}
141
142/// Ask the local interpreter to cancel this actor's observation of `peer`.
143///
144/// Peer observation is a derived Bombay protocol, not an actor-model
145/// primitive. The address names the same observer-local relationship created
146/// by [`ObservePeer`]; exact-incarnation capture and cancellation belong to the
147/// interpreter. Cancellation does not retract a [`PeerStopped`] event already
148/// admitted to the actor's mailbox, and an interpreter treats a request for a
149/// relationship that is no longer present as inert.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct UnwatchPeer<A> {
152    pub peer: A,
153}
154
155impl<A> UnwatchPeer<A> {
156    #[must_use]
157    pub const fn new(peer: A) -> Self {
158        Self { peer }
159    }
160}
161
162impl<A> From<A> for UnwatchPeer<A> {
163    fn from(peer: A) -> Self {
164        Self::new(peer)
165    }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct PeerStopped<A: Address> {
170    pub peer: A,
171    pub outcome: Result<Exit<A>, Crash>,
172}
173
174impl<A: Address> PeerStopped<A> {
175    #[must_use]
176    pub fn new(peer: A, outcome: Result<Exit<A>, Crash>) -> Self {
177        Self { peer, outcome }
178    }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct ChildStopped<A: Address> {
183    pub nonce: A::Nonce,
184    pub outcome: Result<Exit<A>, Crash>,
185    pub at: Instant,
186}
187
188impl<A: Address> ChildStopped<A> {
189    #[must_use]
190    pub fn new(nonce: A::Nonce, outcome: Result<Exit<A>, Crash>, at: Instant) -> Self {
191        Self { nonce, outcome, at }
192    }
193}
194
195/// Ask the local interpreter to observe the exact child generation bound at
196/// `nonce`.
197///
198/// Creation is resolved before same-action service sends. If that creation was
199/// rejected, no child exists to observe: the interpreter consumes this request
200/// without installing an observation or emitting [`ChildStopped`]. The
201/// rejection remains observable through [`ObserveCreation`], and a later
202/// creation cannot inherit the consumed observation.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct ObserveChild<N> {
205    pub nonce: N,
206}
207
208impl<N> ObserveChild<N> {
209    #[must_use]
210    pub const fn new(nonce: N) -> Self {
211        Self { nonce }
212    }
213}
214
215/// A proxy's request for its interpreter to report a worker termination to
216/// the proxy's parent. The interpreter supplies the emitting proxy's child
217/// nonce when constructing [`WorkerStopped`].
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct ReportWorkerStopped<A: Address> {
220    pub worker: A::Nonce,
221    pub outcome: Result<Exit<A>, Crash>,
222    pub at: Instant,
223}
224
225impl<A: Address> ReportWorkerStopped<A> {
226    #[must_use]
227    pub fn new(worker: A::Nonce, outcome: Result<Exit<A>, Crash>, at: Instant) -> Self {
228        Self {
229            worker,
230            outcome,
231            at,
232        }
233    }
234}
235
236impl<A: Address> From<ChildStopped<A>> for ReportWorkerStopped<A> {
237    fn from(stopped: ChildStopped<A>) -> Self {
238        Self::new(stopped.nonce, stopped.outcome, stopped.at)
239    }
240}
241
242/// A worker termination reported by a still-live supervised proxy.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct WorkerStopped<A: Address> {
245    pub proxy: A::Nonce,
246    pub worker: A::Nonce,
247    pub outcome: Result<Exit<A>, Crash>,
248    pub at: Instant,
249}
250
251impl<A: Address> WorkerStopped<A> {
252    #[must_use]
253    pub fn new(
254        proxy: A::Nonce,
255        worker: A::Nonce,
256        outcome: Result<Exit<A>, Crash>,
257        at: Instant,
258    ) -> Self {
259        Self {
260            proxy,
261            worker,
262            outcome,
263            at,
264        }
265    }
266}
267
268impl<A: Address> From<(A::Nonce, ReportWorkerStopped<A>)> for WorkerStopped<A> {
269    fn from((proxy, stopped): (A::Nonce, ReportWorkerStopped<A>)) -> Self {
270        Self::new(proxy, stopped.worker, stopped.outcome, stopped.at)
271    }
272}
273
274/// Why a staged fresh creation was not committed by an interpreter.
275///
276/// This is a closed semantic classification; interpreter-specific error
277/// values remain at the runtime boundary.
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum CreationRejection {
280    /// The creator-local nonce was already bound, so accepting the request
281    /// would overwrite rather than establish a fresh child.
282    NonceAlreadyBound,
283    /// The fresh child's initialization did not complete successfully.
284    InitializationFailed,
285    /// The interpreter could not allocate, install, or commit the fresh child.
286    EnvironmentFailed,
287}
288
289/// The committed result of one staged [`crate::Create`] request.
290///
291/// `Installed` is emitted only after fresh allocation, successful
292/// initialization, and binding at `nonce`. The replacement provenance is the
293/// provenance supplied by Behavior; an interpreter must never infer it from
294/// address reuse or creation order.
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub struct CreationResolved<N> {
297    pub nonce: N,
298    pub kind: CreationKind<N>,
299    pub result: Result<(), CreationRejection>,
300}
301
302impl<N> CreationResolved<N> {
303    #[must_use]
304    pub const fn new(
305        nonce: N,
306        kind: CreationKind<N>,
307        result: Result<(), CreationRejection>,
308    ) -> Self {
309        Self {
310            nonce,
311            kind,
312            result,
313        }
314    }
315
316    #[must_use]
317    pub const fn installed(nonce: N, kind: CreationKind<N>) -> Self {
318        Self::new(nonce, kind, Ok(()))
319    }
320
321    /// A successfully committed ordinary birth.
322    #[must_use]
323    pub const fn birth(nonce: N) -> Self {
324        Self::installed(nonce, CreationKind::Birth)
325    }
326
327    /// A successfully committed replacement incarnation.
328    #[must_use]
329    pub const fn replacement_incarnation(nonce: N, replaces: N) -> Self {
330        Self::installed(nonce, CreationKind::ReplacementIncarnation { replaces })
331    }
332
333    #[must_use]
334    pub const fn rejected(nonce: N, kind: CreationKind<N>, rejection: CreationRejection) -> Self {
335        Self::new(nonce, kind, Err(rejection))
336    }
337}
338
339/// Ask the local interpreter to return the committed result of the same-action
340/// creation at `nonce` through the behavior's typed creation-result lane.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub struct ObserveCreation<N> {
343    pub nonce: N,
344}
345
346impl<N> ObserveCreation<N> {
347    #[must_use]
348    pub const fn new(nonce: N) -> Self {
349        Self { nonce }
350    }
351}
352
353/// Ask a proxy's interpreter to report a worker creation result to its parent.
354/// The interpreter supplies the emitting proxy's nonce.
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub struct ReportWorkerCreationResolved<N> {
357    pub worker: N,
358    pub kind: CreationKind<N>,
359    pub result: Result<(), CreationRejection>,
360}
361
362impl<N> ReportWorkerCreationResolved<N> {
363    #[must_use]
364    pub const fn new(
365        worker: N,
366        kind: CreationKind<N>,
367        result: Result<(), CreationRejection>,
368    ) -> Self {
369        Self {
370            worker,
371            kind,
372            result,
373        }
374    }
375}
376
377impl<N> From<CreationResolved<N>> for ReportWorkerCreationResolved<N> {
378    fn from(resolved: CreationResolved<N>) -> Self {
379        Self::new(resolved.nonce, resolved.kind, resolved.result)
380    }
381}
382
383/// A worker creation result reported by a still-live supervised proxy.
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub struct WorkerCreationResolved<N> {
386    pub proxy: N,
387    pub worker: N,
388    pub kind: CreationKind<N>,
389    pub result: Result<(), CreationRejection>,
390}
391
392/// Consumer-facing resolution of one explicitly designated replacement.
393///
394/// This is a derived view of [`WorkerCreationResolved`], not another runtime
395/// fact or observation request. `replaced` is the exact prior incarnation
396/// carried by Behavior in [`CreationKind::ReplacementIncarnation`];
397/// `replacement`/`attempt` is the fresh creation nonce. The prior worker's
398/// terminal outcome remains the separate [`WorkerStopped`] fact so creation
399/// resolution cannot duplicate, erase, or reinterpret it.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum ReplacementResolution<N> {
402    Installed {
403        proxy: N,
404        replaced: N,
405        replacement: N,
406    },
407    Rejected {
408        proxy: N,
409        replaced: N,
410        attempt: N,
411        rejection: CreationRejection,
412    },
413}
414
415impl<N> WorkerCreationResolved<N> {
416    #[must_use]
417    pub const fn new(
418        proxy: N,
419        worker: N,
420        kind: CreationKind<N>,
421        result: Result<(), CreationRejection>,
422    ) -> Self {
423        Self {
424            proxy,
425            worker,
426            kind,
427            result,
428        }
429    }
430
431    /// Project a replacement result without conflating ordinary birth with
432    /// replacement or inferring provenance from nonce arithmetic.
433    #[must_use]
434    pub fn into_replacement(self) -> Option<ReplacementResolution<N>> {
435        let CreationKind::ReplacementIncarnation { replaces } = self.kind else {
436            return None;
437        };
438        Some(match self.result {
439            Ok(()) => ReplacementResolution::Installed {
440                proxy: self.proxy,
441                replaced: replaces,
442                replacement: self.worker,
443            },
444            Err(rejection) => ReplacementResolution::Rejected {
445                proxy: self.proxy,
446                replaced: replaces,
447                attempt: self.worker,
448                rejection,
449            },
450        })
451    }
452}
453
454impl<N> From<(N, ReportWorkerCreationResolved<N>)> for WorkerCreationResolved<N> {
455    fn from((proxy, resolved): (N, ReportWorkerCreationResolved<N>)) -> Self {
456        Self::new(proxy, resolved.worker, resolved.kind, resolved.result)
457    }
458}
459
460/// A request to finish through one serialized behavior transition.
461#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
462pub struct ShutdownRequested;
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::MailAddr;
468
469    #[test]
470    fn lifecycle_conversions_preserve_every_semantic_field() {
471        let at = Instant::now();
472        let child = ChildStopped::<MailAddr>::new(3, Err(Crash::Failed), at);
473        let report = ReportWorkerStopped::from(child);
474        let worker = WorkerStopped::from((7, report));
475        assert_eq!(worker.proxy, 7);
476        assert_eq!(worker.worker, 3);
477        assert_eq!(worker.outcome, Err(Crash::Failed));
478        assert_eq!(worker.at, at);
479
480        let creation = CreationResolved::<u64>::rejected(
481            4,
482            CreationKind::replacement_of(3),
483            CreationRejection::EnvironmentFailed,
484        );
485        let report = ReportWorkerCreationResolved::from(creation);
486        let worker = WorkerCreationResolved::from((7, report));
487        assert_eq!(worker.proxy, 7);
488        assert_eq!(worker.worker, 4);
489        assert_eq!(worker.kind, CreationKind::replacement_of(3));
490        assert_eq!(worker.result, Err(CreationRejection::EnvironmentFailed));
491        assert_eq!(
492            worker.into_replacement(),
493            Some(ReplacementResolution::Rejected {
494                proxy: 7,
495                replaced: 3,
496                attempt: 4,
497                rejection: CreationRejection::EnvironmentFailed,
498            })
499        );
500
501        let installed = WorkerCreationResolved::new(7, 5, CreationKind::replacement_of(4), Ok(()));
502        assert_eq!(
503            installed.into_replacement(),
504            Some(ReplacementResolution::Installed {
505                proxy: 7,
506                replaced: 4,
507                replacement: 5,
508            })
509        );
510        assert_eq!(
511            WorkerCreationResolved::new(7, 0, CreationKind::Birth, Ok(())).into_replacement(),
512            None
513        );
514        assert_eq!(
515            WorkerCreationResolved::new(
516                7,
517                0,
518                CreationKind::Birth,
519                Err(CreationRejection::NonceAlreadyBound),
520            )
521            .into_replacement(),
522            None
523        );
524    }
525
526    #[test]
527    fn timer_newtypes_and_requests_have_lossless_construction() {
528        let id = TimerId::from(2);
529        let generation = TimerGeneration::from(5);
530        assert_eq!(u64::from(id), 2);
531        assert_eq!(u64::from(generation), 5);
532        assert_eq!(
533            TimerElapsed::from((id, generation)),
534            TimerElapsed::new(id, generation)
535        );
536    }
537}