laminar_core/checkpoint/subscription/
distribution.rs1use super::{
2 OutputPartitionId, StreamGeneration, SubscriptionContractError, SubscriptionDigest,
3 SubscriptionProtocolVersion,
4};
5use crate::checkpoint::PipelineIdentity;
6use crate::state::{KeyGroupCount, PARTITIONING_ABI_VERSION};
7
8pub const OUTPUT_DISTRIBUTION_CERTIFICATE_VERSION: u16 = 1;
10
11const MAX_STREAM_ID_BYTES: usize = 256;
12const MAX_OPERATOR_ID_BYTES: usize = 512;
13
14#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16#[serde(rename_all = "snake_case", tag = "kind", deny_unknown_fields)]
17pub enum OutputDistribution {
18 VnodePartitioned {
20 key_expressions_fingerprint: SubscriptionDigest,
22 partition_abi: u16,
24 vnode_count: u16,
26 },
27 Singleton {
29 partition: OutputPartitionId,
31 },
32}
33
34impl OutputDistribution {
35 #[must_use]
37 pub const fn partition_count(&self) -> u16 {
38 match self {
39 Self::VnodePartitioned { vnode_count, .. } => *vnode_count,
40 Self::Singleton { .. } => 1,
41 }
42 }
43
44 pub(crate) fn validate(
45 &self,
46 key_group_count: KeyGroupCount,
47 ) -> Result<(), SubscriptionContractError> {
48 match self {
49 Self::VnodePartitioned {
50 key_expressions_fingerprint,
51 partition_abi,
52 vnode_count,
53 } => {
54 key_expressions_fingerprint.validate("key_expressions_fingerprint")?;
55 if *partition_abi != PARTITIONING_ABI_VERSION {
56 return Err(SubscriptionContractError::PartitionAbi {
57 actual: *partition_abi,
58 expected: PARTITIONING_ABI_VERSION,
59 });
60 }
61 if *vnode_count != key_group_count.get() {
62 return Err(SubscriptionContractError::VnodeCount {
63 actual: *vnode_count,
64 expected: key_group_count.get(),
65 });
66 }
67 }
68 Self::Singleton { partition } => {
69 partition.validate(key_group_count)?;
70 if partition.get() != 0 {
71 return Err(SubscriptionContractError::NonCanonicalSingleton {
72 partition: partition.get(),
73 });
74 }
75 }
76 }
77 Ok(())
78 }
79
80 #[must_use]
82 pub fn contains(&self, partition: OutputPartitionId) -> bool {
83 match self {
84 Self::VnodePartitioned { vnode_count, .. } => partition.get() < *vnode_count,
85 Self::Singleton {
86 partition: singleton,
87 } => partition == *singleton,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ChangelogMode {
96 Append,
98 FullPartitionSnapshot,
100 WeightedRetractInsert,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct OutputDistributionCertificate {
108 pub version: u16,
110 pub protocol_version: SubscriptionProtocolVersion,
112 pub stream_id: String,
114 pub catalog_generation: u64,
116 pub stream_generation: StreamGeneration,
118 pub final_operator_id: String,
120 pub distribution: OutputDistribution,
122 pub schema_fingerprint: SubscriptionDigest,
124 pub changelog_mode: ChangelogMode,
126 pub history_retention_bytes: u64,
128 pub query_fingerprint: SubscriptionDigest,
130 pub pipeline_identity: PipelineIdentity,
132}
133
134impl OutputDistributionCertificate {
135 pub fn validate(
140 &self,
141 key_group_count: KeyGroupCount,
142 ) -> Result<(), SubscriptionContractError> {
143 if self.version != OUTPUT_DISTRIBUTION_CERTIFICATE_VERSION {
144 return Err(SubscriptionContractError::DistributionCertificateVersion {
145 actual: self.version,
146 expected: OUTPUT_DISTRIBUTION_CERTIFICATE_VERSION,
147 });
148 }
149 self.protocol_version.validate()?;
150 validate_identity("stream_id", &self.stream_id, MAX_STREAM_ID_BYTES)?;
151 validate_identity(
152 "final_operator_id",
153 &self.final_operator_id,
154 MAX_OPERATOR_ID_BYTES,
155 )?;
156 if self.catalog_generation == 0 {
157 return Err(SubscriptionContractError::ZeroCatalogGeneration);
158 }
159 self.stream_generation.validate()?;
160 self.schema_fingerprint.validate("schema_fingerprint")?;
161 self.query_fingerprint.validate("query_fingerprint")?;
162 if !self.pipeline_identity.is_canonical() {
163 return Err(SubscriptionContractError::PipelineIdentity);
164 }
165 self.distribution.validate(key_group_count)
166 }
167
168 pub fn require_match(&self, expected: &Self) -> Result<(), SubscriptionContractError> {
173 if self != expected {
174 return Err(SubscriptionContractError::DistributionCertificateMismatch);
175 }
176 Ok(())
177 }
178
179 #[must_use]
181 pub fn certifies_keyed_aggregate(&self) -> bool {
182 matches!(
183 self.distribution,
184 OutputDistribution::VnodePartitioned { .. }
185 ) && self.changelog_mode == ChangelogMode::WeightedRetractInsert
186 }
187}
188
189fn validate_identity(
190 field: &'static str,
191 value: &str,
192 max_bytes: usize,
193) -> Result<(), SubscriptionContractError> {
194 if value.is_empty() || value.trim() != value || value.len() > max_bytes {
195 return Err(SubscriptionContractError::NonCanonicalIdentity { field, max_bytes });
196 }
197 Ok(())
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn certificate() -> OutputDistributionCertificate {
205 OutputDistributionCertificate {
206 version: OUTPUT_DISTRIBUTION_CERTIFICATE_VERSION,
207 protocol_version: SubscriptionProtocolVersion::CURRENT,
208 stream_id: "positions".into(),
209 catalog_generation: 1,
210 stream_generation: StreamGeneration::from_digest(SubscriptionDigest::from_bytes(
211 [1; 32],
212 )),
213 final_operator_id: "stream:positions".into(),
214 distribution: OutputDistribution::VnodePartitioned {
215 key_expressions_fingerprint: SubscriptionDigest::from_bytes([2; 32]),
216 partition_abi: PARTITIONING_ABI_VERSION,
217 vnode_count: 4,
218 },
219 schema_fingerprint: SubscriptionDigest::from_bytes([3; 32]),
220 changelog_mode: ChangelogMode::WeightedRetractInsert,
221 history_retention_bytes: 0,
222 query_fingerprint: SubscriptionDigest::from_bytes([4; 32]),
223 pipeline_identity: PipelineIdentity::empty(),
224 }
225 }
226
227 #[test]
228 fn certificate_mismatch_is_structured() {
229 let actual = certificate();
230 let mut expected = actual.clone();
231 expected.schema_fingerprint = SubscriptionDigest::from_bytes([9; 32]);
232 assert_eq!(
233 actual.require_match(&expected),
234 Err(SubscriptionContractError::DistributionCertificateMismatch)
235 );
236 }
237
238 #[test]
239 fn protocol_and_schema_fingerprint_fail_closed() {
240 let groups = KeyGroupCount::try_from(4_u16).unwrap();
241 let mut wrong_protocol = certificate();
242 wrong_protocol.protocol_version = SubscriptionProtocolVersion::new(2);
243 assert!(matches!(
244 wrong_protocol.validate(groups),
245 Err(SubscriptionContractError::ProtocolVersion { .. })
246 ));
247
248 let mut wrong_schema = certificate();
249 wrong_schema.schema_fingerprint = SubscriptionDigest::from_bytes([0; 32]);
250 assert_eq!(
251 wrong_schema.validate(groups),
252 Err(SubscriptionContractError::ZeroDigest {
253 field: "schema_fingerprint"
254 })
255 );
256 }
257}