use crate::__compat::wire::{DescribeWire, WireField, WireSchema};
use crate::identity::{ParticipantId, ProducerId};
use crate::participant::metadata::ParticipantKind;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(
phoxal_macros::DescribeWire, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum ProcessState {
Absent,
Present,
}
#[derive(phoxal_macros::DescribeWire, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Process {
pub participant: ParticipantId,
pub kind: ParticipantKind,
pub state: ProcessState,
pub producer: Option<ProducerId>,
}
#[derive(
phoxal_macros::DescribeWire, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Lifecycle {
Starting,
Ready,
Degraded,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Snapshot {
pub revision: u64,
pub lifecycle: Lifecycle,
pub processes: Vec<Process>,
}
impl Snapshot {
pub fn validate(&self) -> Result<(), SnapshotError> {
for (index, pair) in self.processes.windows(2).enumerate() {
match pair[0].participant.cmp(&pair[1].participant) {
std::cmp::Ordering::Less => {}
std::cmp::Ordering::Equal => {
return Err(SnapshotError::DuplicateParticipant { index: index + 1 });
}
std::cmp::Ordering::Greater => {
return Err(SnapshotError::UnorderedParticipants { index: index + 1 });
}
}
}
for process in &self.processes {
if process.producer.is_some() != (process.state == ProcessState::Present) {
return Err(SnapshotError::PresenceProducerMismatch {
participant: process.participant.clone(),
state: process.state,
});
}
}
Ok(())
}
}
impl<'de> Deserialize<'de> for Snapshot {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
revision: u64,
lifecycle: Lifecycle,
processes: Vec<Process>,
}
let wire = Wire::deserialize(deserializer)?;
let snapshot = Self {
revision: wire.revision,
lifecycle: wire.lifecycle,
processes: wire.processes,
};
snapshot.validate().map_err(serde::de::Error::custom)?;
Ok(snapshot)
}
}
impl Serialize for Snapshot {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.validate().map_err(serde::ser::Error::custom)?;
#[derive(Serialize)]
#[serde(deny_unknown_fields)]
struct Wire<'a> {
revision: u64,
lifecycle: Lifecycle,
processes: &'a [Process],
}
Wire {
revision: self.revision,
lifecycle: self.lifecycle,
processes: &self.processes,
}
.serialize(serializer)
}
}
impl DescribeWire for Snapshot {
fn wire_schema() -> WireSchema {
WireSchema::opaque(
"Snapshot",
WireSchema::structure([
WireField::required("revision", u64::wire_schema()),
WireField::required("lifecycle", Lifecycle::wire_schema()),
WireField::required("processes", <Vec<Process>>::wire_schema()),
]),
)
}
}
#[derive(phoxal_macros::DescribeWire, Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "schema")]
pub enum SnapshotDocument {
#[serde(rename = "phoxal/supervisor-snapshot/v0")]
V0(Snapshot),
}
impl SnapshotDocument {
#[must_use]
pub const fn snapshot(&self) -> &Snapshot {
match self {
Self::V0(snapshot) => snapshot,
}
}
#[must_use]
pub fn into_snapshot(self) -> Snapshot {
match self {
Self::V0(snapshot) => snapshot,
}
}
}
impl<'de> Deserialize<'de> for SnapshotDocument {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(tag = "schema")]
enum Wire {
#[serde(rename = "phoxal/supervisor-snapshot/v0")]
V0(Snapshot),
}
match Wire::deserialize(deserializer)? {
Wire::V0(snapshot) => Ok(Self::V0(snapshot)),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum SnapshotError {
#[error("snapshot repeats participant at index {index}")]
DuplicateParticipant { index: usize },
#[error("snapshot processes are unordered at index {index}")]
UnorderedParticipants { index: usize },
#[error("participant {participant} is {state:?} but its producer says otherwise")]
PresenceProducerMismatch {
participant: ParticipantId,
state: ProcessState,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn producer(seed: u128) -> ProducerId {
ProducerId::try_from((1_u128 << 124) | seed).expect("a canonical producer id")
}
fn absent(id: &str) -> Process {
Process {
participant: ParticipantId::new(id).expect("valid participant id"),
kind: ParticipantKind::Brain,
state: ProcessState::Absent,
producer: None,
}
}
fn present(id: &str, seed: u128) -> Process {
Process {
state: ProcessState::Present,
producer: Some(producer(seed)),
..absent(id)
}
}
fn snapshot(processes: Vec<Process>) -> Snapshot {
Snapshot {
revision: 1,
lifecycle: Lifecycle::Starting,
processes,
}
}
#[test]
fn process_rows_are_ordered_and_unique() {
assert_eq!(
snapshot(vec![absent("brain"), absent("drive")]).validate(),
Ok(())
);
assert_eq!(
snapshot(vec![absent("drive"), absent("brain")]).validate(),
Err(SnapshotError::UnorderedParticipants { index: 1 })
);
assert_eq!(
snapshot(vec![absent("drive"), absent("drive")]).validate(),
Err(SnapshotError::DuplicateParticipant { index: 1 })
);
}
#[test]
fn snapshot_document_round_trips_and_rejects_unknown_fields() {
let document = SnapshotDocument::V0(snapshot(vec![present("brain", 7)]));
let encoded = rmp_serde::to_vec_named(&document).expect("snapshot encodes");
assert_eq!(
rmp_serde::from_slice::<SnapshotDocument>(&encoded).expect("snapshot decodes"),
document
);
let malformed = rmp_serde::to_vec_named(&serde_json::json!({
"schema": "phoxal/supervisor-snapshot/v0",
"revision": 1,
"lifecycle": "starting",
"processes": [],
"extra": true
}))
.expect("malformed fixture encodes");
assert!(rmp_serde::from_slice::<SnapshotDocument>(&malformed).is_err());
}
#[test]
fn every_snapshot_relation_is_enforced_on_validation_and_encoding() {
let mut without_producer = present("brain", 3);
without_producer.producer = None;
let invalid = snapshot(vec![without_producer]);
assert!(matches!(
invalid.validate(),
Err(SnapshotError::PresenceProducerMismatch {
state: ProcessState::Present,
..
})
));
assert!(rmp_serde::to_vec_named(&invalid).is_err());
let mut absent_with_producer = absent("brain");
absent_with_producer.producer = Some(producer(4));
assert!(matches!(
snapshot(vec![absent_with_producer]).validate(),
Err(SnapshotError::PresenceProducerMismatch {
state: ProcessState::Absent,
..
})
));
}
#[test]
fn the_declared_snapshot_shape_is_the_shape_the_mirror_writes() {
let mut driver = present("drive", 9);
driver.kind = ParticipantKind::Driver;
let mut populated = snapshot(vec![absent("brain"), driver]);
populated.lifecycle = Lifecycle::Degraded;
let document = SnapshotDocument::V0(populated);
let json = serde_json::to_value(&document).expect("the snapshot document serializes");
assert_eq!(SnapshotDocument::wire_schema().conforms(&json), Ok(()));
assert_eq!(json["schema"], "phoxal/supervisor-snapshot/v0");
assert!(json["processes"].is_array());
}
}