Skip to main content

EpisodeBoard

Struct EpisodeBoard 

Source
pub struct EpisodeBoard { /* private fields */ }
Expand description

The episodes of one server.

In memory, like the peer board, and for a narrower reason: what survives the process is the repository’s own record, written by the same command a provider hook runs. This board holds only the association between a live connection and that record, which is a fact about now and stops being true when the process does.

use majordomus_cli::episodes::EpisodeBoard;
use majordomus_cli::peers::{PeerBoard, Transport};
let peers = PeerBoard::new();
let (a, b) = (peers.attach(Transport::Stdio), peers.attach(Transport::Http));
let board = EpisodeBoard::default();
board.attach(&b, "zeta", "generic");
board.attach(&a, "alpha", "generic");
// listed by external identity, never by peer or attachment order
let ids: Vec<String> = board.list().into_iter().map(|e| e.external_id).collect();
assert_eq!(ids, ["alpha", "zeta"]);
assert_eq!(board.close_all().len(), 2);

Implementations§

Source§

impl EpisodeBoard

Source

pub fn new() -> Self

A board that records nothing in the repository: every episode’s repository field says so. The board the unit suites and a repository-less server use.

use majordomus_cli::episodes::EpisodeBoard;
let board = EpisodeBoard::new();
assert!(board.list().is_empty(), "a new board holds no episode");
Source

pub fn for_repository(root: impl Into<PathBuf>) -> Self

A board that drives the repository at root through its own capture session, by a ToolDriver. Constructing it runs nothing; the first attach does.

use majordomus_cli::episodes::EpisodeBoard;
let board = EpisodeBoard::for_repository(std::env::temp_dir());
assert!(board.list().is_empty(), "no command ran and no episode exists yet");
assert_eq!(board.close_all().len(), 0);
Source

pub fn with_driver(driver: Box<dyn EpisodeDriver>) -> Self

A board over an arbitrary driver: the seam a test, or a server with another way of recording, plugs into. The driver is asked once per opened and once per closed episode.

use majordomus_cli::episodes::{CloseReason, EpisodeBoard, EpisodeDriver};
use majordomus_cli::peers::{PeerBoard, Transport};
struct Fixed;
impl EpisodeDriver for Fixed {
    fn open(&self, _: &str, _: &str) -> String { "opened".into() }
    fn close(&self, _: &str, _: &str, _: CloseReason) -> String { "closed".into() }
}
let board = EpisodeBoard::with_driver(Box::new(Fixed));
let peer = PeerBoard::new().attach(Transport::Stdio);
assert_eq!(board.attach(&peer, "t", "generic").0.repository, "opened");
Source

pub fn attach( &self, peer: &PeerId, external_id: &str, provider: &str, ) -> (ConnectionEpisode, bool)

Open an episode for external_id, or take an existing one over.

The whole point is in the second half. A client that comes back — after a crash, a rebuild, a dropped socket, a client restart — names itself the same way and gets the same episode, with a new peer holding it and attachments one higher. Nothing is re-opened, no second record is written, and the ledger lines on either side of the gap belong to one episode, which is what makes the record of it true.

Returns the episode and whether it was resumed rather than opened.

use majordomus_cli::episodes::EpisodeBoard;
use majordomus_cli::peers::{PeerBoard, Transport};
let peers = PeerBoard::new();
let board = EpisodeBoard::new();
let (p1, p2) = (peers.attach(Transport::Stdio), peers.attach(Transport::Http));
assert!(!board.attach(&p1, "work", "generic").1, "first: opened");
let (episode, resumed) = board.attach(&p2, "work", "generic");
assert!(resumed, "same identity, another peer: resumed");
assert_eq!((episode.attachments, episode.peer.as_ref()), (2, Some(&p2)));
assert_eq!(board.list().len(), 1, "one episode, not two");
Source

pub fn touch(&self, peer: &PeerId)

The client spoke. Every message on a connection holding an episode is a heartbeat for it, so a working client never has its episode reaped and no client has to send anything it would not otherwise send.

use std::time::Duration;
use majordomus_cli::episodes::EpisodeBoard;
use majordomus_cli::peers::{PeerBoard, Transport};
let peer = PeerBoard::new().attach(Transport::Http);
let board = EpisodeBoard::new();
board.attach(&peer, "busy", "generic");
board.touch(&peer);
assert_eq!(board.list()[0].last_activity_seconds_ago, 0);
assert!(board.reap(Duration::ZERO).is_empty(), "a held episode is never reaped");
Source

pub fn detach(&self, peer: &PeerId) -> Vec<ConnectionEpisode>

