laminar_core/checkpoint/subscription/
identity.rs1use std::fmt::Write as _;
2
3use sha2::{Digest as _, Sha256};
4
5use super::SubscriptionContractError;
6use crate::checkpoint::PipelineIdentity;
7use crate::state::KeyGroupCount;
8
9pub const SUBSCRIPTION_PROTOCOL_VERSION: u16 = 1;
11
12#[derive(
14 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
15)]
16#[serde(transparent)]
17pub struct SubscriptionProtocolVersion(u16);
18
19impl SubscriptionProtocolVersion {
20 pub const CURRENT: Self = Self(SUBSCRIPTION_PROTOCOL_VERSION);
22
23 #[must_use]
25 pub const fn new(value: u16) -> Self {
26 Self(value)
27 }
28
29 #[must_use]
31 pub const fn get(self) -> u16 {
32 self.0
33 }
34
35 pub(crate) fn validate(self) -> Result<(), SubscriptionContractError> {
37 if self != Self::CURRENT {
38 return Err(SubscriptionContractError::ProtocolVersion {
39 actual: self.0,
40 expected: SUBSCRIPTION_PROTOCOL_VERSION,
41 });
42 }
43 Ok(())
44 }
45}
46
47#[derive(
49 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
50)]
51#[serde(transparent)]
52pub struct SubscriptionDigest([u8; 32]);
53
54impl SubscriptionDigest {
55 #[must_use]
57 pub fn for_bytes(domain: &[u8], value: &[u8]) -> Self {
58 let mut hash = Sha256::new();
59 update_part(&mut hash, domain);
60 update_part(&mut hash, value);
61 Self(hash.finalize().into())
62 }
63
64 #[must_use]
66 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
67 Self(bytes)
68 }
69
70 #[must_use]
72 pub const fn as_bytes(&self) -> &[u8; 32] {
73 &self.0
74 }
75
76 #[must_use]
78 pub fn to_hex(self) -> String {
79 let mut encoded = String::with_capacity(64);
80 for byte in self.0 {
81 let _ = write!(&mut encoded, "{byte:02x}");
82 }
83 encoded
84 }
85
86 pub(crate) fn validate(self, field: &'static str) -> Result<(), SubscriptionContractError> {
87 if self.0 == [0; 32] {
88 return Err(SubscriptionContractError::ZeroDigest { field });
89 }
90 Ok(())
91 }
92}
93
94impl std::fmt::Display for SubscriptionDigest {
95 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 for byte in self.0 {
97 write!(formatter, "{byte:02x}")?;
98 }
99 Ok(())
100 }
101}
102
103#[derive(
105 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
106)]
107#[serde(transparent)]
108pub struct StreamGeneration(SubscriptionDigest);
109
110impl StreamGeneration {
111 #[must_use]
113 pub fn derive(
114 deployment_id: uuid::Uuid,
115 catalog_generation: u64,
116 stream_name: &str,
117 canonical_query: &str,
118 pipeline_identity: &PipelineIdentity,
119 ) -> Self {
120 let mut hash = Sha256::new();
121 update_part(&mut hash, b"laminardb-subscription-stream-generation-v1");
122 update_part(&mut hash, deployment_id.as_bytes());
123 update_part(&mut hash, &catalog_generation.to_le_bytes());
124 update_part(&mut hash, stream_name.as_bytes());
125 update_part(&mut hash, canonical_query.as_bytes());
126 update_part(
127 &mut hash,
128 &pipeline_identity.canonical_version.to_le_bytes(),
129 );
130 update_part(&mut hash, pipeline_identity.sha256.as_bytes());
131 Self(SubscriptionDigest(hash.finalize().into()))
132 }
133
134 #[must_use]
136 pub const fn from_digest(digest: SubscriptionDigest) -> Self {
137 Self(digest)
138 }
139
140 #[must_use]
142 pub const fn digest(self) -> SubscriptionDigest {
143 self.0
144 }
145
146 pub(crate) fn validate(self) -> Result<(), SubscriptionContractError> {
147 self.0.validate("stream_generation")
148 }
149}
150
151impl std::fmt::Display for StreamGeneration {
152 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 self.0.fmt(formatter)
154 }
155}
156
157#[derive(
159 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
160)]
161#[serde(transparent)]
162pub struct OutputPartitionId(u16);
163
164impl OutputPartitionId {
165 #[must_use]
167 pub const fn new(vnode: u16) -> Self {
168 Self(vnode)
169 }
170
171 #[must_use]
173 pub const fn get(self) -> u16 {
174 self.0
175 }
176
177 pub(crate) fn validate(
178 self,
179 key_group_count: KeyGroupCount,
180 ) -> Result<(), SubscriptionContractError> {
181 if self.0 >= key_group_count.get() {
182 return Err(SubscriptionContractError::PartitionOutOfRange {
183 partition: self.0,
184 vnode_count: key_group_count.get(),
185 });
186 }
187 Ok(())
188 }
189}
190
191#[derive(
193 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
194)]
195#[serde(transparent)]
196pub struct PartitionSequence(u64);
197
198impl PartitionSequence {
199 pub const FIRST: Self = Self(0);
201
202 #[must_use]
204 pub const fn new(value: u64) -> Self {
205 Self(value)
206 }
207
208 #[must_use]
210 pub const fn get(self) -> u64 {
211 self.0
212 }
213
214 pub fn checked_next(self) -> Result<Self, SubscriptionContractError> {
219 self.0
220 .checked_add(1)
221 .map(Self)
222 .ok_or(SubscriptionContractError::SequenceOverflow)
223 }
224}
225
226#[derive(
228 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
229)]
230#[serde(deny_unknown_fields)]
231pub struct OutputFrameId {
232 pub stream_generation: StreamGeneration,
234 pub partition: OutputPartitionId,
236 pub sequence: PartitionSequence,
238}
239
240fn update_part(hash: &mut Sha256, bytes: &[u8]) {
241 hash.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
242 hash.update(bytes);
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 fn pipeline() -> PipelineIdentity {
250 PipelineIdentity::empty()
251 }
252
253 #[test]
254 fn generation_round_trips_and_recreation_changes_it() {
255 let deployment = uuid::Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap();
256 let first = StreamGeneration::derive(deployment, 7, "positions", "select 1", &pipeline());
257 let recreated =
258 StreamGeneration::derive(deployment, 8, "positions", "select 1", &pipeline());
259 assert_ne!(first, recreated);
260
261 let encoded = serde_json::to_vec(&first).unwrap();
262 let decoded: StreamGeneration = serde_json::from_slice(&encoded).unwrap();
263 assert_eq!(decoded, first);
264 assert_eq!(first.to_string().len(), 64);
265 }
266
267 #[test]
268 fn partition_sequence_starts_at_zero_and_never_wraps() {
269 assert_eq!(PartitionSequence::FIRST.get(), 0);
270 assert_eq!(PartitionSequence::FIRST.checked_next().unwrap().get(), 1);
271 assert_eq!(
272 PartitionSequence::new(u64::MAX).checked_next(),
273 Err(SubscriptionContractError::SequenceOverflow)
274 );
275 }
276}