1use std::collections::BTreeMap;
2
3use serde::Serialize;
4
5use super::{
6 OutputDistributionCertificate, OutputPartitionId, OutputSegmentRef, PartitionFrontier,
7 PartitionSequence, SubscriptionCheckpointManifest, SubscriptionContractError,
8 SubscriptionDigest, SubscriptionProtocolVersion, MAX_OUTPUT_SEGMENTS_PER_MANIFEST,
9 MAX_SUBSCRIPTION_MANIFEST_BYTES,
10};
11use crate::checkpoint::{canonical_json_bytes, CheckpointAssignmentFence};
12use crate::state::KeyGroupCount;
13
14#[derive(
16 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
17)]
18#[serde(deny_unknown_fields)]
19pub struct NodePartitionRange {
20 pub partition: OutputPartitionId,
22 pub first_sequence: PartitionSequence,
24 pub through_sequence: PartitionSequence,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct NodeSubscriptionStreamManifest {
32 pub distribution_certificate: OutputDistributionCertificate,
34 pub ranges: Vec<NodePartitionRange>,
36 pub segments: Vec<OutputSegmentRef>,
38}
39
40impl NodeSubscriptionStreamManifest {
41 fn validate(
42 &self,
43 key_group_count: KeyGroupCount,
44 owned_vnodes: &[u16],
45 ) -> Result<(), SubscriptionContractError> {
46 self.distribution_certificate.validate(key_group_count)?;
47 if self.ranges.is_empty()
48 || !self
49 .ranges
50 .windows(2)
51 .all(|pair| pair[0].partition < pair[1].partition)
52 {
53 return Err(SubscriptionContractError::NonCanonicalPartitionRanges);
54 }
55 for range in &self.ranges {
56 range.partition.validate(key_group_count)?;
57 if !self
58 .distribution_certificate
59 .distribution
60 .contains(range.partition)
61 || owned_vnodes.binary_search(&range.partition.get()).is_err()
62 {
63 return Err(SubscriptionContractError::PartitionOwnerMismatch {
64 partition: range.partition.get(),
65 });
66 }
67 if range.first_sequence > range.through_sequence {
68 return Err(SubscriptionContractError::InvalidSegmentRange);
69 }
70 }
71 validate_node_segments(self)
72 }
73}
74
75fn validate_node_segments(
76 stream: &NodeSubscriptionStreamManifest,
77) -> Result<(), SubscriptionContractError> {
78 if stream.segments.len() > MAX_OUTPUT_SEGMENTS_PER_MANIFEST {
79 return Err(SubscriptionContractError::TooManySegments {
80 actual: stream.segments.len(),
81 limit: MAX_OUTPUT_SEGMENTS_PER_MANIFEST,
82 });
83 }
84 if !stream.segments.windows(2).all(|pair| {
85 (pair[0].partition, pair[0].first_sequence) < (pair[1].partition, pair[1].first_sequence)
86 }) {
87 return Err(SubscriptionContractError::NonCanonicalSegments);
88 }
89 for segment in &stream.segments {
90 segment.validate(&stream.distribution_certificate)?;
91 }
92 let mut segment_index = 0;
93 for range in &stream.ranges {
94 let mut cursor = range.first_sequence;
95 while let Some(segment) = stream.segments.get(segment_index) {
96 if segment.partition < range.partition {
97 return Err(SubscriptionContractError::NonCanonicalSegments);
98 }
99 if segment.partition != range.partition {
100 break;
101 }
102 if segment.first_sequence != cursor {
103 return Err(SubscriptionContractError::SequenceGap {
104 partition: range.partition.get(),
105 expected: cursor.get(),
106 actual: segment.first_sequence.get(),
107 });
108 }
109 cursor = segment.exclusive_end_sequence;
110 if cursor > range.through_sequence {
111 return Err(SubscriptionContractError::SegmentBeyondFrontier);
112 }
113 segment_index += 1;
114 }
115 if cursor != range.through_sequence {
116 return Err(SubscriptionContractError::SequenceGap {
117 partition: range.partition.get(),
118 expected: range.through_sequence.get(),
119 actual: cursor.get(),
120 });
121 }
122 }
123 if segment_index != stream.segments.len() {
124 return Err(SubscriptionContractError::NonCanonicalSegments);
125 }
126 Ok(())
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct NodeSubscriptionManifest {
133 pub protocol_version: SubscriptionProtocolVersion,
135 pub epoch: u64,
137 pub checkpoint_id: u64,
139 pub participant_id: u64,
141 pub assignment_certificate: CheckpointAssignmentFence,
143 pub streams: Vec<NodeSubscriptionStreamManifest>,
145 pub manifest_digest: SubscriptionDigest,
147}
148
149impl NodeSubscriptionManifest {
150 pub fn seal(&mut self, owned_vnodes: &[u16]) -> Result<(), SubscriptionContractError> {
155 self.manifest_digest = self.computed_digest()?;
156 self.validate(owned_vnodes)
157 }
158
159 pub fn validate(&self, owned_vnodes: &[u16]) -> Result<(), SubscriptionContractError> {
164 self.protocol_version.validate()?;
165 if self.epoch == 0 || self.checkpoint_id != self.epoch {
166 return Err(SubscriptionContractError::CheckpointAttempt);
167 }
168 if self.participant_id == 0
169 || !self.assignment_certificate.is_canonical()
170 || self
171 .assignment_certificate
172 .participant_incarnation(self.participant_id)
173 .is_none()
174 {
175 return Err(SubscriptionContractError::NodeParticipant);
176 }
177 let key_group_count = KeyGroupCount::try_from(self.assignment_certificate.vnode_count)
178 .map_err(|_| SubscriptionContractError::AssignmentCertificate)?;
179 if !self.streams.windows(2).all(|pair| {
180 pair[0].distribution_certificate.stream_id < pair[1].distribution_certificate.stream_id
181 }) {
182 return Err(SubscriptionContractError::NonCanonicalNodeStreams);
183 }
184 for stream in &self.streams {
185 stream.validate(key_group_count, owned_vnodes)?;
186 }
187 if self.manifest_digest != self.computed_digest()? {
188 return Err(SubscriptionContractError::NodeManifestDigest);
189 }
190 let encoded = canonical_json_bytes(self)
191 .map_err(|error| SubscriptionContractError::Encode(error.to_string()))?;
192 if encoded.len() > MAX_SUBSCRIPTION_MANIFEST_BYTES {
193 return Err(SubscriptionContractError::ManifestTooLarge {
194 actual: encoded.len(),
195 limit: MAX_SUBSCRIPTION_MANIFEST_BYTES,
196 });
197 }
198 Ok(())
199 }
200
201 fn computed_digest(&self) -> Result<SubscriptionDigest, SubscriptionContractError> {
202 let body = NodeManifestBody {
203 protocol_version: self.protocol_version,
204 epoch: self.epoch,
205 checkpoint_id: self.checkpoint_id,
206 participant_id: self.participant_id,
207 assignment_certificate: &self.assignment_certificate,
208 streams: &self.streams,
209 };
210 let encoded = canonical_json_bytes(&body)
211 .map_err(|error| SubscriptionContractError::Encode(error.to_string()))?;
212 Ok(SubscriptionDigest::for_bytes(
213 b"laminardb-node-subscription-manifest-v1",
214 &encoded,
215 ))
216 }
217}
218
219#[derive(Serialize)]
220struct NodeManifestBody<'a> {
221 protocol_version: SubscriptionProtocolVersion,
222 epoch: u64,
223 checkpoint_id: u64,
224 participant_id: u64,
225 assignment_certificate: &'a CheckpointAssignmentFence,
226 streams: &'a [NodeSubscriptionStreamManifest],
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct MergedSubscriptionCheckpoint {
232 pub manifest: SubscriptionCheckpointManifest,
234 pub ranges: Vec<NodePartitionRange>,
236}
237
238impl MergedSubscriptionCheckpoint {
239 pub fn validate_continuity(
247 &self,
248 predecessor: Option<&SubscriptionCheckpointManifest>,
249 ) -> Result<(), SubscriptionContractError> {
250 let predecessor = predecessor
251 .filter(|manifest| manifest.stream_generation == self.manifest.stream_generation);
252 if let Some(predecessor) = predecessor {
253 self.manifest
254 .distribution_certificate
255 .require_match(&predecessor.distribution_certificate)?;
256 }
257 for range in &self.ranges {
258 let expected = predecessor
259 .and_then(|manifest| {
260 manifest
261 .frontiers
262 .binary_search_by_key(&range.partition, |frontier| frontier.partition)
263 .ok()
264 .map(|index| manifest.frontiers[index].through_sequence)
265 })
266 .unwrap_or(PartitionSequence::FIRST);
267 if range.first_sequence != expected {
268 return Err(SubscriptionContractError::SequenceGap {
269 partition: range.partition.get(),
270 expected: expected.get(),
271 actual: range.first_sequence.get(),
272 });
273 }
274 }
275 Ok(())
276 }
277}
278
279struct MergedStream {
280 certificate: OutputDistributionCertificate,
281 ranges: Vec<NodePartitionRange>,
282 segments: Vec<OutputSegmentRef>,
283}
284
285pub fn merge_node_subscription_manifests(
292 epoch: u64,
293 checkpoint_id: u64,
294 assignment_certificate: &CheckpointAssignmentFence,
295 manifests: &[(&NodeSubscriptionManifest, &[u16])],
296) -> Result<Vec<MergedSubscriptionCheckpoint>, SubscriptionContractError> {
297 let expected_participants = assignment_certificate.participant_ids();
298 let actual_participants = manifests
299 .iter()
300 .map(|(manifest, _)| manifest.participant_id)
301 .collect::<Vec<_>>();
302 if actual_participants != expected_participants {
303 return Err(SubscriptionContractError::NodeParticipant);
304 }
305 validate_node_ownership(assignment_certificate, manifests)?;
306 let mut streams = BTreeMap::<String, MergedStream>::new();
307 for (node, owned_vnodes) in manifests {
308 if node.epoch != epoch
309 || node.checkpoint_id != checkpoint_id
310 || node.assignment_certificate != *assignment_certificate
311 {
312 return Err(SubscriptionContractError::AssignmentCertificate);
313 }
314 node.validate(owned_vnodes)?;
315 for stream in &node.streams {
316 let stream_id = stream.distribution_certificate.stream_id.clone();
317 let merged = streams.entry(stream_id).or_insert_with(|| MergedStream {
318 certificate: stream.distribution_certificate.clone(),
319 ranges: Vec::new(),
320 segments: Vec::new(),
321 });
322 merged
323 .certificate
324 .require_match(&stream.distribution_certificate)?;
325 merged.ranges.extend_from_slice(&stream.ranges);
326 merged.segments.extend_from_slice(&stream.segments);
327 }
328 }
329
330 let mut merged_checkpoints = Vec::with_capacity(streams.len());
331 for (_, mut stream) in streams {
332 stream.ranges.sort_unstable_by_key(|range| range.partition);
333 if !stream
334 .ranges
335 .windows(2)
336 .all(|pair| pair[0].partition < pair[1].partition)
337 {
338 return Err(SubscriptionContractError::NonCanonicalPartitionRanges);
339 }
340 stream
341 .segments
342 .sort_unstable_by_key(|segment| (segment.partition, segment.first_sequence));
343 let frontiers = stream
344 .ranges
345 .iter()
346 .map(|range| PartitionFrontier {
347 partition: range.partition,
348 through_sequence: range.through_sequence,
349 })
350 .collect();
351 let mut manifest = SubscriptionCheckpointManifest {
352 protocol_version: SubscriptionProtocolVersion::CURRENT,
353 stream_generation: stream.certificate.stream_generation,
354 schema_fingerprint: stream.certificate.schema_fingerprint,
355 distribution_certificate: stream.certificate,
356 epoch,
357 checkpoint_id,
358 assignment_certificate: assignment_certificate.clone(),
359 frontiers,
360 segments: stream.segments,
361 manifest_digest: SubscriptionDigest::from_bytes([0; 32]),
362 };
363 manifest.seal()?;
364 merged_checkpoints.push(MergedSubscriptionCheckpoint {
365 manifest,
366 ranges: stream.ranges,
367 });
368 }
369 Ok(merged_checkpoints)
370}
371
372fn validate_node_ownership(
373 assignment_certificate: &CheckpointAssignmentFence,
374 manifests: &[(&NodeSubscriptionManifest, &[u16])],
375) -> Result<(), SubscriptionContractError> {
376 let vnode_count = usize::try_from(assignment_certificate.vnode_count)
377 .map_err(|_| SubscriptionContractError::AssignmentCertificate)?;
378 let mut owners = vec![None; vnode_count];
379 for (manifest, owned_vnodes) in manifests {
380 if owned_vnodes.is_empty() || !owned_vnodes.windows(2).all(|pair| pair[0] < pair[1]) {
381 return Err(SubscriptionContractError::AssignmentCertificate);
382 }
383 for vnode in *owned_vnodes {
384 let owner = owners
385 .get_mut(usize::from(*vnode))
386 .ok_or(SubscriptionContractError::AssignmentCertificate)?;
387 if owner.replace(manifest.participant_id).is_some() {
388 return Err(SubscriptionContractError::AssignmentCertificate);
389 }
390 }
391 }
392 if owners.iter().any(Option::is_none) {
393 return Err(SubscriptionContractError::AssignmentCertificate);
394 }
395 let owner_map = owners.into_iter().flatten().collect::<Vec<_>>();
396 if !assignment_certificate.matches_owner_map(&owner_map) {
397 return Err(SubscriptionContractError::AssignmentCertificate);
398 }
399 Ok(())
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::checkpoint::{
406 ChangelogMode, CheckpointParticipant, PipelineIdentity, StreamGeneration,
407 OUTPUT_DISTRIBUTION_CERTIFICATE_VERSION,
408 };
409 use crate::state::PARTITIONING_ABI_VERSION;
410
411 fn certificate() -> OutputDistributionCertificate {
412 OutputDistributionCertificate {
413 version: OUTPUT_DISTRIBUTION_CERTIFICATE_VERSION,
414 protocol_version: SubscriptionProtocolVersion::CURRENT,
415 stream_id: "positions".into(),
416 catalog_generation: 1,
417 stream_generation: StreamGeneration::from_digest(SubscriptionDigest::from_bytes(
418 [1; 32],
419 )),
420 final_operator_id: "stream:positions".into(),
421 distribution: super::super::OutputDistribution::VnodePartitioned {
422 key_expressions_fingerprint: SubscriptionDigest::from_bytes([2; 32]),
423 partition_abi: PARTITIONING_ABI_VERSION,
424 vnode_count: 4,
425 },
426 schema_fingerprint: SubscriptionDigest::from_bytes([3; 32]),
427 changelog_mode: ChangelogMode::WeightedRetractInsert,
428 history_retention_bytes: 0,
429 query_fingerprint: SubscriptionDigest::from_bytes([4; 32]),
430 pipeline_identity: PipelineIdentity::empty(),
431 }
432 }
433
434 fn assignment() -> CheckpointAssignmentFence {
435 CheckpointAssignmentFence::from_owner_map(
436 5,
437 &[1, 2, 1, 2],
438 vec![
439 CheckpointParticipant {
440 node_id: 1,
441 boot_incarnation: uuid::Uuid::parse_str("11111111-1111-4111-8111-111111111111")
442 .unwrap(),
443 },
444 CheckpointParticipant {
445 node_id: 2,
446 boot_incarnation: uuid::Uuid::parse_str("22222222-2222-4222-8222-222222222222")
447 .unwrap(),
448 },
449 ],
450 )
451 .unwrap()
452 }
453
454 fn node(
455 participant_id: u64,
456 partitions: &[u16],
457 first_sequence: u64,
458 through_sequence: u64,
459 ) -> NodeSubscriptionManifest {
460 let assignment = assignment();
461 let mut manifest = NodeSubscriptionManifest {
462 protocol_version: SubscriptionProtocolVersion::CURRENT,
463 epoch: 10,
464 checkpoint_id: 10,
465 participant_id,
466 assignment_certificate: assignment,
467 streams: vec![NodeSubscriptionStreamManifest {
468 distribution_certificate: certificate(),
469 ranges: partitions
470 .iter()
471 .map(|partition| NodePartitionRange {
472 partition: OutputPartitionId::new(*partition),
473 first_sequence: PartitionSequence::new(first_sequence),
474 through_sequence: PartitionSequence::new(through_sequence),
475 })
476 .collect(),
477 segments: Vec::new(),
478 }],
479 manifest_digest: SubscriptionDigest::from_bytes([0; 32]),
480 };
481 manifest.seal(partitions).unwrap();
482 manifest
483 }
484
485 fn segment(partition: u16, first: u64, end: u64, suffix: &str) -> OutputSegmentRef {
486 OutputSegmentRef {
487 protocol_version: SubscriptionProtocolVersion::CURRENT,
488 object_key: format!("subscription-output/test/{partition}/{suffix}.arrow"),
489 stream_generation: certificate().stream_generation,
490 partition: OutputPartitionId::new(partition),
491 first_sequence: PartitionSequence::new(first),
492 exclusive_end_sequence: PartitionSequence::new(end),
493 frame_count: end - first,
494 row_count: 1,
495 encoded_length: 1,
496 schema_fingerprint: certificate().schema_fingerprint,
497 payload_digest: SubscriptionDigest::from_bytes([5; 32]),
498 }
499 }
500
501 #[test]
502 fn participant_ranges_merge_into_one_exact_frontier_vector() {
503 let first = node(1, &[0, 2], 0, 0);
504 let second = node(2, &[1, 3], 0, 0);
505 let merged = merge_node_subscription_manifests(
506 10,
507 10,
508 &assignment(),
509 &[(&first, &[0, 2]), (&second, &[1, 3])],
510 )
511 .unwrap();
512 assert_eq!(merged.len(), 1);
513 assert_eq!(
514 merged[0]
515 .manifest
516 .frontiers
517 .iter()
518 .map(|frontier| frontier.partition.get())
519 .collect::<Vec<_>>(),
520 vec![0, 1, 2, 3]
521 );
522 }
523
524 #[test]
525 fn missing_participant_and_wrong_owner_fail_closed() {
526 let first = node(1, &[0, 2], 0, 0);
527 assert_eq!(
528 merge_node_subscription_manifests(10, 10, &assignment(), &[(&first, &[0, 2])]),
529 Err(SubscriptionContractError::NodeParticipant)
530 );
531
532 let mut wrong_owner = node(1, &[0, 2], 0, 0);
533 wrong_owner.streams[0].ranges[1].partition = OutputPartitionId::new(1);
534 wrong_owner.manifest_digest = wrong_owner.computed_digest().unwrap();
535 assert_eq!(
536 wrong_owner.validate(&[0, 2]),
537 Err(SubscriptionContractError::PartitionOwnerMismatch { partition: 1 })
538 );
539 }
540
541 #[test]
542 fn predecessor_frontier_gap_is_explicit() {
543 let predecessor_first = node(1, &[0, 2], 0, 0);
544 let predecessor_second = node(2, &[1, 3], 0, 0);
545 let predecessor = merge_node_subscription_manifests(
546 10,
547 10,
548 &assignment(),
549 &[
550 (&predecessor_first, &[0, 2]),
551 (&predecessor_second, &[1, 3]),
552 ],
553 )
554 .unwrap()
555 .remove(0);
556
557 let current_first = node(1, &[0, 2], 1, 1);
558 let current_second = node(2, &[1, 3], 1, 1);
559 let current = merge_node_subscription_manifests(
560 10,
561 10,
562 &assignment(),
563 &[(¤t_first, &[0, 2]), (¤t_second, &[1, 3])],
564 )
565 .unwrap()
566 .remove(0);
567 assert_eq!(
568 current.validate_continuity(Some(&predecessor.manifest)),
569 Err(SubscriptionContractError::SequenceGap {
570 partition: 0,
571 expected: 0,
572 actual: 1,
573 })
574 );
575 }
576
577 #[test]
578 fn duplicate_segment_and_partition_gap_are_rejected() {
579 let mut duplicate = node(1, &[0, 2], 0, 0);
580 duplicate.streams[0].ranges[0].through_sequence = PartitionSequence::new(1);
581 let output = segment(0, 0, 1, "duplicate");
582 duplicate.streams[0].segments = vec![output.clone(), output];
583 duplicate.manifest_digest = duplicate.computed_digest().unwrap();
584 assert_eq!(
585 duplicate.validate(&[0, 2]),
586 Err(SubscriptionContractError::NonCanonicalSegments)
587 );
588
589 let mut gap = node(1, &[0, 2], 0, 0);
590 gap.streams[0].ranges[0].through_sequence = PartitionSequence::new(3);
591 gap.streams[0].segments = vec![segment(0, 0, 1, "first"), segment(0, 2, 3, "last")];
592 gap.manifest_digest = gap.computed_digest().unwrap();
593 assert_eq!(
594 gap.validate(&[0, 2]),
595 Err(SubscriptionContractError::SequenceGap {
596 partition: 0,
597 expected: 1,
598 actual: 2,
599 })
600 );
601 }
602
603 #[test]
604 fn merge_revalidates_node_digest_segments_and_assignment_ownership() {
605 let mut unvalidated = node(1, &[0, 2], 0, 0);
606 unvalidated.streams[0].ranges[0].through_sequence = PartitionSequence::new(1);
607 let second = node(2, &[1, 3], 0, 0);
608 assert_eq!(
609 merge_node_subscription_manifests(
610 10,
611 10,
612 &assignment(),
613 &[(&unvalidated, &[0, 2]), (&second, &[1, 3])],
614 ),
615 Err(SubscriptionContractError::SequenceGap {
616 partition: 0,
617 expected: 1,
618 actual: 0,
619 })
620 );
621
622 let first = node(1, &[0, 2], 0, 0);
623 assert_eq!(
624 merge_node_subscription_manifests(
625 10,
626 10,
627 &assignment(),
628 &[(&first, &[0, 1]), (&second, &[2, 3])],
629 ),
630 Err(SubscriptionContractError::AssignmentCertificate)
631 );
632 }
633}