1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{self, Display, Formatter};
6use std::future::Future;
7use std::num::{NonZeroU32, NonZeroU64, NonZeroU128};
8use std::pin::Pin;
9use std::time::Instant;
10
11use serde::de::{DeserializeOwned, Visitor};
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13
14use crate::{CollectionIndexRef, FenceToken, IndexMode, ManifestGeneration, ShardKey};
15
16pub const REPLAYABLE_SOURCE_FORMAT_VERSION: u16 = 1;
17pub const MAX_SOURCE_PARTITIONS: usize = 128;
18pub const MAX_SOURCE_RESUME_TOKEN_BYTES: usize = 4_096;
19pub const MAX_SOURCE_RECORD_BYTES: usize = 1_048_576;
20pub const MAX_SOURCE_BATCH_RECORDS: usize = 4_096;
21pub const MAX_SOURCE_BATCH_BYTES: usize = 16_777_216;
22pub const MAX_SOURCE_CONFIGURATION_BYTES: usize = 262_144;
23pub const MAX_ATOMIC_SOURCE_SHARDS: usize = 512;
24pub const MAX_ATOMIC_SOURCE_COMMAND_BYTES: usize = 1_048_576;
25
26macro_rules! nonzero_number {
27 ($name:ident, $inner:ty, $primitive:ty, $description:literal) => {
28 #[doc = $description]
29 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
30 #[serde(transparent)]
31 pub struct $name($inner);
32
33 impl $name {
34 pub fn new(value: $primitive) -> Result<Self, SourceContractError> {
35 <$inner>::new(value)
36 .map(Self)
37 .ok_or(SourceContractError::ZeroIdentity(stringify!($name)))
38 }
39
40 #[must_use]
41 pub const fn get(self) -> $primitive {
42 self.0.get()
43 }
44 }
45
46 impl Display for $name {
47 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
48 Display::fmt(&self.get(), formatter)
49 }
50 }
51 };
52}
53
54macro_rules! nonzero_u128_decimal {
58 ($name:ident, $description:literal) => {
59 #[doc = $description]
60 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
61 pub struct $name(NonZeroU128);
62
63 impl $name {
64 pub fn new(value: u128) -> Result<Self, SourceContractError> {
65 NonZeroU128::new(value)
66 .map(Self)
67 .ok_or(SourceContractError::ZeroIdentity(stringify!($name)))
68 }
69
70 #[must_use]
71 pub const fn get(self) -> u128 {
72 self.0.get()
73 }
74 }
75
76 impl Display for $name {
77 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
78 Display::fmt(&self.get(), formatter)
79 }
80 }
81
82 impl Serialize for $name {
83 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
84 where
85 S: Serializer,
86 {
87 serializer.serialize_str(&self.get().to_string())
88 }
89 }
90
91 impl<'de> Deserialize<'de> for $name {
92 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93 where
94 D: Deserializer<'de>,
95 {
96 struct CanonicalNonzeroDecimalVisitor;
97
98 impl Visitor<'_> for CanonicalNonzeroDecimalVisitor {
99 type Value = $name;
100
101 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
102 formatter.write_str("a canonical nonzero u128 decimal string")
103 }
104
105 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
106 where
107 E: serde::de::Error,
108 {
109 let bytes = value.as_bytes();
110 if bytes.is_empty()
111 || !matches!(bytes[0], b'1'..=b'9')
112 || !bytes[1..].iter().all(u8::is_ascii_digit)
113 {
114 return Err(E::custom("expected a canonical nonzero u128 decimal string"));
115 }
116 let parsed = value
117 .parse::<u128>()
118 .map_err(|_| E::custom("source identity exceeds u128"))?;
119 NonZeroU128::new(parsed)
120 .map($name)
121 .ok_or_else(|| E::custom("source identity cannot be zero"))
122 }
123 }
124
125 deserializer.deserialize_str(CanonicalNonzeroDecimalVisitor)
126 }
127 }
128 };
129}
130
131nonzero_number!(
132 SourceIncarnation,
133 NonZeroU64,
134 u64,
135 "A never-reused lifetime of one exact external source binding."
136);
137#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
139#[serde(transparent)]
140pub struct SourcePartitionId(u32);
141
142impl SourcePartitionId {
143 #[must_use]
144 pub const fn new(value: u32) -> Self {
145 Self(value)
146 }
147
148 #[must_use]
149 pub const fn get(self) -> u32 {
150 self.0
151 }
152}
153
154impl Display for SourcePartitionId {
155 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
156 Display::fmt(&self.0, formatter)
157 }
158}
159nonzero_number!(
160 SourcePartitionIncarnation,
161 NonZeroU64,
162 u64,
163 "A never-reused lifetime of one source partition identity."
164);
165nonzero_u128_decimal!(
166 SourceOperationId,
167 "A stable operation identity reproduced by every replay."
168);
169nonzero_u128_decimal!(
170 SourceTransactionId,
171 "A stable source transaction identity reproduced by every replay."
172);
173nonzero_u128_decimal!(SourceBatchId, "A stable identity for one bounded provider batch.");
174nonzero_number!(
175 SourceControlGeneration,
176 NonZeroU64,
177 u64,
178 "The monotonic generation of one replayable-source lifecycle state."
179);
180
181#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
183#[serde(transparent)]
184pub struct SourceOrdinal(u64);
185
186impl SourceOrdinal {
187 #[must_use]
188 pub const fn new(value: u64) -> Self {
189 Self(value)
190 }
191
192 #[must_use]
193 pub const fn get(self) -> u64 {
194 self.0
195 }
196
197 #[must_use]
198 pub const fn checked_next(self) -> Option<Self> {
199 match self.0.checked_add(1) {
200 Some(value) => Some(Self(value)),
201 None => None,
202 }
203 }
204}
205
206impl Display for SourceOrdinal {
207 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
208 Display::fmt(&self.0, formatter)
209 }
210}
211
212#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
213#[serde(rename_all = "snake_case")]
214pub enum SourceProviderKindV1 {
215 Kafka,
216 ObjectLog,
217 HttpReplay,
218 GrpcReplay,
219}
220
221#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
222#[serde(rename_all = "snake_case")]
223pub enum SourceDecoderKindV1 {
224 NativeDocumentJson,
225 DebeziumPostgresJson,
226}
227
228#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
229#[serde(deny_unknown_fields)]
230pub struct SourceReplayGuaranteesV1 {
231 pub durable_replay: bool,
232 pub stable_partition_identity: bool,
233 pub stable_operation_identity: bool,
234 pub ordered_positions: bool,
235 pub bounded_reads: bool,
236 pub explicit_retention: bool,
237 pub explicit_rewind: bool,
238 pub minimum_replay_window_millis: u64,
239}
240
241impl SourceReplayGuaranteesV1 {
242 pub fn validate(&self) -> Result<(), SourceContractError> {
243 if !self.durable_replay
244 || !self.stable_partition_identity
245 || !self.stable_operation_identity
246 || !self.ordered_positions
247 || !self.bounded_reads
248 || !self.explicit_retention
249 || !self.explicit_rewind
250 || self.minimum_replay_window_millis == 0
251 {
252 return Err(SourceContractError::Violation(
253 "WAL elision requires every replay guarantee and a positive replay window",
254 ));
255 }
256 Ok(())
257 }
258}
259
260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
261#[serde(deny_unknown_fields)]
262pub struct SourceCursorV1 {
263 pub next_ordinal: SourceOrdinal,
265 pub resume_token: Vec<u8>,
267}
268
269impl SourceCursorV1 {
270 pub fn validate(&self) -> Result<(), SourceContractError> {
271 if self.resume_token.len() > MAX_SOURCE_RESUME_TOKEN_BYTES {
272 return Err(SourceContractError::Budget {
273 field: "source resume token bytes",
274 observed: self.resume_token.len(),
275 maximum: MAX_SOURCE_RESUME_TOKEN_BYTES,
276 });
277 }
278 Ok(())
279 }
280}
281
282#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
283#[serde(deny_unknown_fields)]
284pub struct SourcePartitionCheckpointV1 {
285 pub partition_id: SourcePartitionId,
286 pub partition_incarnation: SourcePartitionIncarnation,
287 pub cursor: SourceCursorV1,
288}
289
290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
291#[serde(deny_unknown_fields)]
292pub struct SourceCutV1 {
293 pub format_version: u16,
294 pub source_incarnation: SourceIncarnation,
295 pub partitions: Vec<SourcePartitionCheckpointV1>,
296}
297
298impl SourceCutV1 {
299 pub fn validate(&self) -> Result<(), SourceContractError> {
300 validate_format(self.format_version)?;
301 if self.partitions.is_empty() || self.partitions.len() > MAX_SOURCE_PARTITIONS {
302 return Err(SourceContractError::Budget {
303 field: "source partitions",
304 observed: self.partitions.len(),
305 maximum: MAX_SOURCE_PARTITIONS,
306 });
307 }
308 let mut prior = None;
309 for checkpoint in &self.partitions {
310 if prior.is_some_and(|value| value >= checkpoint.partition_id) {
311 return Err(SourceContractError::Violation(
312 "partition checkpoints must be strictly ordered and unique",
313 ));
314 }
315 checkpoint.cursor.validate()?;
316 prior = Some(checkpoint.partition_id);
317 }
318 Ok(())
319 }
320
321 pub fn advances_to(&self, replacement: &Self) -> Result<(), SourceContractError> {
322 self.validate()?;
323 replacement.validate()?;
324 if self.source_incarnation != replacement.source_incarnation
325 || self.partitions.len() != replacement.partitions.len()
326 {
327 return Err(SourceContractError::Violation(
328 "a source cut cannot change source incarnation or partition membership",
329 ));
330 }
331 let mut advanced = false;
332 for (expected, next) in self.partitions.iter().zip(&replacement.partitions) {
333 if expected.partition_id != next.partition_id
334 || expected.partition_incarnation != next.partition_incarnation
335 || next.cursor.next_ordinal < expected.cursor.next_ordinal
336 || (next.cursor.next_ordinal == expected.cursor.next_ordinal
337 && next.cursor.resume_token != expected.cursor.resume_token)
338 {
339 return Err(SourceContractError::Violation(
340 "replacement source cut must preserve partition identities and move forward",
341 ));
342 }
343 advanced |= next.cursor.next_ordinal > expected.cursor.next_ordinal;
344 }
345 if !advanced {
346 return Err(SourceContractError::Violation(
347 "replacement source cut must advance at least one partition",
348 ));
349 }
350 Ok(())
351 }
352}
353
354#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
355#[serde(deny_unknown_fields)]
356pub struct SourcePartitionBindingV1 {
357 pub partition_id: SourcePartitionId,
358 pub partition_incarnation: SourcePartitionIncarnation,
359}
360
361#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
362#[serde(deny_unknown_fields)]
363pub struct SourceBindingV1 {
364 pub format_version: u16,
365 pub collection_index_ref: CollectionIndexRef,
366 pub source_incarnation: SourceIncarnation,
367 pub source_identity_digest: [u8; 32],
368 pub provider: SourceProviderKindV1,
369 pub decoder: SourceDecoderKindV1,
370 pub partitions: Vec<SourcePartitionBindingV1>,
371 pub guarantees: SourceReplayGuaranteesV1,
372}
373
374impl SourceBindingV1 {
375 pub fn validate(&self) -> Result<(), SourceContractError> {
376 validate_format(self.format_version)?;
377 if self.source_identity_digest == [0; 32] {
378 return Err(SourceContractError::ZeroIdentity("source_identity_digest"));
379 }
380 if self.partitions.is_empty() || self.partitions.len() > MAX_SOURCE_PARTITIONS {
381 return Err(SourceContractError::Budget {
382 field: "source partitions",
383 observed: self.partitions.len(),
384 maximum: MAX_SOURCE_PARTITIONS,
385 });
386 }
387 let mut prior = None;
388 for partition in &self.partitions {
389 if prior.is_some_and(|value| value >= partition.partition_id) {
390 return Err(SourceContractError::Violation(
391 "source partition bindings must be strictly ordered and unique",
392 ));
393 }
394 prior = Some(partition.partition_id);
395 }
396 self.guarantees.validate()
397 }
398
399 pub fn validate_cut(&self, cut: &SourceCutV1) -> Result<(), SourceContractError> {
400 self.validate()?;
401 cut.validate()?;
402 if self.source_incarnation != cut.source_incarnation || self.partitions.len() != cut.partitions.len() {
403 return Err(SourceContractError::Violation(
404 "source cut does not belong to the catalog source binding",
405 ));
406 }
407 for (binding, checkpoint) in self.partitions.iter().zip(&cut.partitions) {
408 if binding.partition_id != checkpoint.partition_id
409 || binding.partition_incarnation != checkpoint.partition_incarnation
410 {
411 return Err(SourceContractError::Violation(
412 "source cut partition identity differs from the catalog binding",
413 ));
414 }
415 }
416 Ok(())
417 }
418}
419
420#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
421#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
422pub enum IngestAuthorityV1 {
423 PushWithWal {
424 format_version: u16,
425 },
426 ReplayableSource {
427 format_version: u16,
428 binding: SourceBindingV1,
429 },
430}
431
432impl IngestAuthorityV1 {
433 #[must_use]
434 pub const fn push_with_wal() -> Self {
435 Self::PushWithWal {
436 format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
437 }
438 }
439
440 #[must_use]
441 pub const fn replayable_source(binding: SourceBindingV1) -> Self {
442 Self::ReplayableSource {
443 format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
444 binding,
445 }
446 }
447
448 pub fn validate_for_mode(&self, mode: IndexMode) -> Result<(), SourceContractError> {
449 match self {
450 Self::PushWithWal { format_version } => validate_format(*format_version),
451 Self::ReplayableSource {
452 format_version,
453 binding,
454 } if mode.is_state() => {
455 validate_format(*format_version)?;
456 let _ = binding;
457 Err(SourceContractError::UnsupportedMode(mode))
458 }
459 Self::ReplayableSource {
460 format_version,
461 binding,
462 } => {
463 validate_format(*format_version)?;
464 binding.validate()
465 }
466 }
467 }
468}
469
470#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
471#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
472pub enum SourceTransactionMembershipV1 {
473 Standalone,
474 Transaction {
475 transaction_id: SourceTransactionId,
476 member_index: u32,
477 member_count: NonZeroU32,
478 },
479}
480
481#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct RawSourceRecordV1 {
484 pub source_incarnation: SourceIncarnation,
485 pub partition_id: SourcePartitionId,
486 pub partition_incarnation: SourcePartitionIncarnation,
487 pub position: SourceCursorV1,
488 pub next_position: SourceCursorV1,
489 pub operation_id: SourceOperationId,
490 pub transaction: SourceTransactionMembershipV1,
491 pub key: Vec<u8>,
492 pub payload: Vec<u8>,
493}
494
495impl RawSourceRecordV1 {
496 pub fn validate(&self) -> Result<(), SourceContractError> {
497 self.position.validate()?;
498 self.next_position.validate()?;
499 let bytes = self
500 .key
501 .len()
502 .checked_add(self.payload.len())
503 .ok_or(SourceContractError::ArithmeticOverflow)?;
504 if bytes > MAX_SOURCE_RECORD_BYTES {
505 return Err(SourceContractError::Budget {
506 field: "source record bytes",
507 observed: bytes,
508 maximum: MAX_SOURCE_RECORD_BYTES,
509 });
510 }
511 if self.next_position.next_ordinal <= self.position.next_ordinal {
512 return Err(SourceContractError::Violation(
513 "a source record must advance its exclusive cursor",
514 ));
515 }
516 if let SourceTransactionMembershipV1::Transaction {
517 member_index,
518 member_count,
519 ..
520 } = self.transaction
521 && member_index >= member_count.get()
522 {
523 return Err(SourceContractError::Violation(
524 "source transaction member index is outside member count",
525 ));
526 }
527 Ok(())
528 }
529}
530
531#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
532#[serde(deny_unknown_fields)]
533pub struct SourceBatchV1 {
534 pub format_version: u16,
535 pub batch_id: SourceBatchId,
536 pub expected_cut: SourceCutV1,
537 pub replacement_cut: SourceCutV1,
538 pub records: Vec<RawSourceRecordV1>,
539}
540
541impl SourceBatchV1 {
542 pub fn validate(&self) -> Result<(), SourceContractError> {
543 validate_format(self.format_version)?;
544 self.expected_cut.advances_to(&self.replacement_cut)?;
545 if self.records.is_empty() || self.records.len() > MAX_SOURCE_BATCH_RECORDS {
546 return Err(SourceContractError::Budget {
547 field: "source batch records",
548 observed: self.records.len(),
549 maximum: MAX_SOURCE_BATCH_RECORDS,
550 });
551 }
552 let encoded = serde_json::to_vec(self).map_err(SourceContractError::serde)?;
553 if encoded.len() > MAX_SOURCE_BATCH_BYTES {
554 return Err(SourceContractError::Budget {
555 field: "source batch bytes",
556 observed: encoded.len(),
557 maximum: MAX_SOURCE_BATCH_BYTES,
558 });
559 }
560
561 let mut cursors = self
562 .expected_cut
563 .partitions
564 .iter()
565 .map(|checkpoint| {
566 (
567 checkpoint.partition_id,
568 (checkpoint.partition_incarnation, checkpoint.cursor.clone()),
569 )
570 })
571 .collect::<BTreeMap<_, _>>();
572 let mut operations = BTreeSet::new();
573 let mut transactions = BTreeMap::<SourceTransactionId, (u32, BTreeSet<u32>)>::new();
574 let mut prior_record_key = None;
575 for record in &self.records {
576 record.validate()?;
577 let record_key = (record.partition_id, record.position.next_ordinal, record.operation_id);
578 if prior_record_key.is_some_and(|prior| prior >= record_key) {
579 return Err(SourceContractError::Violation(
580 "source records must be strictly ordered by partition, position, and operation",
581 ));
582 }
583 prior_record_key = Some(record_key);
584 if record.source_incarnation != self.expected_cut.source_incarnation {
585 return Err(SourceContractError::Violation("source record incarnation mismatch"));
586 }
587 let (partition_incarnation, cursor) = cursors
588 .get_mut(&record.partition_id)
589 .ok_or(SourceContractError::Violation("source record has an unknown partition"))?;
590 if *partition_incarnation != record.partition_incarnation || *cursor != record.position {
591 return Err(SourceContractError::Violation(
592 "source records must form a gap-free cursor chain",
593 ));
594 }
595 *cursor = record.next_position.clone();
596 if !operations.insert(record.operation_id) {
597 return Err(SourceContractError::Violation(
598 "source operation identity was duplicated",
599 ));
600 }
601 if let SourceTransactionMembershipV1::Transaction {
602 transaction_id,
603 member_index,
604 member_count,
605 } = record.transaction
606 {
607 let entry = transactions
608 .entry(transaction_id)
609 .or_insert_with(|| (member_count.get(), BTreeSet::new()));
610 if entry.0 != member_count.get() || !entry.1.insert(member_index) {
611 return Err(SourceContractError::Violation(
612 "source transaction membership is inconsistent or duplicated",
613 ));
614 }
615 }
616 }
617 for (_, (member_count, members)) in transactions {
618 if members.len() != member_count as usize || members.iter().copied().ne(0..member_count) {
619 return Err(SourceContractError::Violation(
620 "a source batch must contain every member of each admitted transaction",
621 ));
622 }
623 }
624 for replacement in &self.replacement_cut.partitions {
625 let observed = cursors
626 .get(&replacement.partition_id)
627 .ok_or(SourceContractError::Violation(
628 "replacement cut has an unknown partition",
629 ))?;
630 if observed.0 != replacement.partition_incarnation || observed.1 != replacement.cursor {
631 return Err(SourceContractError::Violation(
632 "replacement cut must equal the exact end of the record chains",
633 ));
634 }
635 }
636 Ok(())
637 }
638}
639
640#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
641#[serde(deny_unknown_fields)]
642pub struct SourceShardPublicationV1 {
643 pub shard_key: ShardKey,
644 pub fence_token: FenceToken,
645 pub expected_generation: ManifestGeneration,
646 pub replacement_generation: ManifestGeneration,
647 pub replacement_root_digest: [u8; 32],
648}
649
650#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
651#[serde(deny_unknown_fields)]
652pub struct AtomicSourcePublicationV1 {
655 pub format_version: u16,
656 pub collection_index_ref: CollectionIndexRef,
657 pub source_incarnation: SourceIncarnation,
658 pub batch_id: SourceBatchId,
659 pub expected_cut: SourceCutV1,
660 pub replacement_cut: SourceCutV1,
661 pub operation_count: u32,
662 pub operation_set_digest: [u8; 32],
663 pub committed_transactions: Vec<SourceTransactionId>,
664 pub shard_publications: Vec<SourceShardPublicationV1>,
665}
666
667impl AtomicSourcePublicationV1 {
668 pub fn validate(&self) -> Result<(), SourceContractError> {
669 validate_format(self.format_version)?;
670 self.expected_cut.advances_to(&self.replacement_cut)?;
671 if self.source_incarnation != self.expected_cut.source_incarnation
672 || self.operation_count == 0
673 || self.operation_set_digest == [0; 32]
674 {
675 return Err(SourceContractError::Violation(
676 "atomic source publication identity, operation count, or operation digest is invalid",
677 ));
678 }
679 if self.shard_publications.is_empty() || self.shard_publications.len() > MAX_ATOMIC_SOURCE_SHARDS {
680 return Err(SourceContractError::Budget {
681 field: "atomic source shards",
682 observed: self.shard_publications.len(),
683 maximum: MAX_ATOMIC_SOURCE_SHARDS,
684 });
685 }
686 let mut prior_shard = None;
687 for publication in &self.shard_publications {
688 if publication.shard_key.collection_index_ref() != self.collection_index_ref
689 || prior_shard.is_some_and(|prior| prior >= publication.shard_key)
690 || publication.expected_generation.checked_next() != Some(publication.replacement_generation)
691 || publication.replacement_root_digest == [0; 32]
692 {
693 return Err(SourceContractError::Violation(
694 "source shard publications must be scoped, ordered, unique, and one-generation successors",
695 ));
696 }
697 prior_shard = Some(publication.shard_key);
698 }
699 if self.committed_transactions.windows(2).any(|pair| pair[0] >= pair[1]) {
700 return Err(SourceContractError::Violation(
701 "committed source transactions must be strictly ordered and unique",
702 ));
703 }
704 let encoded = serde_json::to_vec(self).map_err(SourceContractError::serde)?;
705 if encoded.len() > MAX_ATOMIC_SOURCE_COMMAND_BYTES {
706 return Err(SourceContractError::Budget {
707 field: "atomic source command bytes",
708 observed: encoded.len(),
709 maximum: MAX_ATOMIC_SOURCE_COMMAND_BYTES,
710 });
711 }
712 Ok(())
713 }
714
715 pub fn validate_against_batch(&self, batch: &SourceBatchV1) -> Result<(), SourceContractError> {
716 self.validate()?;
717 batch.validate()?;
718 if self.source_incarnation != batch.expected_cut.source_incarnation
719 || self.batch_id != batch.batch_id
720 || self.expected_cut != batch.expected_cut
721 || self.replacement_cut != batch.replacement_cut
722 || usize::try_from(self.operation_count).ok() != Some(batch.records.len())
723 || self.operation_set_digest != source_batch_operation_set_digest_v1(batch)?
724 || self.committed_transactions != source_batch_transaction_ids_v1(batch)?
725 {
726 return Err(SourceContractError::Violation(
727 "atomic source publication does not exactly summarize its validated source batch",
728 ));
729 }
730 Ok(())
731 }
732}
733
734pub fn source_batch_operation_set_digest_v1(batch: &SourceBatchV1) -> Result<[u8; 32], SourceContractError> {
735 batch.validate()?;
736 let mut hasher = blake3::Hasher::new();
737 hasher.update(b"lodestone:source-operation-set:v1\0");
738 for record in &batch.records {
739 let encoded = serde_json::to_vec(record).map_err(SourceContractError::serde)?;
740 let length = u64::try_from(encoded.len()).map_err(|_| SourceContractError::ArithmeticOverflow)?;
741 hasher.update(&length.to_be_bytes());
742 hasher.update(&encoded);
743 }
744 Ok(*hasher.finalize().as_bytes())
745}
746
747pub fn source_batch_transaction_ids_v1(batch: &SourceBatchV1) -> Result<Vec<SourceTransactionId>, SourceContractError> {
748 batch.validate()?;
749 Ok(batch
750 .records
751 .iter()
752 .filter_map(|record| match record.transaction {
753 SourceTransactionMembershipV1::Standalone => None,
754 SourceTransactionMembershipV1::Transaction { transaction_id, .. } => Some(transaction_id),
755 })
756 .collect::<BTreeSet<_>>()
757 .into_iter()
758 .collect())
759}
760
761#[derive(Clone, Debug, Eq, PartialEq)]
762pub struct SourceFetchRequestV1 {
763 pub cut: SourceCutV1,
764 pub maximum_records: usize,
765 pub maximum_bytes: usize,
766 pub deadline: Instant,
767}
768
769impl SourceFetchRequestV1 {
770 pub fn validate(&self, now: Instant) -> Result<(), SourceContractError> {
771 self.cut.validate()?;
772 if self.maximum_records == 0 || self.maximum_records > MAX_SOURCE_BATCH_RECORDS {
773 return Err(SourceContractError::Budget {
774 field: "fetch records",
775 observed: self.maximum_records,
776 maximum: MAX_SOURCE_BATCH_RECORDS,
777 });
778 }
779 if self.maximum_bytes == 0 || self.maximum_bytes > MAX_SOURCE_BATCH_BYTES {
780 return Err(SourceContractError::Budget {
781 field: "fetch bytes",
782 observed: self.maximum_bytes,
783 maximum: MAX_SOURCE_BATCH_BYTES,
784 });
785 }
786 if self.deadline <= now {
787 return Err(SourceContractError::DeadlineElapsed);
788 }
789 Ok(())
790 }
791}
792
793#[derive(Clone, Debug, Eq, PartialEq)]
794pub enum SourceHighWatermarkV1 {
795 Known { cursor: SourceCursorV1 },
796 Unknown,
797}
798
799#[derive(Clone, Debug, Eq, PartialEq)]
800pub struct SourceFetchResponseV1 {
801 pub batch: SourceBatchV1,
802 pub high_watermarks: Vec<(SourcePartitionId, SourceHighWatermarkV1)>,
803 pub retention_floors: Vec<(SourcePartitionId, SourceCursorV1)>,
804}
805
806impl SourceFetchResponseV1 {
807 pub fn validate(&self) -> Result<(), SourceContractError> {
808 self.batch.validate()?;
809 if self.high_watermarks.len() != self.batch.replacement_cut.partitions.len() {
810 return Err(SourceContractError::Violation(
811 "source response must report one watermark state per partition",
812 ));
813 }
814 if self.retention_floors.len() != self.batch.expected_cut.partitions.len() {
815 return Err(SourceContractError::Violation(
816 "source response must report one retention floor per partition",
817 ));
818 }
819 for (reported, checkpoint) in self.high_watermarks.iter().zip(&self.batch.replacement_cut.partitions) {
820 if reported.0 != checkpoint.partition_id {
821 return Err(SourceContractError::Violation(
822 "source watermarks must follow the complete ordered partition cut",
823 ));
824 }
825 if let SourceHighWatermarkV1::Known { cursor } = &reported.1 {
826 cursor.validate()?;
827 if cursor.next_ordinal < checkpoint.cursor.next_ordinal
828 || (cursor.next_ordinal == checkpoint.cursor.next_ordinal
829 && cursor.resume_token != checkpoint.cursor.resume_token)
830 {
831 return Err(SourceContractError::Violation(
832 "known source watermark cannot precede the fetched replacement cut",
833 ));
834 }
835 }
836 }
837 for (reported, checkpoint) in self.retention_floors.iter().zip(&self.batch.expected_cut.partitions) {
838 reported.1.validate()?;
839 if reported.0 != checkpoint.partition_id
840 || reported.1.next_ordinal > checkpoint.cursor.next_ordinal
841 || (reported.1.next_ordinal == checkpoint.cursor.next_ordinal
842 && reported.1.resume_token != checkpoint.cursor.resume_token)
843 {
844 return Err(SourceContractError::Violation(
845 "source retention floors must cover the complete requested cut",
846 ));
847 }
848 }
849 Ok(())
850 }
851}
852
853#[derive(Clone, Debug, Eq, PartialEq)]
854pub enum DecodedSourceMutationV1 {
855 Control { operation_id: SourceOperationId },
858 Put {
859 operation_id: SourceOperationId,
860 document_key: Vec<u8>,
861 source: Vec<u8>,
862 },
863 Delete {
864 operation_id: SourceOperationId,
865 document_key: Vec<u8>,
866 },
867}
868
869pub type SourceProviderFuture<'a> =
870 Pin<Box<dyn Future<Output = Result<SourceFetchResponseV1, SourceProviderError>> + Send + 'a>>;
871
872pub trait ReplayableSourceProvider: Send {
874 fn provider_kind(&self) -> SourceProviderKindV1;
875 fn fetch<'a>(&'a mut self, request: &'a SourceFetchRequestV1) -> SourceProviderFuture<'a>;
876}
877
878pub trait SourceRecordDecoder: Send + Sync {
880 fn decoder_kind(&self) -> SourceDecoderKindV1;
881 fn decode(&self, record: &RawSourceRecordV1) -> Result<Vec<DecodedSourceMutationV1>, SourceDecodeError>;
882}
883
884#[derive(Clone, Debug, Eq, PartialEq)]
885pub enum SourceProviderError {
886 Contract(SourceContractError),
887 TemporarilyUnavailable,
888 AuthenticationRefused,
889 SourceIdentityChanged,
890 AmbiguousPosition(SourcePartitionId),
891 RetentionExpired {
892 partition_id: SourcePartitionId,
893 requested: SourceCursorV1,
894 earliest_available: SourceCursorV1,
895 },
896}
897
898impl Display for SourceProviderError {
899 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
900 match self {
901 Self::Contract(error) => Display::fmt(error, formatter),
902 Self::TemporarilyUnavailable => formatter.write_str("replay source is temporarily unavailable"),
903 Self::AuthenticationRefused => formatter.write_str("replay source authentication was refused"),
904 Self::SourceIdentityChanged => formatter.write_str("replay source identity changed"),
905 Self::AmbiguousPosition(partition_id) => {
906 write!(
907 formatter,
908 "replay source position is ambiguous for partition {partition_id}"
909 )
910 }
911 Self::RetentionExpired {
912 partition_id,
913 requested,
914 earliest_available,
915 } => write!(
916 formatter,
917 "replay source retention expired for partition {partition_id}: requested {}, earliest available {}",
918 requested.next_ordinal, earliest_available.next_ordinal
919 ),
920 }
921 }
922}
923
924impl Error for SourceProviderError {}
925
926impl From<SourceContractError> for SourceProviderError {
927 fn from(error: SourceContractError) -> Self {
928 Self::Contract(error)
929 }
930}
931
932#[derive(Clone, Debug, Eq, PartialEq)]
933pub enum SourceDecodeError {
934 Contract(SourceContractError),
935 UnsupportedEnvelopeVersion(u32),
936 MissingTransactionMetadata(SourceOperationId),
937 PoisonRecord(SourceOperationId),
938 SchemaRefused(SourceOperationId),
939}
940
941impl Display for SourceDecodeError {
942 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
943 match self {
944 Self::Contract(error) => Display::fmt(error, formatter),
945 Self::UnsupportedEnvelopeVersion(version) => {
946 write!(formatter, "unsupported source envelope version {version}")
947 }
948 Self::MissingTransactionMetadata(operation_id) => {
949 write!(
950 formatter,
951 "source operation {operation_id} is missing transaction metadata"
952 )
953 }
954 Self::PoisonRecord(operation_id) => write!(formatter, "source operation {operation_id} is malformed"),
955 Self::SchemaRefused(operation_id) => {
956 write!(
957 formatter,
958 "source operation {operation_id} was refused by the index schema"
959 )
960 }
961 }
962 }
963}
964
965impl Error for SourceDecodeError {}
966
967impl From<SourceContractError> for SourceDecodeError {
968 fn from(error: SourceContractError) -> Self {
969 Self::Contract(error)
970 }
971}
972
973pub fn encode_source_binding_v1(binding: &SourceBindingV1) -> Result<Vec<u8>, SourceContractError> {
974 binding.validate()?;
975 encode_with_budget(binding, "source configuration bytes", MAX_SOURCE_CONFIGURATION_BYTES)
976}
977
978pub fn encode_ingest_authority_v1(authority: &IngestAuthorityV1) -> Result<Vec<u8>, SourceContractError> {
979 authority.validate_for_mode(IndexMode::Mutable)?;
980 encode_with_budget(authority, "source configuration bytes", MAX_SOURCE_CONFIGURATION_BYTES)
981}
982
983pub fn decode_ingest_authority_v1(bytes: &[u8]) -> Result<IngestAuthorityV1, SourceContractError> {
984 decode_current(
985 bytes,
986 "source configuration bytes",
987 MAX_SOURCE_CONFIGURATION_BYTES,
988 |authority: &IngestAuthorityV1| authority.validate_for_mode(IndexMode::Mutable),
989 )
990}
991
992pub fn decode_source_binding_v1(bytes: &[u8]) -> Result<SourceBindingV1, SourceContractError> {
993 decode_current(
994 bytes,
995 "source configuration bytes",
996 MAX_SOURCE_CONFIGURATION_BYTES,
997 SourceBindingV1::validate,
998 )
999}
1000
1001pub fn encode_source_batch_v1(batch: &SourceBatchV1) -> Result<Vec<u8>, SourceContractError> {
1002 batch.validate()?;
1003 serde_json::to_vec(batch).map_err(SourceContractError::serde)
1004}
1005
1006pub fn decode_source_batch_v1(bytes: &[u8]) -> Result<SourceBatchV1, SourceContractError> {
1007 decode_current(
1008 bytes,
1009 "source batch bytes",
1010 MAX_SOURCE_BATCH_BYTES,
1011 SourceBatchV1::validate,
1012 )
1013}
1014
1015pub fn encode_atomic_source_publication_v1(commit: &AtomicSourcePublicationV1) -> Result<Vec<u8>, SourceContractError> {
1016 commit.validate()?;
1017 serde_json::to_vec(commit).map_err(SourceContractError::serde)
1018}
1019
1020pub fn decode_atomic_source_publication_v1(bytes: &[u8]) -> Result<AtomicSourcePublicationV1, SourceContractError> {
1021 decode_current(
1022 bytes,
1023 "atomic source command bytes",
1024 MAX_ATOMIC_SOURCE_COMMAND_BYTES,
1025 AtomicSourcePublicationV1::validate,
1026 )
1027}
1028
1029fn decode_current<T>(
1030 bytes: &[u8],
1031 budget_field: &'static str,
1032 maximum_bytes: usize,
1033 validate: impl FnOnce(&T) -> Result<(), SourceContractError>,
1034) -> Result<T, SourceContractError>
1035where
1036 T: DeserializeOwned + Serialize,
1037{
1038 #[derive(Deserialize)]
1039 struct FormatProbe {
1040 format_version: Option<u16>,
1041 }
1042
1043 validate_encoded_budget(bytes.len(), budget_field, maximum_bytes)?;
1044 let probe: FormatProbe = serde_json::from_slice(bytes).map_err(SourceContractError::serde)?;
1045 let version = probe.format_version.ok_or(SourceContractError::MissingFormatVersion)?;
1046 validate_format(version)?;
1047 let decoded: T = serde_json::from_slice(bytes).map_err(SourceContractError::serde)?;
1050 validate(&decoded)?;
1051 let canonical = serde_json::to_vec(&decoded).map_err(SourceContractError::serde)?;
1052 if canonical != bytes {
1053 return Err(SourceContractError::NonCanonical);
1054 }
1055 Ok(decoded)
1056}
1057
1058fn encode_with_budget<T: Serialize>(
1059 value: &T,
1060 budget_field: &'static str,
1061 maximum_bytes: usize,
1062) -> Result<Vec<u8>, SourceContractError> {
1063 let encoded = serde_json::to_vec(value).map_err(SourceContractError::serde)?;
1064 validate_encoded_budget(encoded.len(), budget_field, maximum_bytes)?;
1065 Ok(encoded)
1066}
1067
1068fn validate_encoded_budget(observed: usize, field: &'static str, maximum: usize) -> Result<(), SourceContractError> {
1069 if observed > maximum {
1070 return Err(SourceContractError::Budget {
1071 field,
1072 observed,
1073 maximum,
1074 });
1075 }
1076 Ok(())
1077}
1078
1079fn validate_format(version: u16) -> Result<(), SourceContractError> {
1080 if version != REPLAYABLE_SOURCE_FORMAT_VERSION {
1081 return Err(SourceContractError::UnsupportedFormat(version));
1082 }
1083 Ok(())
1084}
1085
1086#[derive(Clone, Debug, Eq, PartialEq)]
1087pub enum SourceContractError {
1088 UnsupportedFormat(u16),
1089 MissingFormatVersion,
1090 NonCanonical,
1091 ZeroIdentity(&'static str),
1092 UnsupportedMode(IndexMode),
1093 Budget {
1094 field: &'static str,
1095 observed: usize,
1096 maximum: usize,
1097 },
1098 Violation(&'static str),
1099 DeadlineElapsed,
1100 ArithmeticOverflow,
1101 Serde(String),
1102}
1103
1104impl SourceContractError {
1105 fn serde(error: serde_json::Error) -> Self {
1106 Self::Serde(error.to_string())
1107 }
1108}
1109
1110impl Display for SourceContractError {
1111 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1112 match self {
1113 Self::UnsupportedFormat(version) => write!(formatter, "unsupported replayable-source format {version}"),
1114 Self::MissingFormatVersion => formatter.write_str("replayable-source format version is missing"),
1115 Self::NonCanonical => formatter.write_str("replayable-source bytes are not canonical current format"),
1116 Self::ZeroIdentity(kind) => write!(formatter, "{kind} cannot use the reserved zero identity"),
1117 Self::UnsupportedMode(mode) => write!(
1118 formatter,
1119 "replayable-source authority is unavailable for {mode:?} indexes"
1120 ),
1121 Self::Budget {
1122 field,
1123 observed,
1124 maximum,
1125 } => {
1126 write!(formatter, "{field} {observed} is outside 1..={maximum}")
1127 }
1128 Self::Violation(message) => formatter.write_str(message),
1129 Self::DeadlineElapsed => formatter.write_str("source fetch deadline elapsed"),
1130 Self::ArithmeticOverflow => formatter.write_str("source contract arithmetic overflowed"),
1131 Self::Serde(message) => write!(formatter, "replayable-source codec refused input: {message}"),
1132 }
1133 }
1134}
1135
1136impl Error for SourceContractError {}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141 use crate::{CollectionId, Epoch, IndexId, RecoveryEra, ShardId};
1142
1143 fn scope() -> Result<CollectionIndexRef, Box<dyn Error>> {
1144 Ok(CollectionIndexRef::new(CollectionId::new(1)?, IndexId::new(2)?))
1145 }
1146
1147 fn cursor(ordinal: u64) -> SourceCursorV1 {
1148 SourceCursorV1 {
1149 next_ordinal: SourceOrdinal::new(ordinal),
1150 resume_token: ordinal.to_be_bytes().to_vec(),
1151 }
1152 }
1153
1154 fn cut(ordinal: u64) -> Result<SourceCutV1, Box<dyn Error>> {
1155 Ok(SourceCutV1 {
1156 format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1157 source_incarnation: SourceIncarnation::new(1)?,
1158 partitions: vec![SourcePartitionCheckpointV1 {
1159 partition_id: SourcePartitionId::new(0),
1160 partition_incarnation: SourcePartitionIncarnation::new(1)?,
1161 cursor: cursor(ordinal),
1162 }],
1163 })
1164 }
1165
1166 fn batch() -> Result<SourceBatchV1, Box<dyn Error>> {
1167 Ok(SourceBatchV1 {
1168 format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1169 batch_id: SourceBatchId::new(1)?,
1170 expected_cut: cut(0)?,
1171 replacement_cut: cut(1)?,
1172 records: vec![RawSourceRecordV1 {
1173 source_incarnation: SourceIncarnation::new(1)?,
1174 partition_id: SourcePartitionId::new(0),
1175 partition_incarnation: SourcePartitionIncarnation::new(1)?,
1176 position: cursor(0),
1177 next_position: cursor(1),
1178 operation_id: SourceOperationId::new(1)?,
1179 transaction: SourceTransactionMembershipV1::Standalone,
1180 key: b"doc-1".to_vec(),
1181 payload: br#"{"value":1}"#.to_vec(),
1182 }],
1183 })
1184 }
1185
1186 #[test]
1187 fn current_batch_round_trips_exactly() -> Result<(), Box<dyn Error>> {
1188 let expected = batch()?;
1189 let bytes = encode_source_batch_v1(&expected)?;
1190 assert_eq!(decode_source_batch_v1(&bytes)?, expected);
1191 Ok(())
1192 }
1193
1194 #[test]
1195 fn full_width_source_identities_use_canonical_decimal_strings() -> Result<(), Box<dyn Error>> {
1196 let operation_id = SourceOperationId::new(u128::MAX)?;
1197 assert_eq!(serde_json::to_string(&operation_id)?, format!("\"{}\"", u128::MAX));
1198 assert_eq!(
1199 serde_json::from_str::<SourceOperationId>(&format!("\"{}\"", u128::MAX))?,
1200 operation_id
1201 );
1202
1203 for refused in ["0", "01", "+1", " 1", "1 ", "340282366920938463463374607431768211456"] {
1204 assert!(serde_json::from_str::<SourceBatchId>(&format!("\"{refused}\"")).is_err());
1205 }
1206 assert!(serde_json::from_str::<SourceTransactionId>("1").is_err());
1207
1208 let mut expected = batch()?;
1209 expected.batch_id = SourceBatchId::new(u128::from(u64::MAX) + 1)?;
1210 expected.records[0].operation_id = SourceOperationId::new(u128::MAX)?;
1211 let bytes = encode_source_batch_v1(&expected)?;
1212 assert_eq!(decode_source_batch_v1(&bytes)?, expected);
1213 Ok(())
1214 }
1215
1216 #[test]
1217 fn newer_unknown_and_noncanonical_inputs_refuse() -> Result<(), Box<dyn Error>> {
1218 let bytes = encode_source_batch_v1(&batch()?)?;
1219 let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
1220 value["format_version"] = serde_json::Value::from(2);
1221 let newer = serde_json::to_vec(&value)?;
1222 assert_eq!(
1223 decode_source_batch_v1(&newer),
1224 Err(SourceContractError::UnsupportedFormat(2))
1225 );
1226
1227 let mut older: serde_json::Value = serde_json::from_slice(&bytes)?;
1228 older["format_version"] = serde_json::Value::from(0);
1229 assert_eq!(
1230 decode_source_batch_v1(&serde_json::to_vec(&older)?),
1231 Err(SourceContractError::UnsupportedFormat(0))
1232 );
1233
1234 let mut missing: serde_json::Value = serde_json::from_slice(&bytes)?;
1235 missing
1236 .as_object_mut()
1237 .ok_or("source batch must encode as an object")?
1238 .remove("format_version");
1239 assert_eq!(
1240 decode_source_batch_v1(&serde_json::to_vec(&missing)?),
1241 Err(SourceContractError::MissingFormatVersion)
1242 );
1243
1244 let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
1245 value["unknown"] = serde_json::Value::Bool(true);
1246 let unknown = serde_json::to_vec(&value)?;
1247 assert!(matches!(
1248 decode_source_batch_v1(&unknown),
1249 Err(SourceContractError::Serde(_))
1250 ));
1251
1252 let spaced = [b" ".as_slice(), bytes.as_slice()].concat();
1253 assert_eq!(decode_source_batch_v1(&spaced), Err(SourceContractError::NonCanonical));
1254
1255 let text = String::from_utf8(bytes)?;
1256 let duplicate = text.replacen("\"batch_id\":\"1\"", "\"batch_id\":\"1\",\"batch_id\":\"1\"", 1);
1257 if duplicate == text {
1258 return Err("batch id was not present in canonical bytes".into());
1259 }
1260 assert!(matches!(
1261 decode_source_batch_v1(duplicate.as_bytes()),
1262 Err(SourceContractError::Serde(_))
1263 ));
1264 Ok(())
1265 }
1266
1267 #[test]
1268 fn incomplete_transaction_and_duplicate_operation_refuse() -> Result<(), Box<dyn Error>> {
1269 let mut incomplete = batch()?;
1270 incomplete.records[0].transaction = SourceTransactionMembershipV1::Transaction {
1271 transaction_id: SourceTransactionId::new(1)?,
1272 member_index: 0,
1273 member_count: NonZeroU32::new(2).ok_or("nonzero member count")?,
1274 };
1275 assert!(matches!(incomplete.validate(), Err(SourceContractError::Violation(_))));
1276
1277 let mut duplicate = batch()?;
1278 let mut second = duplicate.records[0].clone();
1279 second.position = cursor(1);
1280 second.next_position = cursor(2);
1281 duplicate.replacement_cut = cut(2)?;
1282 duplicate.records.push(second);
1283 assert_eq!(
1284 duplicate.validate(),
1285 Err(SourceContractError::Violation(
1286 "source operation identity was duplicated"
1287 ))
1288 );
1289 Ok(())
1290 }
1291
1292 #[test]
1293 fn replay_authority_mode_matrix_is_explicit() -> Result<(), Box<dyn Error>> {
1294 let binding = SourceBindingV1 {
1295 format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1296 collection_index_ref: scope()?,
1297 source_incarnation: SourceIncarnation::new(1)?,
1298 source_identity_digest: [1; 32],
1299 provider: SourceProviderKindV1::Kafka,
1300 decoder: SourceDecoderKindV1::DebeziumPostgresJson,
1301 partitions: vec![SourcePartitionBindingV1 {
1302 partition_id: SourcePartitionId::new(0),
1303 partition_incarnation: SourcePartitionIncarnation::new(1)?,
1304 }],
1305 guarantees: SourceReplayGuaranteesV1 {
1306 durable_replay: true,
1307 stable_partition_identity: true,
1308 stable_operation_identity: true,
1309 ordered_positions: true,
1310 bounded_reads: true,
1311 explicit_retention: true,
1312 explicit_rewind: true,
1313 minimum_replay_window_millis: 86_400_000,
1314 },
1315 };
1316 binding.validate_cut(&cut(0)?)?;
1317 let authority = IngestAuthorityV1::replayable_source(binding);
1318 assert_eq!(authority.validate_for_mode(IndexMode::Mutable), Ok(()));
1319 assert_eq!(authority.validate_for_mode(IndexMode::AppendLog), Ok(()));
1320 assert_eq!(
1321 authority.validate_for_mode(IndexMode::State),
1322 Err(SourceContractError::UnsupportedMode(IndexMode::State))
1323 );
1324 assert_eq!(
1325 IngestAuthorityV1::push_with_wal().validate_for_mode(IndexMode::State),
1326 Ok(())
1327 );
1328 let encoded = encode_ingest_authority_v1(&authority)?;
1329 assert_eq!(decode_ingest_authority_v1(&encoded)?, authority);
1330 Ok(())
1331 }
1332
1333 #[test]
1334 fn same_ordinal_token_drift_is_not_progress() -> Result<(), Box<dyn Error>> {
1335 let expected = cut(1)?;
1336 let mut replacement = cut(1)?;
1337 replacement.partitions[0].cursor.resume_token = b"different".to_vec();
1338 assert!(matches!(
1339 expected.advances_to(&replacement),
1340 Err(SourceContractError::Violation(_))
1341 ));
1342 Ok(())
1343 }
1344
1345 #[test]
1346 fn fetch_response_requires_exact_watermark_and_retention_partition_sets() -> Result<(), Box<dyn Error>> {
1347 let mut response = SourceFetchResponseV1 {
1348 batch: batch()?,
1349 high_watermarks: vec![(
1350 SourcePartitionId::new(0),
1351 SourceHighWatermarkV1::Known { cursor: cursor(2) },
1352 )],
1353 retention_floors: vec![(SourcePartitionId::new(0), cursor(0))],
1354 };
1355 response.validate()?;
1356 response.retention_floors[0].1 = cursor(1);
1357 assert!(matches!(response.validate(), Err(SourceContractError::Violation(_))));
1358 Ok(())
1359 }
1360
1361 #[test]
1362 fn atomic_cut_requires_one_scoped_successor_per_shard() -> Result<(), Box<dyn Error>> {
1363 let scope = scope()?;
1364 let commit = AtomicSourcePublicationV1 {
1365 format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1366 collection_index_ref: scope,
1367 source_incarnation: SourceIncarnation::new(1)?,
1368 batch_id: SourceBatchId::new(1)?,
1369 expected_cut: cut(0)?,
1370 replacement_cut: cut(1)?,
1371 operation_count: 1,
1372 operation_set_digest: source_batch_operation_set_digest_v1(&batch()?)?,
1373 committed_transactions: Vec::new(),
1374 shard_publications: vec![SourceShardPublicationV1 {
1375 shard_key: ShardKey::new(scope, ShardId::new(1)),
1376 fence_token: FenceToken::new(RecoveryEra::new(1)?, Epoch::new(1))?,
1377 expected_generation: ManifestGeneration::new(4),
1378 replacement_generation: ManifestGeneration::new(5),
1379 replacement_root_digest: [3; 32],
1380 }],
1381 };
1382 let bytes = encode_atomic_source_publication_v1(&commit)?;
1383 assert_eq!(decode_atomic_source_publication_v1(&bytes)?, commit);
1384 commit.validate_against_batch(&batch()?)?;
1385 Ok(())
1386 }
1387
1388 #[test]
1389 fn oversized_inputs_refuse_before_json_decode() {
1390 let oversized_configuration = vec![b' '; MAX_SOURCE_CONFIGURATION_BYTES + 1];
1391 assert_eq!(
1392 decode_source_binding_v1(&oversized_configuration),
1393 Err(SourceContractError::Budget {
1394 field: "source configuration bytes",
1395 observed: MAX_SOURCE_CONFIGURATION_BYTES + 1,
1396 maximum: MAX_SOURCE_CONFIGURATION_BYTES,
1397 })
1398 );
1399
1400 let oversized_batch = vec![b' '; MAX_SOURCE_BATCH_BYTES + 1];
1401 assert_eq!(
1402 decode_source_batch_v1(&oversized_batch),
1403 Err(SourceContractError::Budget {
1404 field: "source batch bytes",
1405 observed: MAX_SOURCE_BATCH_BYTES + 1,
1406 maximum: MAX_SOURCE_BATCH_BYTES,
1407 })
1408 );
1409
1410 let oversized_command = vec![b' '; MAX_ATOMIC_SOURCE_COMMAND_BYTES + 1];
1411 assert_eq!(
1412 decode_atomic_source_publication_v1(&oversized_command),
1413 Err(SourceContractError::Budget {
1414 field: "atomic source command bytes",
1415 observed: MAX_ATOMIC_SOURCE_COMMAND_BYTES + 1,
1416 maximum: MAX_ATOMIC_SOURCE_COMMAND_BYTES,
1417 })
1418 );
1419 }
1420}