The connection went. The episode is not closed: it is detached, and the client can come back to it by name for REATTACH_GRACE.

This is the difference between a lifecycle and a session counter. Closing here would publish a record saying the work ended every time a socket dropped, and the work almost never ends when a socket drops.

use majordomus_cli::episodes::{ConnectionEpisodeState, EpisodeBoard};
use majordomus_cli::peers::{PeerBoard, Transport};
let peer = PeerBoard::new().attach(Transport::Http);
let board = EpisodeBoard::new();
board.attach(&peer, "kept", "generic");
let detached = board.detach(&peer);
assert_eq!(detached.len(), 1);
assert!(detached[0].peer.is_none());
assert_eq!(board.list()[0].state, ConnectionEpisodeState::Detached, "still on the board");
assert!(board.detach(&peer).is_empty(), "nothing is held twice");
Source

pub fn close( &self, external_id: &str, reason: CloseReason, ) -> Option<ConnectionEpisode>

Close one episode deliberately, by name: the client said it was done.

use majordomus_cli::episodes::{CloseReason, EpisodeBoard};
use majordomus_cli::peers::{PeerBoard, Transport};
let peer = PeerBoard::new().attach(Transport::Stdio);
let board = EpisodeBoard::new();
board.attach(&peer, "done", "generic");
let closed = board.close("done", CloseReason::Detach).expect("it was open");
assert!(closed.peer.is_none());
assert!(board.close("done", CloseReason::Detach).is_none(), "closed once only");
assert!(board.of_peer(&peer).is_none());
Source

pub fn reap(&self, grace: Duration) -> Vec<ConnectionEpisode>

Close every episode detached longer than grace. The reaper, and the answer to the client that never comes back. Returns what it closed, each as CloseReason::Expired, which the record reads as interrupted.

use std::time::Duration;
use majordomus_cli::episodes::EpisodeBoard;
use majordomus_cli::peers::{PeerBoard, Transport};
let peer = PeerBoard::new().attach(Transport::Http);
let board = EpisodeBoard::new();
board.attach(&peer, "gone", "generic");
board.detach(&peer);
assert!(board.reap(Duration::from_secs(3600)).is_empty(), "inside the grace it waits");
assert_eq!(board.reap(Duration::ZERO).len(), 1);
assert!(board.list().is_empty());
Source

pub fn close_all(&self) -> Vec<ConnectionEpisode>

Close every episode: the server is stopping.

An episode left open by a server that has gone is an episode nothing will ever close, and the repository would carry it as open for ever — the failure ADR 0041 names on the hook side, reached here by a different road.

use majordomus_cli::episodes::EpisodeBoard;
use majordomus_cli::peers::{PeerBoard, Transport};
let peers = PeerBoard::new();
let board = EpisodeBoard::new();
board.attach(&peers.attach(Transport::Stdio), "held", "generic");
let detached = peers.attach(Transport::Http);
board.attach(&detached, "waiting", "generic");
board.detach(&detached);
assert_eq!(board.close_all().len(), 2, "held and detached alike");
assert!(board.list().is_empty());
Source

pub fn list(&self) -> Vec<ConnectionEpisode>

Every episode this server holds, open and detached, in external-identity order, each with its idle time measured now rather than when it was last written.

use majordomus_cli::episodes::{ConnectionEpisodeState, EpisodeBoard};
use majordomus_cli::peers::{PeerBoard, Transport};
let peers = PeerBoard::new();
let board = EpisodeBoard::new();
let p = peers.attach(Transport::Stdio);
board.attach(&p, "b", "generic");
board.detach(&p);
board.attach(&peers.attach(Transport::Http), "a", "generic");
let states: Vec<_> = board.list().into_iter().map(|e| (e.external_id, e.state)).collect();
assert_eq!(states, [("a".to_string(), ConnectionEpisodeState::Open),
                    ("b".to_string(), ConnectionEpisodeState::Detached)]);
Source

pub fn of_peer(&self, peer: &PeerId) -> Option<ConnectionEpisode>

The episode a connection holds, if it holds one. A connection that only initialised holds none, and neither does one whose episode was detached from it.

use majordomus_cli::episodes::EpisodeBoard;
use majordomus_cli::peers::{PeerBoard, Transport};
let peer = PeerBoard::new().attach(Transport::Http);
let board = EpisodeBoard::new();
assert!(board.of_peer(&peer).is_none(), "a peer is not a worker until it attaches");
board.attach(&peer, "mine", "generic");
assert_eq!(board.of_peer(&peer).map(|e| e.external_id).as_deref(), Some("mine"));

Trait Implementations§

Source§

impl Debug for EpisodeBoard

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for EpisodeBoard

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more