//! `/gateway/state.json`: what `gatewayd` has handled, the threads it takes part in, and the //! turns in flight (M4a spec, section 9). Written atomically after every change. A file that //! cannot be read, parsed or written stops `gatewayd`: guessing would answer posts twice. use std::collections::BTreeMap; use std::fs; use std::io::{self, Write}; use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::config::valid_id; pub const RECENT_KEPT: usize = 500; pub const THREADS_KEPT: usize = 5000; pub const RUNBOOK: &str = "see docs/runbook.md#gateway-state-damaged"; #[derive(Debug)] pub enum StateError { Read(PathBuf, String), Write(PathBuf, io::Error), } impl std::fmt::Display for StateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { StateError::Read(path, why) => write!(f, "{}: {why}\n{RUNBOOK}", path.display()), StateError::Write(path, err) => { write!(f, "{}: cannot write: {err}\n{RUNBOOK}", path.display()) } } } } impl std::error::Error for StateError {} /// A turn sent to `loopd` and not yet answered. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct InFlight { pub session: String, pub channel: String, pub root: String, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct StateFile { channels: BTreeMap, recent: Vec, threads: Vec, in_flight: Vec, } impl StateFile { /// Every id is a Mattermost id; a session is `mm-`. fn problem(&self) -> Option { // Every key of channels, every entry of recent and threads must be `valid_id` ("not a // Mattermost id: "). Each in_flight entry: session is "mm-" + a valid id, // channel and root valid ids ("a turn in flight is not valid: "). None // when all are good. todo!() } } pub struct State { path: PathBuf, file: StateFile, } impl State { /// Read the state; a missing file is a first start. pub fn load(path: &Path) -> Result { // Read the file. Missing (NotFound only) -> an empty StateFile. Any other read error, a // parse error (serde_json), or `problem()` -> Read(path, why). todo!() } /// Write the state to its path atomically, in six steps; an error leaves the old file. fn save(&self) -> Result<(), StateError> { // `persist`, its error as Write(path, error). todo!() } fn persist(&self) -> io::Result<()> { // The six steps of brokerd/src/state.rs, persist: 1. the parent directory, recursive, 0700; // 2. serde_json::to_string plus "\n"; 3. ".tmp" (with_extension("json.tmp")), create // + truncate, mode 0600; 4. write_all and sync_all; 5. rename over the path; 6. open the // directory and sync_all. todo!() } /// Was this post handled already? pub fn seen(&self, post_id: &str) -> bool { // Is the id in recent? todo!() } /// A post was handled (acted on or ignored): remember its id and move its channel's mark. pub fn handled( &mut self, post_id: &str, channel: &str, create_at: i64, ) -> Result<(), StateError> { // If not seen: push the id to recent, then drop the oldest beyond RECENT_KEPT. The channel // mark becomes the larger of its old value and create_at (a new channel starts at // create_at). Save. todo!() } /// The `create_at` of the last post handled in a channel, if any. pub fn since(&self, channel: &str) -> Option { // The mark of the channel. todo!() } /// The channels with a mark, for catching up. pub fn channels(&self) -> Vec { // The channel ids that have a mark. todo!() } /// Start catching up a channel from `at` (milliseconds), if it has no mark yet. pub fn mark(&mut self, channel: &str, at: i64) -> Result<(), StateError> { // A channel that has a mark is left alone (nothing saved). Otherwise set it to `at` and // save. todo!() } pub fn knows_thread(&self, root: &str) -> bool { // Is the root in threads? todo!() } /// This Boxmaker takes part in a thread; the newest `THREADS_KEPT` are kept. pub fn join_thread(&mut self, root: &str) -> Result<(), StateError> { // Known -> nothing. Else push it, drop the oldest beyond THREADS_KEPT, save. todo!() } pub fn start_turn(&mut self, turn: InFlight) -> Result<(), StateError> { // Remove any entry of the same session, push this one, save. todo!() } pub fn end_turn(&mut self, session: &str) -> Result<(), StateError> { // Remove the entries of the session, save. todo!() } /// The turns left in flight by the last run, removed from the state. pub fn take_in_flight(&mut self) -> Result, StateError> { // Take the whole list out (std::mem::take); save only when it was not empty. todo!() } }