laminar_core/checkpoint/
prepared_witness.rs1use super::{PipelineIdentity, PIPELINE_IDENTITY_VERSION};
4use crate::state::CheckpointAttempt;
5
6pub const MAX_PREPARED_CHECKPOINT_WITNESSES: usize = 64;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct PreparedCheckpointWitness {
19 pub attempt: CheckpointAttempt,
21 pub participant_id: u64,
23 pub deployment_id: String,
25 pub pipeline_identity: PipelineIdentity,
27}
28
29impl PreparedCheckpointWitness {
30 pub fn new(
35 attempt: CheckpointAttempt,
36 participant_id: u64,
37 deployment_id: String,
38 pipeline_identity: PipelineIdentity,
39 ) -> Result<Self, String> {
40 let witness = Self {
41 attempt,
42 participant_id,
43 deployment_id,
44 pipeline_identity,
45 };
46 witness.validate()?;
47 Ok(witness)
48 }
49
50 pub(crate) fn validate(&self) -> Result<(), String> {
55 if !self.attempt.is_canonical() {
56 return Err(
57 "prepared checkpoint attempt must use one nonzero canonical checkpoint ID".into(),
58 );
59 }
60 if self.participant_id == 0 {
61 return Err("prepared checkpoint participant must be nonzero".into());
62 }
63 let deployment = uuid::Uuid::parse_str(&self.deployment_id)
64 .map_err(|error| format!("prepared checkpoint deployment ID is invalid: {error}"))?;
65 if deployment.is_nil() || deployment.to_string() != self.deployment_id {
66 return Err(
67 "prepared checkpoint deployment ID must be a canonical non-nil UUID".into(),
68 );
69 }
70 if !self.pipeline_identity.is_canonical() {
71 return Err(format!(
72 "prepared checkpoint pipeline identity must use canonical version {PIPELINE_IDENTITY_VERSION} and a lowercase SHA-256 digest"
73 ));
74 }
75 Ok(())
76 }
77
78 #[must_use]
80 pub fn is_canonical(&self) -> bool {
81 self.validate().is_ok()
82 }
83
84 #[cfg(feature = "cluster")]
85 pub(crate) const fn ordering_key(&self) -> (u64, u64) {
86 (self.attempt.checkpoint_id, self.participant_id)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 fn witness() -> PreparedCheckpointWitness {
95 PreparedCheckpointWitness::new(
96 CheckpointAttempt::canonical(7),
97 3,
98 uuid::Uuid::from_u128(9).to_string(),
99 PipelineIdentity::empty(),
100 )
101 .unwrap()
102 }
103
104 #[test]
105 fn witness_rejects_zero_and_noncanonical_identities() {
106 let valid = witness();
107 assert!(valid.is_canonical());
108
109 let mut invalid = valid.clone();
110 invalid.attempt.epoch = 0;
111 assert!(!invalid.is_canonical());
112
113 let mut invalid = valid.clone();
114 invalid.attempt.checkpoint_id = 0;
115 assert!(!invalid.is_canonical());
116
117 let mut invalid = valid.clone();
118 invalid.attempt.checkpoint_id = invalid.attempt.epoch + 1;
119 assert!(!invalid.is_canonical());
120
121 let mut invalid = valid.clone();
122 invalid.participant_id = 0;
123 assert!(!invalid.is_canonical());
124
125 let mut invalid = valid.clone();
126 invalid.deployment_id = uuid::Uuid::nil().to_string();
127 assert!(!invalid.is_canonical());
128
129 let mut invalid = valid;
130 invalid.pipeline_identity.canonical_version = PIPELINE_IDENTITY_VERSION + 1;
131 assert!(!invalid.is_canonical());
132 }
133
134 #[test]
135 fn witness_wire_rejects_unknown_fields() {
136 let mut value = serde_json::to_value(witness()).unwrap();
137 value["unknown"] = serde_json::json!(true);
138 assert!(serde_json::from_value::<PreparedCheckpointWitness>(value).is_err());
139 }
140}