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
impl EpisodeBoard
Sourcepub fn new() -> Self
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");Sourcepub fn for_repository(root: impl Into<PathBuf>) -> Self
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);Sourcepub fn with_driver(driver: Box<dyn EpisodeDriver>) -> Self
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");Sourcepub fn attach(
&self,
peer: &PeerId,
external_id: &str,
provider: &str,
) -> (ConnectionEpisode, bool)
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");Sourcepub fn touch(&self, peer: &PeerId)
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");Sourcepub fn detach(&self, peer: &PeerId) -> Vec<ConnectionEpisode>
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");Sourcepub fn close(
&self,
external_id: &str,
reason: CloseReason,
) -> Option<ConnectionEpisode>
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());Sourcepub fn reap(&self, grace: Duration) -> Vec<ConnectionEpisode>
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());Sourcepub fn close_all(&self) -> Vec<ConnectionEpisode>
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());Sourcepub fn list(&self) -> Vec<ConnectionEpisode>
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)]);Sourcepub fn of_peer(&self, peer: &PeerId) -> Option<ConnectionEpisode>
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"));