use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::note::{Addressee, Criterion, NoteText};
use crate::plan::Node;
pub const REPLY_ENVELOPE_VERSION: u32 = 3;
pub const REPLY_ENVELOPE_VERSIONS_READ: &[u32] = &[REPLY_ENVELOPE_VERSION, 2];
fn read_at_a_version_this_build_reads<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let declared = Option::<u32>::deserialize(deserializer)?;
Ok(declared.map(|version| {
if REPLY_ENVELOPE_VERSIONS_READ.contains(&version) {
REPLY_ENVELOPE_VERSION
} else {
version
}
}))
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Author {
#[default]
Planner,
Monitor,
}
impl Author {
pub fn as_str(self) -> &'static str {
match self {
Self::Planner => "planner",
Self::Monitor => "monitor",
}
}
pub(crate) fn is_planner(&self) -> bool {
matches!(self, Self::Planner)
}
}
pub fn allows_completion(author: Author, completion: Option<bool>) -> crate::Result<()> {
if author == Author::Planner || completion != Some(true) {
return Ok(());
}
Err(crate::Error::Refused(
"declaring the run complete is not something the monitor may do: whether the run \
is finished is the planner's verdict, not an observation. Surface it to the \
planner instead"
.to_string(),
))
}
pub fn allows(author: Author, command: &Command) -> crate::Result<()> {
if author == Author::Planner {
return Ok(());
}
let refused = match command {
Command::Retry { .. }
| Command::Requeue { .. }
| Command::Cancel { .. }
| Command::Finding { .. }
| Command::Add { .. } => return Ok(()),
Command::Complete { .. } => {
"whether the run is finished is the planner's verdict, not an observation"
}
Command::Attest { .. } => {
"a human action is attested by the person who took it, never by a watcher"
}
Command::Drop { .. } => {
"removing work from the graph is a decomposition decision the planner owns"
}
Command::Reparent { .. } => {
"rewiring dependencies is a decomposition decision the planner owns"
}
Command::Amend { .. } => {
"what a node is judged against is a decomposition decision the planner owns"
}
Command::Note { .. } => {
"a note may bind a criterion the node's judge decides against, which is the \
planner's decision rather than an observation"
}
Command::Settle { .. } => {
"settling a node from evidence declares an outcome this run never observed, \
which is the planner's decision rather than an observation"
}
};
Err(crate::Error::Refused(format!(
"'{}' is not an op the monitor may issue: {refused}. Surface it to the planner instead",
op_of(command)
)))
}
pub fn op_of(command: &Command) -> &'static str {
match command {
Command::Add { .. } => "add",
Command::Drop { .. } => "drop",
Command::Reparent { .. } => "reparent",
Command::Retry { .. } => "retry",
Command::Cancel { .. } => "cancel",
Command::Requeue { .. } => "requeue",
Command::Attest { .. } => "attest",
Command::Complete { .. } => "complete",
Command::Amend { .. } => "amend",
Command::Note { .. } => "note",
Command::Finding { .. } => "finding",
Command::Settle { .. } => "settle",
}
}
pub fn target_of(command: &Command) -> Option<String> {
match command {
Command::Add { node } => Some(node.id.clone()),
Command::Drop { id, .. }
| Command::Reparent { id, .. }
| Command::Retry { id, .. }
| Command::Cancel { id, .. }
| Command::Requeue { id, .. }
| Command::Note { id, .. }
| Command::Settle { id, .. }
| Command::Amend { id, .. } => Some(id.clone()),
Command::Attest { reference } => Some(reference.clone()),
Command::Finding { id, .. } => id.clone(),
Command::Complete { .. } => None,
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Reply {
#[serde(
default,
deserialize_with = "read_at_a_version_this_build_reads",
skip_serializing_if = "Option::is_none"
)]
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "Author::is_planner")]
pub author: Author,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completion: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<Command>,
}
impl Reply {
pub(crate) fn carries_verdict(&self) -> bool {
self.completion.is_some() || self.message.is_some() || self.reason.is_some()
}
pub(crate) fn carries_edits_without_a_verdict(&self) -> bool {
!self.commands.is_empty() && !self.carries_verdict()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Dependents {
Drop,
Detach,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "lowercase", deny_unknown_fields)]
pub enum Command {
Add {
node: Node,
},
Drop {
id: String,
dependents: Dependents,
},
Reparent {
id: String,
deps: Vec<String>,
},
Retry {
id: String,
node: Node,
},
Cancel {
id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Requeue {
id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
amend: Option<Map<String, Value>>,
},
Attest {
#[serde(rename = "ref")]
reference: String,
},
Complete {
reason: String,
},
Amend {
id: String,
text: String,
},
Note {
id: String,
addressee: Addressee,
text: NoteText,
#[serde(default, skip_serializing_if = "Option::is_none")]
criterion: Option<Criterion>,
#[serde(default, skip_serializing_if = "Deliver::is_default")]
deliver: Deliver,
#[serde(default = "persists", skip_serializing_if = "is_true")]
persist: bool,
},
Finding {
message: String,
#[serde(default, skip_serializing_if = "is_false")]
blocking: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
},
Settle {
id: String,
outcome: SettleOutcome,
evidence: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
landing: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SettleOutcome {
Done,
Failed,
}
impl SettleOutcome {
pub fn as_str(self) -> &'static str {
match self {
Self::Done => "done",
Self::Failed => "failed",
}
}
}
fn is_false(value: &bool) -> bool {
!*value
}
fn is_true(value: &bool) -> bool {
*value
}
fn persists() -> bool {
true
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Deliver {
#[default]
Live,
Next,
}
impl Deliver {
fn is_default(&self) -> bool {
matches!(self, Self::Live)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
#[value(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum SurfaceKind {
CheckIn,
Finding,
}
impl SurfaceKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::CheckIn => "check-in",
Self::Finding => "finding",
}
}
}
pub const REPLY_TIMEOUT_ENV: &str = "ONEPIPELINE_REPLY_TIMEOUT_SECONDS";
pub const DEFAULT_REPLY_TIMEOUT_SECONDS: u64 = 30;
pub const SERVE_SESSION_ENV: &str = "ONEPIPELINE_SERVE_SESSION_SECONDS";
pub const ASKER_ENV: &str = "ONEPIPELINE_CHANNEL_ASKER";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub(crate) struct Asker(String);
impl Asker {
pub(crate) fn named(value: &std::ffi::OsStr) -> crate::Result<Self> {
let value = value.to_str().ok_or_else(|| {
crate::Error::Refused(format!(
"{ASKER_ENV} is set to a value this host cannot read as text; an asker is \
compared to other askers as one word, and two values that are not text read \
as the same word — set it to a name in Unicode, or leave it unset for a \
session that listens on its own"
))
})?;
Self::checked(value)
}
fn checked(value: &str) -> crate::Result<Self> {
if value.trim().is_empty() {
return Err(crate::Error::Refused(format!(
"{ASKER_ENV} is set to a blank value, which names no asker; leave it unset for \
a session that listens on its own, or set it to the one value every session \
of this asker carries"
)));
}
Ok(Self(value.to_owned()))
}
}
fn recorded_asker<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Asker>, D::Error> {
Ok(Option::<String>::deserialize(deserializer)?.and_then(|name| Asker::checked(&name).ok()))
}
pub(crate) mod source {
pub const CHECK_IN: &str = "check-in";
pub const PROPOSAL: &str = "proposal";
pub const RECONCILER: &str = "reconciler";
pub const MONITOR: &str = "monitor";
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct Surface {
pub id: u64,
pub kind: String,
pub message: String,
pub source: String,
pub blocking: bool,
pub queued_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workstream: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub abandoned: bool,
#[serde(
default,
deserialize_with = "recorded_asker",
skip_serializing_if = "Option::is_none"
)]
pub asker: Option<Asker>,
}
#[derive(Debug, Clone)]
pub(crate) struct ChannelState {
paths: crate::ledger::RunPaths,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct Queue {
#[serde(default)]
pub waiting: Vec<Surface>,
#[serde(default)]
pub pending: Option<Surface>,
#[serde(default)]
pub next_id: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accounted: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "as_hex_opt",
deserialize_with = "of_hex_opt"
)]
pub seal: Option<u128>,
}
fn as_hex_opt<S: serde::Serializer>(digest: &Option<u128>, writer: S) -> Result<S::Ok, S::Error> {
match digest {
Some(digest) => crate::checkpoint::as_hex(digest, writer),
None => writer.serialize_none(),
}
}
fn of_hex_opt<'de, D: serde::Deserializer<'de>>(reader: D) -> Result<Option<u128>, D::Error> {
#[derive(serde::Deserialize)]
struct Hex(#[serde(deserialize_with = "crate::checkpoint::of_hex")] u128);
Ok(Option::<Hex>::deserialize(reader)?.map(|Hex(digest)| digest))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum SurfaceEvent {
Queued,
Claimed,
Answered,
Abandoned,
Attended,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct SurfaceRecord {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event: Option<SurfaceEvent>,
#[serde(flatten)]
pub surface: Surface,
}
impl SurfaceRecord {
fn event(&self, next_id: u64) -> SurfaceEvent {
self.event.unwrap_or(if self.surface.id >= next_id {
SurfaceEvent::Queued
} else if self.surface.abandoned {
SurfaceEvent::Abandoned
} else {
SurfaceEvent::Attended
})
}
}
impl Queue {
fn sealed(&self) -> Option<u128> {
let accounted = self.accounted?;
let claims =
serde_json::to_vec(&(&self.waiting, &self.pending, self.next_id)).unwrap_or_default();
let sealed = crate::checkpoint::digested(crate::checkpoint::NOTHING_DIGESTED, &claims);
Some(crate::checkpoint::digested(
sealed,
&accounted.to_le_bytes(),
))
}
fn seal(&mut self) {
self.seal = self.sealed();
}
fn is_intact(&self) -> bool {
self.accounted.is_none() || self.seal.is_some() && self.seal == self.sealed()
}
fn apply(&mut self, record: &SurfaceRecord) {
let surface = &record.surface;
match record.event(self.next_id) {
SurfaceEvent::Queued => {
let Some(after) = surface.id.checked_add(1) else {
return;
};
if surface.source == source::CHECK_IN {
self.waiting
.retain(|existing| existing.source != source::CHECK_IN);
}
self.waiting.push(surface.clone());
self.next_id = self.next_id.max(after);
}
SurfaceEvent::Claimed => {
let Some(at) = self.waiting.iter().position(|w| w.id == surface.id) else {
return;
};
let taken = self.waiting.remove(at);
if taken.blocking && (!taken.abandoned || self.pending.is_none()) {
if let Some(displaced) = self.pending.replace(taken) {
if displaced.abandoned {
self.waiting.push(displaced);
}
}
}
}
SurfaceEvent::Answered => {
if self
.pending
.as_ref()
.is_some_and(|held| held.id == surface.id)
{
self.pending = None;
}
}
SurfaceEvent::Abandoned | SurfaceEvent::Attended => {
let abandoned = record.event(self.next_id) == SurfaceEvent::Abandoned;
for held in self.waiting.iter_mut().chain(self.pending.iter_mut()) {
if held.id == surface.id {
held.abandoned = abandoned;
}
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct QueuedReply {
pub id: u64,
pub reply: Reply,
pub at: u64,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct QueuedCommands {
pub id: u64,
#[serde(default)]
pub author: Author,
pub commands: Vec<Command>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Fingerprint {
queue: Option<(u64, std::time::SystemTime)>,
surfaces: Option<(u64, std::time::SystemTime)>,
commands: Option<(u64, std::time::SystemTime)>,
}
fn mark(path: &std::path::Path) -> Option<(u64, std::time::SystemTime)> {
let metadata = std::fs::metadata(path).ok()?;
Some((
metadata.len(),
metadata
.modified()
.unwrap_or(std::time::SystemTime::UNIX_EPOCH),
))
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct CommandOutcome {
pub id: u64,
pub applied: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub results: Vec<CommandResult>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct CommandResult {
pub index: usize,
pub op: String,
pub outcome: CommandVerdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum CommandVerdict {
Applied,
Validated,
Delivered,
Refused,
}
impl ChannelState {
pub fn new(paths: &crate::ledger::RunPaths) -> Self {
Self {
paths: paths.clone(),
}
}
fn queue_path(&self) -> std::path::PathBuf {
self.paths.channel("queue.json")
}
fn log_path(&self) -> std::path::PathBuf {
self.paths.channel("surfaces.jsonl")
}
pub(crate) fn fingerprint(&self) -> Fingerprint {
Fingerprint {
queue: mark(&self.queue_path()),
surfaces: mark(&self.log_path()),
commands: mark(&self.paths.channel("commands.jsonl")),
}
}
pub fn queue(&self) -> Queue {
let Ok((queue, folded)) = self.current(|from| {
Ok::<_, std::convert::Infallible>(crate::ledger::read_records_from(
&self.log_path(),
from,
))
});
if folded {
let _ = self.write_queue(&queue);
}
queue
}
fn current<E>(
&self,
tail: impl FnOnce(u64) -> Result<Vec<crate::ledger::Record>, E>,
) -> Result<(Queue, bool), E> {
let checkpoint: Option<Queue> =
crate::ledger::read_json_opt(&self.queue_path()).filter(Queue::is_intact);
let mut floor: Option<u64> = None;
let (mut queue, from) = match checkpoint {
Some(queue) => match queue.accounted {
Some(accounted) => (queue, accounted),
None => {
floor = Some(queue.next_id);
(queue, 0)
}
},
None => (Queue::default(), 0),
};
let length = mark(&self.log_path()).map_or(0, |(length, _)| length);
let replaced = length < from;
let from = if replaced {
queue = Queue::default();
0
} else {
from
};
let mut accounted = from;
let mut folded = replaced || floor.is_some();
let records = if length > from {
tail(from)?
} else {
Vec::new()
};
for record in records {
if !record.terminated {
break;
}
accounted = record.offset + record.bytes + 1;
folded = true;
if let Ok(record) = serde_json::from_str::<SurfaceRecord>(&record.text) {
if floor.is_some_and(|floor| record.surface.id < floor) {
continue;
}
queue.apply(&record);
}
}
queue.accounted = Some(accounted);
queue.seal();
Ok((queue, folded))
}
fn write_queue(&self, queue: &Queue) -> crate::Result<()> {
crate::ledger::write_json(&self.queue_path(), queue)
}
fn record(
&self,
derive: impl FnOnce(&Queue) -> crate::Result<Vec<(SurfaceEvent, Surface)>>,
) -> crate::Result<Vec<Surface>> {
let mut log = crate::ledger::Appender::open(&self.log_path())?;
let (mut queue, _) = self.current(|from| log.records_from(from))?;
let mut recorded = Vec::new();
for (event, surface) in derive(&queue)? {
let record = SurfaceRecord {
event: Some(event),
surface,
};
log.append(
&serde_json::to_string(&record)
.map_err(|e| crate::Error::Invalid(format!("surface: {e}")))?,
)?;
queue.apply(&record);
recorded.push(record.surface);
}
queue.accounted = Some(log.len()?);
queue.seal();
self.write_queue(&queue)?;
Ok(recorded)
}
pub fn push(&self, mut surface: Surface) -> crate::Result<Surface> {
let mut queued = self.record(|queue| {
if queue.next_id.checked_add(1).is_none() {
return Err(crate::Error::Refused(format!(
"surface: the channel has no id left to allocate; the last one, \
{}, has already been queued",
queue.next_id - 1
)));
}
surface.id = queue.next_id;
Ok(vec![(SurfaceEvent::Queued, surface)])
})?;
queued
.pop()
.ok_or_else(|| crate::Error::Invalid("surface: nothing was queued".to_owned()))
}
pub fn claim(&self) -> crate::Result<Option<Surface>> {
let mut claimed = self.record(|queue| {
let next = queue
.waiting
.iter()
.position(|surface| surface.blocking && !surface.abandoned)
.or_else(|| queue.waiting.iter().position(|surface| !surface.abandoned))
.unwrap_or(0);
Ok(queue
.waiting
.get(next)
.map(|surface| vec![(SurfaceEvent::Claimed, surface.clone())])
.unwrap_or_default())
})?;
Ok(claimed.pop())
}
pub fn abandon(&self, raised: &[u64]) -> crate::Result<Vec<Surface>> {
self.record(|queue| {
Ok(queue
.waiting
.iter()
.chain(queue.pending.iter())
.filter(|surface| raised.contains(&surface.id) && !surface.abandoned)
.map(|surface| {
(
SurfaceEvent::Abandoned,
Surface {
abandoned: true,
..surface.clone()
},
)
})
.collect())
})
}
pub fn attend(&self, asker: &Asker) -> crate::Result<Vec<Surface>> {
self.record(|queue| {
Ok(queue
.waiting
.iter()
.chain(queue.pending.iter())
.filter(|surface| surface.abandoned && surface.asker.as_ref() == Some(asker))
.map(|surface| {
(
SurfaceEvent::Attended,
Surface {
abandoned: false,
..surface.clone()
},
)
})
.collect())
})
}
pub fn pending(&self) -> Option<Surface> {
self.held().filter(|surface| !surface.abandoned)
}
pub fn held(&self) -> Option<Surface> {
self.queue().pending
}
pub fn answer(&self, reply: &Reply) -> crate::Result<u64> {
self.record(|queue| {
Ok(queue
.pending
.iter()
.map(|held| (SurfaceEvent::Answered, held.clone()))
.collect())
})?;
let path = self.paths.channel("replies.jsonl");
let id = crate::ledger::read_lines(&path).len() as u64;
let queued = QueuedReply {
id,
reply: reply.clone(),
at: crate::sys::now_millis(),
};
crate::ledger::append_line(
&path,
&serde_json::to_string(&queued)
.map_err(|e| crate::Error::Invalid(format!("reply: {e}")))?,
)?;
Ok(id)
}
pub fn answer_if_verdict(&self, reply: &Reply) -> crate::Result<()> {
if reply.carries_verdict() {
self.answer(reply)?;
}
Ok(())
}
pub fn replies(&self) -> Vec<QueuedReply> {
crate::ledger::read_lines(&self.paths.channel("replies.jsonl"))
.iter()
.filter_map(|line| serde_json::from_str(line).ok())
.collect()
}
pub fn claim_reply(&self) -> crate::Result<Option<QueuedReply>> {
let cursor_path = self.paths.channel("replies-cursor.json");
let claimed_through: u64 = crate::ledger::read_json_opt(&cursor_path).unwrap_or(0);
let claimed = self.replies().into_iter().find(|queued| {
queued.id >= claimed_through && !queued.reply.carries_edits_without_a_verdict()
});
if let Some(claimed) = &claimed {
crate::ledger::write_json(&cursor_path, &(claimed.id + 1))?;
}
Ok(claimed)
}
pub fn submit(&self, author: Author, commands: &[Command]) -> crate::Result<u64> {
let path = self.paths.channel("commands.jsonl");
let id = crate::ledger::read_lines(&path).len() as u64;
let queued = QueuedCommands {
id,
author,
commands: commands.to_vec(),
};
crate::ledger::append_line(
&path,
&serde_json::to_string(&queued)
.map_err(|e| crate::Error::Invalid(format!("commands: {e}")))?,
)?;
Ok(id)
}
pub(crate) fn claimable_commands(&self) -> Vec<QueuedCommands> {
let claimed_through: u64 =
crate::ledger::read_json_opt(&self.paths.channel("commands-cursor.json")).unwrap_or(0);
crate::ledger::read_lines(&self.paths.channel("commands.jsonl"))
.iter()
.filter_map(|line| serde_json::from_str::<QueuedCommands>(line).ok())
.filter(|queued| queued.id >= claimed_through)
.collect()
}
pub fn claim_commands(&self) -> crate::Result<Vec<QueuedCommands>> {
let cursor_path = self.paths.channel("commands-cursor.json");
let claimed_through: u64 = crate::ledger::read_json_opt(&cursor_path).unwrap_or(0);
let fresh: Vec<QueuedCommands> =
crate::ledger::read_lines(&self.paths.channel("commands.jsonl"))
.iter()
.filter_map(|line| serde_json::from_str::<QueuedCommands>(line).ok())
.filter(|queued| queued.id >= claimed_through)
.collect();
if let Some(last) = fresh.last() {
crate::ledger::write_json(&cursor_path, &(last.id + 1))?;
}
Ok(fresh)
}
pub fn answer_commands(&self, outcome: &CommandOutcome) -> crate::Result<()> {
crate::ledger::append_line(
&self.paths.channel("command-outcomes.jsonl"),
&serde_json::to_string(outcome)
.map_err(|e| crate::Error::Invalid(format!("outcome: {e}")))?,
)
}
pub fn outcome_of(&self, id: u64) -> Option<CommandOutcome> {
crate::ledger::read_lines(&self.paths.channel("command-outcomes.jsonl"))
.iter()
.filter_map(|line| serde_json::from_str::<CommandOutcome>(line).ok())
.find(|outcome| outcome.id == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const ENVELOPE_GOLDEN: &str = include_str!("../tests/golden/reply-envelope-v3.json");
const ENVELOPE_GOLDEN_BEFORE: &str = include_str!("../tests/golden/reply-envelope-v2.json");
fn envelope_golden() -> Reply {
let settled = |id: &str, outcome: SettleOutcome, evidence: &str, landing: Option<&str>| {
Command::Settle {
id: id.to_owned(),
outcome,
evidence: evidence.to_owned(),
landing: landing.map(str::to_owned),
}
};
Reply {
version: Some(REPLY_ENVELOPE_VERSION),
author: Author::Planner,
commands: vec![
settled(
"publish",
SettleOutcome::Done,
"the change merged while the dispatch was dying; the run recorded the \
death and never the merge",
Some("https://github.com/owner/engine/pull/12"),
),
settled(
"release",
SettleOutcome::Done,
"the operator read the merge on the base branch",
Some("3f9a1c2e5b7d9081f2a3b4c5d6e7f8091a2b3c4d"),
),
settled(
"announce",
SettleOutcome::Failed,
"the wait it was on can never clear, and nothing published",
None,
),
],
..Reply::default()
}
}
#[test]
fn the_reply_envelope_is_the_shape_the_golden_pins() {
let rendered =
serde_json::to_string_pretty(&envelope_golden()).expect("the envelope serialises");
assert_eq!(
rendered.trim(),
ENVELOPE_GOLDEN.trim(),
"the reply envelope changed shape. Bump REPLY_ENVELOPE_VERSION, add the golden \
for the new version beside this one, keep this one as the version the build goes \
on reading, and say so in entry 57"
);
}
#[test]
fn an_envelope_at_the_version_before_this_one_is_still_read() {
let before: Reply =
serde_json::from_str(ENVELOPE_GOLDEN_BEFORE).expect("the older envelope still reads");
let declared: Value =
serde_json::from_str(ENVELOPE_GOLDEN_BEFORE).expect("the golden is JSON");
assert_eq!(
declared["version"],
json!(2),
"the golden for the version before this one is not at that version"
);
assert_eq!(
before.version,
Some(REPLY_ENVELOPE_VERSION),
"an envelope at a version this build reads was not read at the version it reads"
);
assert!(
REPLY_ENVELOPE_VERSIONS_READ.contains(&2),
"the version that golden was written against is no longer read"
);
assert_eq!(
before.commands.len(),
2,
"the older envelope's commands did not survive: {before:?}"
);
for command in &before.commands {
let Command::Settle { landing, .. } = command else {
panic!("the older envelope carries something other than a settle: {command:?}");
};
assert_eq!(
landing.as_deref(),
None,
"a settle written before the landing existed came back carrying one"
);
}
let ancient: Reply = serde_json::from_value(json!({
"version": 1,
"commands": [{"op": "cancel", "id": "build"}],
}))
.expect("an unreadable version is not a parse failure");
assert_eq!(
ancient.version,
Some(1),
"a version this build does not read was carried forward as one it does"
);
}
#[test]
fn a_settled_landing_round_trips_at_both_spellings_and_is_omitted_where_there_is_none() {
let envelope = envelope_golden();
let read: Reply =
serde_json::from_str(ENVELOPE_GOLDEN).expect("the golden reads back into the types");
assert_eq!(read, envelope, "the golden is not the envelope it pins");
let again: Reply =
serde_json::from_str(&serde_json::to_string(&envelope).expect("it serialises"))
.expect("it reads back");
assert_eq!(again, envelope, "the envelope does not round-trip");
let document: Value =
serde_json::from_str(&serde_json::to_string(&envelope).expect("it serialises"))
.expect("it is JSON");
assert_eq!(
document["commands"][0]["landing"],
json!("https://github.com/owner/engine/pull/12"),
"the change-request spelling of a landing did not survive the wire"
);
assert_eq!(
document["commands"][1]["landing"],
json!("3f9a1c2e5b7d9081f2a3b4c5d6e7f8091a2b3c4d"),
"the commit spelling of a landing did not survive the wire"
);
assert!(
document["commands"][2].get("landing").is_none(),
"a settle that named no landing carries a landing key anyway: {}",
document["commands"][2]
);
let bare = json!({
"version": REPLY_ENVELOPE_VERSION,
"commands": [{
"op": "settle", "id": "announce", "outcome": "failed",
"evidence": "the wait it was on can never clear, and nothing published",
}],
});
let before: Reply = serde_json::from_value(bare.clone()).expect("it parses");
assert_eq!(
serde_json::to_value(&before).expect("it serialises"),
bare,
"an envelope written before the landing existed did not round-trip unchanged"
);
}
fn surface(id: u64, blocking: bool) -> Surface {
Surface {
id,
kind: "finding".to_owned(),
message: "something happened".to_owned(),
source: source::PROPOSAL.to_owned(),
blocking,
queued_at: 0,
abandoned: false,
asker: None,
workstream: Some("ship".to_owned()),
}
}
#[test]
fn every_queue_change_the_loop_reads_shows_in_its_length() {
let root =
std::env::temp_dir().join(format!("onepipeline-queuemark-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "marks");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let outstanding = |channel: &ChannelState| -> Vec<u64> {
let queue = channel.queue();
queue
.waiting
.iter()
.chain(queue.pending.iter())
.filter(|surface| surface.blocking && !surface.abandoned)
.map(|surface| surface.id)
.collect()
};
let length = |channel: &ChannelState| -> Option<u64> {
mark(&channel.queue_path()).map(|(bytes, _)| bytes)
};
let empty = length(&channel);
let pushed = channel.push(surface(0, true)).expect("a surface is queued");
let queued = length(&channel);
assert_ne!(empty, queued, "a queued surface did not change the length");
assert_eq!(outstanding(&channel), vec![pushed.id]);
let claimed = channel.claim().expect("the surface is claimed");
assert!(claimed.is_some());
assert_eq!(
outstanding(&channel),
vec![pushed.id],
"a claimed blocking surface stopped being outstanding"
);
channel
.answer(&Reply {
completion: None,
commands: Vec::new(),
..Reply::default()
})
.expect("the surface is answered");
assert_ne!(
length(&channel),
queued,
"an answered surface did not change the length"
);
assert_eq!(outstanding(&channel), Vec::<u64>::new());
let second = channel.push(surface(0, true)).expect("a surface is queued");
let waiting = length(&channel);
assert_eq!(outstanding(&channel), vec![second.id]);
let marked = channel
.abandon(&[second.id])
.expect("the surface is marked");
assert_eq!(marked.len(), 1, "{marked:?}");
assert!(marked[0].abandoned);
assert_ne!(
length(&channel),
waiting,
"an abandoned surface did not change the length"
);
assert_eq!(outstanding(&channel), Vec::<u64>::new());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn abandoning_keeps_every_surface_readable_and_leaves_the_slot_holding_its_own() {
let root = std::env::temp_dir().join(format!("onepipeline-abandon-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "gone");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let read = channel.push(surface(0, true)).expect("the question queues");
let unread = channel
.push(Surface {
message: "nobody has seen this".to_owned(),
..surface(0, false)
})
.expect("the narration queues");
channel.claim().expect("a claim").expect("a surface");
assert_eq!(channel.pending().map(|held| held.id), Some(read.id));
let marked = channel
.abandon(&[read.id, unread.id])
.expect("both are marked");
assert_eq!(marked.len(), 2, "{marked:?}");
assert!(marked.iter().all(|surface| surface.abandoned));
assert_eq!(channel.pending(), None);
assert_eq!(channel.held().map(|held| held.id), Some(read.id));
assert!(channel.held().is_some_and(|held| held.abandoned));
let queue = channel.queue();
assert_eq!(queue.waiting.len(), 1, "{queue:?}");
let mut messages: Vec<String> = queue
.waiting
.iter()
.chain(queue.pending.iter())
.map(|surface| surface.message.clone())
.collect();
messages.sort();
assert_eq!(messages, vec!["nobody has seen this", "something happened"]);
let first = channel.claim().expect("a claim").expect("a surface");
assert_eq!(first.id, unread.id);
assert!(first.abandoned);
assert_eq!(channel.pending(), None);
assert_eq!(channel.claim().expect("a claim"), None);
let logged: Vec<Surface> = crate::ledger::read_lines(&paths.channel("surfaces.jsonl"))
.iter()
.filter_map(|line| serde_json::from_str(line).ok())
.collect();
for id in [read.id, unread.id] {
assert!(
logged
.iter()
.any(|surface| surface.id == id && surface.abandoned),
"no record that surface {id} was abandoned: {logged:?}"
);
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_recorded_asker_that_names_nobody_reads_as_nobody() {
let raised = Surface {
asker: Some(Asker::checked("dispatch-a").expect("a name")),
..surface(7, true)
};
let written = serde_json::to_string(&raised).expect("the surface writes");
assert!(written.contains(r#""asker":"dispatch-a""#), "{written}");
let read: Surface = serde_json::from_str(&written).expect("the surface reads back");
assert_eq!(read.asker, raised.asker);
for recorded in [r##","asker":"""##, ""] {
let line = written.replace(r#","asker":"dispatch-a""#, recorded);
let read: Surface = serde_json::from_str(&line)
.unwrap_or_else(|e| panic!("a queue recording {recorded:?} was lost: {e}"));
assert_eq!(read.asker, None, "{line}");
assert_eq!(read.message, raised.message);
}
}
#[test]
fn a_live_question_takes_the_slot_and_the_abandoned_one_it_displaces_stays_readable() {
let root = std::env::temp_dir().join(format!("onepipeline-displace-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "displaced");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let gone = channel.push(surface(0, true)).expect("the question queues");
channel.claim().expect("a claim").expect("a surface");
channel.abandon(&[gone.id]).expect("it is marked");
assert_eq!(channel.held().map(|held| held.id), Some(gone.id));
let live = channel
.push(Surface {
message: "somebody is waiting on this".to_owned(),
..surface(0, true)
})
.expect("the live question queues");
let claimed = channel.claim().expect("a claim").expect("a surface");
assert_eq!(claimed.id, live.id);
assert_eq!(channel.pending().map(|held| held.id), Some(live.id));
let queue = channel.queue();
assert_eq!(
queue
.waiting
.iter()
.map(|surface| (surface.id, surface.message.as_str()))
.collect::<Vec<_>>(),
vec![(gone.id, "something happened")],
"{queue:?}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn attending_takes_back_one_askers_surfaces_and_leaves_every_other_alone() {
let root = std::env::temp_dir().join(format!("onepipeline-attend-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "back");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let asked = |asker: Option<&str>, blocking: bool, message: &str| Surface {
message: message.to_owned(),
asker: asker.map(|name| Asker::checked(name).expect("a name")),
..surface(0, blocking)
};
let mine = channel
.push(asked(Some("dispatch-a"), true, "is this base still right?"))
.expect("the question queues");
let theirs = channel
.push(asked(Some("dispatch-b"), true, "somebody else's question"))
.expect("their question queues");
let nobodys = channel
.push(asked(None, false, "raised by no session"))
.expect("the narration queues");
channel.claim().expect("a claim").expect("a surface");
assert_eq!(channel.pending().map(|held| held.id), Some(mine.id));
channel
.abandon(&[mine.id, theirs.id, nobodys.id])
.expect("all three are marked");
assert_eq!(channel.pending(), None);
let named = |name: &str| Asker::checked(name).expect("a name");
let taken = channel
.attend(&named("dispatch-a"))
.expect("mine comes back");
assert_eq!(
taken.iter().map(|surface| surface.id).collect::<Vec<_>>(),
vec![mine.id]
);
assert_eq!(channel.pending().map(|held| held.id), Some(mine.id));
let queue = channel.queue();
assert!(
queue
.waiting
.iter()
.all(|surface| surface.abandoned && surface.id != mine.id),
"attending took a surface belonging to another asker: {queue:?}"
);
let lines = crate::ledger::read_lines(&paths.channel("surfaces.jsonl")).len();
assert!(channel
.attend(&named("dispatch-a"))
.expect("nothing")
.is_empty());
assert!(channel
.attend(&named("dispatch-c"))
.expect("nothing")
.is_empty());
assert_eq!(
crate::ledger::read_lines(&paths.channel("surfaces.jsonl")).len(),
lines,
"attending what was already attended wrote a second record"
);
let logged: Vec<Surface> = crate::ledger::read_lines(&paths.channel("surfaces.jsonl"))
.iter()
.filter_map(|line| serde_json::from_str(line).ok())
.collect();
assert!(
logged.iter().any(|surface| surface.id == mine.id
&& !surface.abandoned
&& surface.asker.is_some()),
"no record that surface {} was taken back: {logged:?}",
mine.id
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_surface_queued_during_a_read_of_the_channel_is_neither_lost_nor_given_a_used_id() {
const WRITERS: usize = 4;
const READERS: usize = 3;
const EACH: usize = 40;
let root = std::env::temp_dir().join(format!("onepipeline-raced-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "raced");
paths.create().expect("the run directory");
let claimed = std::sync::Arc::new(std::sync::Mutex::new(Vec::<Surface>::new()));
let queued = std::sync::Arc::new(std::sync::Mutex::new(Vec::<Surface>::new()));
let writing = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let writers: Vec<_> = (0..WRITERS)
.map(|writer| {
let channel = ChannelState::new(&paths);
let queued = std::sync::Arc::clone(&queued);
std::thread::spawn(move || {
for n in 0..EACH {
let pushed = channel
.push(Surface {
message: format!("writer {writer} question {n}"),
..surface(0, n % 2 == 0)
})
.expect("a surface is queued");
queued.lock().expect("the list").push(pushed);
}
})
})
.collect();
let readers: Vec<_> = (0..READERS)
.map(|_| {
let channel = ChannelState::new(&paths);
let claimed = std::sync::Arc::clone(&claimed);
let writing = std::sync::Arc::clone(&writing);
std::thread::spawn(move || loop {
if let Some(surface) = channel.claim().expect("a claim") {
claimed.lock().expect("the list").push(surface);
continue;
}
if !writing.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
std::thread::yield_now();
})
})
.collect();
let written: Vec<_> = writers.into_iter().map(|writer| writer.join()).collect();
writing.store(false, std::sync::atomic::Ordering::SeqCst);
let read_out: Vec<_> = readers.into_iter().map(|reader| reader.join()).collect();
for writer in written {
writer.expect("a writer finishes");
}
for reader in read_out {
reader.expect("a reader finishes");
}
let queued = queued.lock().expect("the list").clone();
let claimed = claimed.lock().expect("the list").clone();
let mut ids: Vec<u64> = queued.iter().map(|surface| surface.id).collect();
ids.sort_unstable();
assert_eq!(
ids,
(0..(WRITERS * EACH) as u64).collect::<Vec<_>>(),
"an id was handed out twice or skipped"
);
let mut read: Vec<(u64, String)> = claimed
.iter()
.map(|surface| (surface.id, surface.message.clone()))
.collect();
read.sort();
let mut sent: Vec<(u64, String)> = queued
.iter()
.map(|surface| (surface.id, surface.message.clone()))
.collect();
sent.sort();
assert_eq!(read, sent, "a surface was lost or delivered twice");
let queue = ChannelState::new(&paths).queue();
assert!(queue.waiting.is_empty(), "{queue:?}");
assert_eq!(queue.next_id, (WRITERS * EACH) as u64);
let records: Vec<SurfaceRecord> =
crate::ledger::read_lines(&paths.channel("surfaces.jsonl"))
.iter()
.map(|line| serde_json::from_str(line).expect("a record this build wrote"))
.collect();
for id in 0..(WRITERS * EACH) as u64 {
for event in [SurfaceEvent::Queued, SurfaceEvent::Claimed] {
assert_eq!(
records
.iter()
.filter(|record| record.surface.id == id && record.event == Some(event))
.count(),
1,
"surface {id} is not recorded {event:?} exactly once"
);
}
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn abandoning_what_is_already_abandoned_records_nothing_further() {
let root =
std::env::temp_dir().join(format!("onepipeline-reabandon-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "twice");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let queued = channel.push(surface(0, true)).expect("the surface queues");
assert_eq!(channel.abandon(&[queued.id]).expect("marked").len(), 1);
let lines = crate::ledger::read_lines(&paths.channel("surfaces.jsonl")).len();
assert!(channel.abandon(&[queued.id]).expect("nothing").is_empty());
assert!(channel.abandon(&[]).expect("nothing").is_empty());
assert_eq!(
crate::ledger::read_lines(&paths.channel("surfaces.jsonl")).len(),
lines,
"abandoning the same surface twice wrote a second record"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_queue_is_rebuilt_from_the_log_alone_whatever_became_of_the_projection() {
let root = std::env::temp_dir().join(format!("onepipeline-rebuilt-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "rebuilt");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let queue_path = channel.queue_path();
let named = |name: &str| Asker::checked(name).expect("a name");
let mut earlier = std::fs::read(&queue_path).ok();
let lost_six_ways = |channel: &ChannelState, earlier: &Option<Vec<u8>>, what: &str| {
let expected = channel.queue();
let stamped = expected
.accounted
.expect("this build stamps what it writes");
let current = std::fs::read(&queue_path).expect("the projection was written");
let claims_moved = |edit: &dyn Fn(&mut serde_json::Value)| -> Vec<u8> {
let mut document: serde_json::Value =
serde_json::from_slice(¤t).expect("the projection is JSON");
edit(&mut document);
serde_json::to_vec(&document).expect("the edited document")
};
let placed: Vec<(&str, Option<Vec<u8>>)> = vec![
("deleted", None),
(
"replaced with something that is not a queue",
Some(b"{\"waiting\": ".to_vec()),
),
("overwritten with a stale copy", earlier.clone()),
(
"stamped but with its waiting surfaces emptied",
Some(claims_moved(&|document| {
document["waiting"] = serde_json::json!([]);
document["pending"] = serde_json::Value::Null;
})),
),
(
"stamped but with its counter reset",
Some(claims_moved(&|document| {
document["next_id"] = serde_json::json!(0);
})),
),
(
"stamped but with no seal",
Some(claims_moved(&|document| {
document["waiting"] = serde_json::json!([]);
document.as_object_mut().expect("an object").remove("seal");
})),
),
];
for (how, bytes) in placed {
match bytes {
Some(bytes) => {
std::fs::write(&queue_path, bytes).expect("the projection is placed")
}
None => std::fs::remove_file(&queue_path).expect("the projection is removed"),
}
let rebuilt = channel.queue();
assert_eq!(rebuilt, expected, "after {what}, with the projection {how}");
assert_eq!(
rebuilt.accounted,
Some(stamped),
"after {what}, with the projection {how}: the stamp moved"
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(
&std::fs::read(&queue_path).expect("the projection is written back")
)
.expect("a document"),
serde_json::from_slice::<serde_json::Value>(¤t).expect("a document"),
"after {what}, with the projection {how}: the repair was not written back"
);
}
};
let mut step = |what: &str, act: &dyn Fn(&ChannelState)| {
act(&channel);
lost_six_ways(&channel, &earlier, what);
earlier = std::fs::read(&queue_path).ok();
};
step("a question is queued", &|channel| {
channel
.push(Surface {
asker: Some(named("dispatch-a")),
..surface(0, true)
})
.expect("queued");
});
step("narration is queued behind it", &|channel| {
channel.push(surface(0, false)).expect("queued");
});
step("a check-in is queued", &|channel| {
channel
.push(Surface {
source: source::CHECK_IN.to_owned(),
message: "first".to_owned(),
..surface(0, false)
})
.expect("queued");
});
step("a second check-in replaces it", &|channel| {
channel
.push(Surface {
source: source::CHECK_IN.to_owned(),
message: "second".to_owned(),
..surface(0, false)
})
.expect("queued");
let queue = channel.queue();
assert_eq!(
queue
.waiting
.iter()
.filter(|surface| surface.source == source::CHECK_IN)
.map(|surface| surface.message.as_str())
.collect::<Vec<_>>(),
vec!["second"]
);
});
step("the question is claimed", &|channel| {
let claimed = channel.claim().expect("a claim").expect("a surface");
assert_eq!(claimed.id, 0);
assert_eq!(channel.pending().map(|held| held.id), Some(0));
});
step("its listener leaves", &|channel| {
assert_eq!(channel.abandon(&[0, 1]).expect("marked").len(), 2);
assert_eq!(channel.pending(), None);
});
step("its asker comes back", &|channel| {
assert_eq!(
channel.attend(&named("dispatch-a")).expect("taken").len(),
1
);
assert_eq!(channel.pending().map(|held| held.id), Some(0));
});
step("it is answered", &|channel| {
channel.answer(&Reply::default()).expect("answered");
assert_eq!(channel.held(), None);
});
step("everything left is read", &|channel| {
while channel.claim().expect("a claim").is_some() {}
let queue = channel.queue();
assert!(queue.waiting.is_empty(), "{queue:?}");
assert_eq!(queue.next_id, 4);
});
let log = paths.channel("surfaces.jsonl");
let settled = channel.queue();
let fragment = serde_json::to_string(&SurfaceRecord {
event: Some(SurfaceEvent::Queued),
surface: surface(4, true),
})
.expect("a record");
let (head, tail) = fragment.split_at(fragment.len() / 2);
std::fs::OpenOptions::new()
.append(true)
.open(&log)
.and_then(|mut file| std::io::Write::write_all(&mut file, head.as_bytes()))
.expect("the fragment is written");
let torn = channel.queue();
assert_eq!(torn, settled, "a torn record was folded");
std::fs::OpenOptions::new()
.append(true)
.open(&log)
.and_then(|mut file| {
std::io::Write::write_all(&mut file, format!("{tail}\nnot a record\n").as_bytes())
})
.expect("the record is finished");
let whole = channel.queue();
assert_eq!(
whole
.waiting
.iter()
.map(|surface| surface.id)
.collect::<Vec<_>>(),
vec![4],
"{whole:?}"
);
assert_eq!(
whole.accounted,
Some(std::fs::metadata(&log).expect("the log").len()),
"the unreadable line was not accounted for"
);
assert_eq!(channel.queue(), whole, "a settled log was folded again");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_log_and_a_projection_an_older_build_wrote_are_read_as_that_build_meant_them() {
let root = std::env::temp_dir().join(format!("onepipeline-legacy-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "legacy");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
let asked = Surface {
asker: Some(Asker::checked("dispatch-a").expect("a name")),
..surface(0, true)
};
let gone = Surface {
abandoned: true,
..asked.clone()
};
let narration = surface(1, false);
for line in [&asked, &narration, &gone, &asked] {
crate::ledger::append_line(
&paths.channel("surfaces.jsonl"),
&serde_json::to_string(line).expect("a line"),
)
.expect("the older build's line");
}
let rebuilt = channel.queue();
assert_eq!(
rebuilt
.waiting
.iter()
.map(|surface| (surface.id, surface.abandoned))
.collect::<Vec<_>>(),
vec![(0, false), (1, false)],
"{rebuilt:?}"
);
assert_eq!(rebuilt.next_id, 2);
assert_eq!(rebuilt.waiting[0].asker, asked.asker);
crate::ledger::write_json(
&channel.queue_path(),
&serde_json::json!({"waiting": [narration], "pending": null, "next_id": 2}),
)
.expect("the older build's projection");
let trusted = channel.queue();
assert_eq!(
trusted
.waiting
.iter()
.map(|surface| surface.id)
.collect::<Vec<_>>(),
vec![1]
);
assert_eq!(trusted.next_id, 2);
assert!(trusted.accounted.is_some(), "{trusted:?}");
let claimed = channel.claim().expect("a claim").expect("a surface");
assert_eq!(claimed.id, 1);
let stamped = channel.queue();
assert!(stamped.waiting.is_empty(), "{stamped:?}");
assert!(stamped.accounted.is_some(), "{stamped:?}");
crate::ledger::write_json(
&channel.queue_path(),
&serde_json::json!({"waiting": [], "pending": null, "next_id": 0}),
)
.expect("the older build's projection");
let restored = channel.queue();
assert_eq!(
restored
.waiting
.iter()
.map(|surface| (surface.id, surface.abandoned))
.collect::<Vec<_>>(),
vec![(0, false)],
"{restored:?}"
);
assert_eq!(restored.next_id, 2);
let fresh = channel.push(surface(0, false)).expect("queued");
assert_eq!(
fresh.id, 2,
"an id the log had allocated was handed out again"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_tail_the_fold_cannot_read_refuses_the_fold_and_stamps_nothing() {
let root =
std::env::temp_dir().join(format!("onepipeline-unreadtail-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "unreadtail");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
channel.push(surface(0, true)).expect("queued");
let written = std::fs::read(channel.queue_path()).expect("the projection");
crate::ledger::append_line(
&paths.channel("surfaces.jsonl"),
&serde_json::to_string(&SurfaceRecord {
event: Some(SurfaceEvent::Queued),
surface: surface(1, false),
})
.expect("a record"),
)
.expect("the log grows");
let refused = channel.current(|from| {
Err(crate::Error::Refused(format!(
"the tail from byte {from} cannot be read"
)))
});
assert!(
matches!(&refused, Err(crate::Error::Refused(why)) if why.contains("cannot be read")),
"{refused:?}"
);
assert_eq!(
std::fs::read(channel.queue_path()).expect("the projection"),
written,
"a refused fold moved the projection"
);
let queue = channel.queue();
assert_eq!(
queue.waiting.iter().map(|s| s.id).collect::<Vec<_>>(),
vec![0, 1]
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_record_appended_without_its_projection_still_moves_the_fingerprint() {
let root = std::env::temp_dir().join(format!("onepipeline-logmark-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "logmark");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
channel.push(surface(0, false)).expect("queued");
let projected = std::fs::read(channel.queue_path()).expect("the projection");
let seen = channel.fingerprint();
crate::ledger::append_line(
&channel.log_path(),
&serde_json::to_string(&SurfaceRecord {
event: Some(SurfaceEvent::Queued),
surface: surface(1, true),
})
.expect("a record"),
)
.expect("the log grows");
assert_eq!(
std::fs::read(channel.queue_path()).expect("the projection"),
projected,
"the append moved the projection, so this proves nothing about the log's mark"
);
assert_ne!(
channel.fingerprint(),
seen,
"a record appended without its projection left the fingerprint where it was"
);
let queue = channel.queue();
assert_eq!(
queue.waiting.iter().map(|s| s.id).collect::<Vec<_>>(),
vec![0, 1]
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_unchanged_channel_is_answered_without_reading_the_log() {
let root = std::env::temp_dir().join(format!("onepipeline-oneread-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = crate::ledger::RunPaths::under(&root, "oneread");
paths.create().expect("the run directory");
let channel = ChannelState::new(&paths);
channel.push(surface(0, true)).expect("queued");
channel.push(surface(0, false)).expect("queued");
channel.claim().expect("a claim").expect("a surface");
let projection = std::fs::metadata(channel.queue_path())
.expect("the projection")
.len();
let log = std::fs::metadata(paths.channel("surfaces.jsonl"))
.expect("the log")
.len();
assert!(log > 0);
let before = crate::ledger::bytes_read();
let queue = channel.queue();
let cost = crate::ledger::bytes_read() - before;
assert_eq!(
cost, projection,
"an unchanged channel cost {cost} byte(s) to read against a {projection}-byte \
projection and a {log}-byte log: {queue:?}"
);
let Ok((same, folded)) = channel.current(|from| -> Result<_, std::convert::Infallible> {
panic!("an unchanged channel read its log from byte {from}");
});
assert_eq!(same, queue);
assert!(!folded);
let _ = std::fs::remove_dir_all(&root);
}
}