1use fsqlite_types::sync_primitives::Instant;
15use std::fmt;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use fsqlite_error::{FrankenError, Result};
19use fsqlite_types::{ObjectId, cx::Cx};
20use tracing::{debug, error, info, warn};
21use xxhash_rust::xxh3::xxh3_64;
22
23use crate::decode_proofs::EcsDecodeProof;
24
25const BEAD_ID: &str = "bd-1hi.5";
26
27pub struct RaptorQMetrics {
37 pub encoded_bytes_total: AtomicU64,
39 pub repair_symbols_generated_total: AtomicU64,
41 pub decoded_bytes_total: AtomicU64,
43 pub encode_ops: AtomicU64,
45 pub decode_ops: AtomicU64,
47 pub decode_failures: AtomicU64,
49}
50
51impl RaptorQMetrics {
52 #[must_use]
55 pub const fn new() -> Self {
56 Self {
57 encoded_bytes_total: AtomicU64::new(0),
58 repair_symbols_generated_total: AtomicU64::new(0),
59 decoded_bytes_total: AtomicU64::new(0),
60 encode_ops: AtomicU64::new(0),
61 decode_ops: AtomicU64::new(0),
62 decode_failures: AtomicU64::new(0),
63 }
64 }
65
66 pub fn record_encode(&self, encoded_bytes: u64, repair_symbols: u64) {
68 self.encoded_bytes_total
69 .fetch_add(encoded_bytes, Ordering::Relaxed);
70 self.repair_symbols_generated_total
71 .fetch_add(repair_symbols, Ordering::Relaxed);
72 self.encode_ops.fetch_add(1, Ordering::Relaxed);
73 }
74
75 pub fn record_decode_success(&self, decoded_bytes: u64) {
77 self.decoded_bytes_total
78 .fetch_add(decoded_bytes, Ordering::Relaxed);
79 self.decode_ops.fetch_add(1, Ordering::Relaxed);
80 }
81
82 pub fn record_decode_failure(&self) {
84 self.decode_ops.fetch_add(1, Ordering::Relaxed);
85 self.decode_failures.fetch_add(1, Ordering::Relaxed);
86 }
87
88 #[must_use]
90 pub fn snapshot(&self) -> RaptorQMetricsSnapshot {
91 RaptorQMetricsSnapshot {
92 encoded_bytes_total: self.encoded_bytes_total.load(Ordering::Relaxed),
93 repair_symbols_generated_total: self
94 .repair_symbols_generated_total
95 .load(Ordering::Relaxed),
96 decoded_bytes_total: self.decoded_bytes_total.load(Ordering::Relaxed),
97 encode_ops: self.encode_ops.load(Ordering::Relaxed),
98 decode_ops: self.decode_ops.load(Ordering::Relaxed),
99 decode_failures: self.decode_failures.load(Ordering::Relaxed),
100 }
101 }
102
103 pub fn reset(&self) {
105 self.encoded_bytes_total.store(0, Ordering::Relaxed);
106 self.repair_symbols_generated_total
107 .store(0, Ordering::Relaxed);
108 self.decoded_bytes_total.store(0, Ordering::Relaxed);
109 self.encode_ops.store(0, Ordering::Relaxed);
110 self.decode_ops.store(0, Ordering::Relaxed);
111 self.decode_failures.store(0, Ordering::Relaxed);
112 }
113}
114
115impl Default for RaptorQMetrics {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121pub static GLOBAL_RAPTORQ_METRICS: RaptorQMetrics = RaptorQMetrics::new();
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct RaptorQMetricsSnapshot {
127 pub encoded_bytes_total: u64,
128 pub repair_symbols_generated_total: u64,
129 pub decoded_bytes_total: u64,
130 pub encode_ops: u64,
131 pub decode_ops: u64,
132 pub decode_failures: u64,
133}
134
135impl fmt::Display for RaptorQMetricsSnapshot {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(
138 f,
139 "raptorq: encoded={} bytes ({} ops, {} repair syms), decoded={} bytes ({} ops, {} failures)",
140 self.encoded_bytes_total,
141 self.encode_ops,
142 self.repair_symbols_generated_total,
143 self.decoded_bytes_total,
144 self.decode_ops,
145 self.decode_failures,
146 )
147 }
148}
149
150fn duration_us_saturating(d: std::time::Duration) -> u64 {
152 u64::try_from(d.as_micros()).unwrap_or(u64::MAX)
153}
154
155pub const MIN_PIPELINE_SYMBOL_SIZE: u32 = 512;
161
162pub const MAX_PIPELINE_SYMBOL_SIZE: u32 = 65_536;
164
165pub const DEFAULT_CHECKPOINT_INTERVAL: u32 = 64;
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct DecodeProofEmissionPolicy {
174 pub emit_on_decode_failure: bool,
176 pub emit_on_repair_success: bool,
178}
179
180impl DecodeProofEmissionPolicy {
181 #[must_use]
183 pub const fn disabled() -> Self {
184 Self {
185 emit_on_decode_failure: false,
186 emit_on_repair_success: false,
187 }
188 }
189
190 #[must_use]
192 pub const fn durability_critical() -> Self {
193 Self {
194 emit_on_decode_failure: true,
195 emit_on_repair_success: true,
196 }
197 }
198}
199
200impl Default for DecodeProofEmissionPolicy {
201 fn default() -> Self {
202 Self::disabled()
203 }
204}
205
206#[derive(Debug, Clone, PartialEq)]
211pub struct PipelineConfig {
212 pub symbol_size: u32,
215 pub max_block_size: u32,
217 pub repair_overhead: f64,
219 pub checkpoint_interval: u32,
221 pub decode_proof_policy: DecodeProofEmissionPolicy,
223}
224
225impl PipelineConfig {
226 #[must_use]
228 pub fn for_page_size(page_size: u32) -> Self {
229 Self {
230 symbol_size: page_size,
231 max_block_size: 64 * 1024,
232 repair_overhead: 1.25,
233 checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
234 decode_proof_policy: DecodeProofEmissionPolicy::default(),
235 }
236 }
237
238 pub fn validate(&self) -> Result<()> {
248 if self.symbol_size == 0 {
249 return Err(FrankenError::OutOfRange {
250 what: "pipeline symbol_size".to_owned(),
251 value: "0".to_owned(),
252 });
253 }
254 if !self.symbol_size.is_power_of_two() {
255 return Err(FrankenError::OutOfRange {
256 what: "pipeline symbol_size (must be power of 2)".to_owned(),
257 value: self.symbol_size.to_string(),
258 });
259 }
260 if self.symbol_size < MIN_PIPELINE_SYMBOL_SIZE
261 || self.symbol_size > MAX_PIPELINE_SYMBOL_SIZE
262 {
263 return Err(FrankenError::OutOfRange {
264 what: format!(
265 "pipeline symbol_size (must be in [{MIN_PIPELINE_SYMBOL_SIZE}, {MAX_PIPELINE_SYMBOL_SIZE}])"
266 ),
267 value: self.symbol_size.to_string(),
268 });
269 }
270 if self.max_block_size == 0 {
271 return Err(FrankenError::OutOfRange {
272 what: "pipeline max_block_size".to_owned(),
273 value: "0".to_owned(),
274 });
275 }
276 if self.repair_overhead < 1.0 {
277 return Err(FrankenError::OutOfRange {
278 what: "pipeline repair_overhead (must be >= 1.0)".to_owned(),
279 value: self.repair_overhead.to_string(),
280 });
281 }
282 if self.checkpoint_interval == 0 {
283 return Err(FrankenError::OutOfRange {
284 what: "pipeline checkpoint_interval".to_owned(),
285 value: "0".to_owned(),
286 });
287 }
288 Ok(())
289 }
290}
291
292impl Default for PipelineConfig {
293 fn default() -> Self {
294 Self::for_page_size(4096)
295 }
296}
297
298pub trait PageSymbolSink {
304 fn write_symbol(&mut self, esi: u32, data: &[u8]) -> Result<()>;
306
307 fn flush(&mut self) -> Result<()>;
309
310 fn written_count(&self) -> u32;
312}
313
314pub trait PageSymbolSource {
316 fn read_symbol(&mut self, esi: u32) -> Result<Option<Vec<u8>>>;
318
319 fn available_esis(&self) -> Vec<u32>;
321
322 fn available_count(&self) -> u32;
324}
325
326pub trait SymbolCodec: Send + Sync {
335 fn encode(
337 &self,
338 cx: &Cx,
339 source_data: &[u8],
340 symbol_size: u32,
341 repair_overhead: f64,
342 ) -> Result<CodecEncodeResult>;
343
344 fn decode(
346 &self,
347 cx: &Cx,
348 symbols: &[(u32, Vec<u8>)],
349 k_source: u32,
350 symbol_size: u32,
351 ) -> Result<CodecDecodeResult>;
352}
353
354#[derive(Debug, Clone)]
356pub struct CodecEncodeResult {
357 pub source_symbols: Vec<(u32, Vec<u8>)>,
359 pub repair_symbols: Vec<(u32, Vec<u8>)>,
361 pub k_source: u32,
363}
364
365#[derive(Debug, Clone)]
367pub enum CodecDecodeResult {
368 Success {
370 data: Vec<u8>,
372 symbols_used: u32,
374 peeled_count: u32,
376 inactivated_count: u32,
378 },
379 Failure {
381 reason: DecodeFailureReason,
383 symbols_received: u32,
385 k_required: u32,
387 },
388}
389
390#[derive(Debug, Clone)]
396pub struct EncodeOutcome {
397 pub source_count: u32,
399 pub repair_count: u32,
401 pub symbol_size: u32,
403}
404
405#[derive(Debug, Clone)]
407pub enum DecodeOutcome {
408 Success(DecodeSuccess),
410 Failure(DecodeFailure),
412}
413
414#[derive(Debug, Clone)]
416pub struct DecodeSuccess {
417 pub data: Vec<u8>,
419 pub symbols_used: u32,
421 pub peeled_count: u32,
423 pub inactivated_count: u32,
425 pub decode_proof: Option<EcsDecodeProof>,
427}
428
429#[derive(Debug, Clone)]
431pub struct DecodeFailure {
432 pub reason: DecodeFailureReason,
434 pub symbols_received: u32,
436 pub k_required: u32,
438 pub decode_proof: Option<EcsDecodeProof>,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum DecodeFailureReason {
445 InsufficientSymbols,
447 SingularMatrix,
449 SymbolSizeMismatch,
451 Cancelled,
453}
454
455pub struct RaptorQPageEncoder<C: SymbolCodec> {
462 config: PipelineConfig,
463 codec: C,
464}
465
466impl<C: SymbolCodec> RaptorQPageEncoder<C> {
467 pub fn new(config: PipelineConfig, codec: C) -> Result<Self> {
469 config.validate()?;
470 info!(
471 bead_id = BEAD_ID,
472 symbol_size = config.symbol_size,
473 max_block_size = config.max_block_size,
474 repair_overhead = config.repair_overhead,
475 "RaptorQ page encoder created"
476 );
477 Ok(Self { config, codec })
478 }
479
480 #[allow(clippy::cast_possible_truncation)]
486 pub fn encode_pages(
487 &self,
488 cx: &Cx,
489 page_data: &[u8],
490 sink: &mut dyn PageSymbolSink,
491 ) -> Result<EncodeOutcome> {
492 cx.checkpoint().map_err(|_| FrankenError::Abort)?;
493
494 let symbol_size = self.config.symbol_size;
495 let t0 = Instant::now();
496 debug!(
497 bead_id = BEAD_ID,
498 data_len = page_data.len(),
499 symbol_size,
500 "starting page encode"
501 );
502
503 let result = self
504 .codec
505 .encode(cx, page_data, symbol_size, self.config.repair_overhead)?;
506
507 let interval = self.config.checkpoint_interval as usize;
509 for (idx, (esi, data)) in result.source_symbols.iter().enumerate() {
510 if idx > 0 && idx % interval == 0 {
511 cx.checkpoint().map_err(|_| FrankenError::Abort)?;
512 }
513 sink.write_symbol(*esi, data)?;
514 }
515
516 for (idx, (esi, data)) in result.repair_symbols.iter().enumerate() {
518 if idx > 0 && idx % interval == 0 {
519 cx.checkpoint().map_err(|_| FrankenError::Abort)?;
520 }
521 sink.write_symbol(*esi, data)?;
522 }
523
524 sink.flush()?;
525
526 let outcome = EncodeOutcome {
527 source_count: result.k_source,
528 repair_count: result.repair_symbols.len() as u32,
529 symbol_size,
530 };
531
532 let encode_time_us = duration_us_saturating(t0.elapsed());
533
534 let span = tracing::span!(
536 tracing::Level::DEBUG,
537 "raptorq_encode",
538 source_symbols = outcome.source_count,
539 repair_symbols = outcome.repair_count,
540 encode_time_us,
541 encoded_bytes = page_data.len(),
542 symbol_size = outcome.symbol_size,
543 );
544 let _guard = span.enter();
545
546 info!(
547 bead_id = BEAD_ID,
548 source_count = outcome.source_count,
549 repair_count = outcome.repair_count,
550 symbol_size = outcome.symbol_size,
551 encode_time_us,
552 "page encode complete"
553 );
554
555 GLOBAL_RAPTORQ_METRICS
557 .record_encode(page_data.len() as u64, u64::from(outcome.repair_count));
558
559 Ok(outcome)
560 }
561
562 #[must_use]
564 pub const fn config(&self) -> &PipelineConfig {
565 &self.config
566 }
567}
568
569pub struct RaptorQPageDecoder<C: SymbolCodec> {
576 config: PipelineConfig,
577 codec: C,
578}
579
580impl<C: SymbolCodec> RaptorQPageDecoder<C> {
581 pub fn new(config: PipelineConfig, codec: C) -> Result<Self> {
583 config.validate()?;
584 info!(
585 bead_id = BEAD_ID,
586 symbol_size = config.symbol_size,
587 "RaptorQ page decoder created"
588 );
589 Ok(Self { config, codec })
590 }
591
592 #[allow(clippy::cast_possible_truncation, clippy::too_many_lines)]
599 pub fn decode_pages(
600 &self,
601 cx: &Cx,
602 source: &mut dyn PageSymbolSource,
603 k_source: u32,
604 ) -> Result<DecodeOutcome> {
605 cx.checkpoint().map_err(|_| FrankenError::Abort)?;
606 let t0 = Instant::now();
607
608 let available = source.available_count();
609 debug!(
610 bead_id = BEAD_ID,
611 k_source, available, "starting page decode"
612 );
613
614 if available < k_source {
615 warn!(
616 bead_id = BEAD_ID,
617 k_source, available, "fewer symbols than K_source — decode likely to fail"
618 );
619 }
620
621 let esis = source.available_esis();
623 let interval = self.config.checkpoint_interval as usize;
624 let mut symbols = Vec::with_capacity(esis.len());
625 for (idx, esi) in esis.iter().enumerate() {
626 if idx > 0 && idx % interval == 0 {
627 cx.checkpoint().map_err(|_| FrankenError::Abort)?;
628 }
629 if let Some(data) = source.read_symbol(*esi)? {
630 symbols.push((*esi, data));
631 }
632 }
633
634 let codec_result = self
636 .codec
637 .decode(cx, &symbols, k_source, self.config.symbol_size)?;
638 let all_esis = canonical_esis(&symbols);
639 let proof_object_id =
640 derive_decode_proof_object_id(k_source, self.config.symbol_size, &all_esis);
641 let proof_seed = xxh3_64(proof_object_id.as_bytes());
642
643 match codec_result {
644 CodecDecodeResult::Success {
645 data,
646 symbols_used,
647 peeled_count,
648 inactivated_count,
649 } => {
650 info!(
651 bead_id = BEAD_ID,
652 k_source,
653 symbols_used,
654 peeled_count,
655 inactivated_count,
656 "page decode succeeded"
657 );
658 let decode_proof = if self.config.decode_proof_policy.emit_on_repair_success
659 && contains_repair_esi(&all_esis, k_source)
660 {
661 let proof = EcsDecodeProof::from_esis(
662 proof_object_id,
663 k_source,
664 &all_esis,
665 true,
666 Some(symbols_used),
667 deterministic_timing_ns(k_source, self.config.symbol_size, symbols_used),
668 proof_seed,
669 );
670 debug!(
671 bead_id = "bd-faz4",
672 symbols_used, k_source, "emitted repair-success decode proof"
673 );
674 Some(proof)
675 } else {
676 None
677 };
678 if symbols_used == k_source {
679 warn!(
680 bead_id = BEAD_ID,
681 k_source,
682 symbols_used,
683 "fragile recovery: decoded with minimum symbol count"
684 );
685 }
686 let decoded_len = data.len() as u64;
687 let decode_time_us = duration_us_saturating(t0.elapsed());
688
689 let span = tracing::span!(
691 tracing::Level::DEBUG,
692 "raptorq_decode",
693 k_source,
694 symbols_used,
695 decoded_bytes = decoded_len,
696 decode_time_us,
697 ok = true,
698 );
699 let _guard = span.enter();
700
701 GLOBAL_RAPTORQ_METRICS.record_decode_success(decoded_len);
702
703 Ok(DecodeOutcome::Success(DecodeSuccess {
704 data,
705 symbols_used,
706 peeled_count,
707 inactivated_count,
708 decode_proof,
709 }))
710 }
711 CodecDecodeResult::Failure {
712 reason,
713 symbols_received,
714 k_required,
715 } => {
716 let decode_proof = if self.config.decode_proof_policy.emit_on_decode_failure {
717 let intermediate_rank = Some(symbols_received.min(k_required));
718 let proof = EcsDecodeProof::from_esis(
719 proof_object_id,
720 k_source,
721 &all_esis,
722 false,
723 intermediate_rank,
724 deterministic_timing_ns(
725 k_source,
726 self.config.symbol_size,
727 symbols_received,
728 ),
729 proof_seed,
730 );
731 debug!(
732 bead_id = "bd-faz4",
733 symbols_received, k_required, "emitted decode-failure proof"
734 );
735 Some(proof)
736 } else {
737 None
738 };
739 let decode_time_us = duration_us_saturating(t0.elapsed());
740
741 let span = tracing::span!(
743 tracing::Level::DEBUG,
744 "raptorq_decode",
745 k_source,
746 symbols_received,
747 k_required,
748 decode_time_us,
749 ok = false,
750 );
751 let _guard = span.enter();
752
753 error!(
754 bead_id = BEAD_ID,
755 k_source,
756 symbols_received,
757 k_required,
758 reason = ?reason,
759 "page decode failed"
760 );
761
762 GLOBAL_RAPTORQ_METRICS.record_decode_failure();
763
764 Ok(DecodeOutcome::Failure(DecodeFailure {
765 reason,
766 symbols_received,
767 k_required,
768 decode_proof,
769 }))
770 }
771 }
772 }
773
774 #[must_use]
776 pub const fn config(&self) -> &PipelineConfig {
777 &self.config
778 }
779}
780
781fn canonical_esis(symbols: &[(u32, Vec<u8>)]) -> Vec<u32> {
782 let mut esis: Vec<u32> = symbols.iter().map(|(esi, _)| *esi).collect();
783 esis.sort_unstable();
784 esis.dedup();
785 esis
786}
787
788fn contains_repair_esi(esis: &[u32], k_source: u32) -> bool {
789 esis.iter().any(|&esi| esi >= k_source)
790}
791
792fn derive_decode_proof_object_id(k_source: u32, symbol_size: u32, esis: &[u32]) -> ObjectId {
793 let mut material = Vec::with_capacity(40 + esis.len() * 4);
794 material.extend_from_slice(b"fsqlite:raptorq:decode-proof:v1");
795 material.extend_from_slice(&k_source.to_le_bytes());
796 material.extend_from_slice(&symbol_size.to_le_bytes());
797 for esi in esis {
798 material.extend_from_slice(&esi.to_le_bytes());
799 }
800 ObjectId::derive_from_canonical_bytes(&material)
801}
802
803fn deterministic_timing_ns(k_source: u32, symbol_size: u32, symbols_used: u32) -> u64 {
804 let mut material = [0_u8; 12];
805 material[..4].copy_from_slice(&k_source.to_le_bytes());
806 material[4..8].copy_from_slice(&symbol_size.to_le_bytes());
807 material[8..12].copy_from_slice(&symbols_used.to_le_bytes());
808 xxh3_64(&material)
809}
810
811#[cfg(test)]
816#[allow(
817 clippy::cast_possible_truncation,
818 clippy::cast_lossless,
819 clippy::cast_precision_loss,
820 clippy::cast_sign_loss
821)]
822mod tests {
823 use std::collections::{BTreeMap, VecDeque};
824 use std::pin::Pin;
825 use std::task::{Context, Poll};
826
827 use asupersync::error::ErrorKind as AsErrorKind;
828 use asupersync::raptorq::RaptorQReceiverBuilder;
829 use asupersync::raptorq::RaptorQSenderBuilder;
830 use asupersync::security::AuthenticationTag;
831 use asupersync::security::authenticated::AuthenticatedSymbol;
832 use asupersync::transport::error::{SinkError, StreamError};
833 use asupersync::transport::sink::SymbolSink;
834 use asupersync::transport::stream::SymbolStream;
835 use asupersync::types::{ObjectId as AsObjectId, ObjectParams, Symbol, SymbolId, SymbolKind};
836 use asupersync::{Cx as AsCx, RaptorQConfig};
837
838 use super::*;
839
840 struct VecPageSink {
845 symbols: BTreeMap<u32, Vec<u8>>,
846 flushed: bool,
847 }
848
849 impl VecPageSink {
850 fn new() -> Self {
851 Self {
852 symbols: BTreeMap::new(),
853 flushed: false,
854 }
855 }
856 }
857
858 impl PageSymbolSink for VecPageSink {
859 fn write_symbol(&mut self, esi: u32, data: &[u8]) -> Result<()> {
860 self.symbols.insert(esi, data.to_vec());
861 Ok(())
862 }
863
864 fn flush(&mut self) -> Result<()> {
865 self.flushed = true;
866 Ok(())
867 }
868
869 fn written_count(&self) -> u32 {
870 self.symbols.len() as u32
871 }
872 }
873
874 struct VecPageSource {
875 symbols: BTreeMap<u32, Vec<u8>>,
876 }
877
878 impl VecPageSource {
879 fn from_sink(sink: &VecPageSink) -> Self {
880 Self {
881 symbols: sink.symbols.clone(),
882 }
883 }
884
885 fn from_map(symbols: BTreeMap<u32, Vec<u8>>) -> Self {
886 Self { symbols }
887 }
888 }
889
890 impl PageSymbolSource for VecPageSource {
891 fn read_symbol(&mut self, esi: u32) -> Result<Option<Vec<u8>>> {
892 Ok(self.symbols.get(&esi).cloned())
893 }
894
895 fn available_esis(&self) -> Vec<u32> {
896 self.symbols.keys().copied().collect()
897 }
898
899 fn available_count(&self) -> u32 {
900 self.symbols.len() as u32
901 }
902 }
903
904 #[derive(Debug)]
909 struct VecTransportSink {
910 symbols: Vec<Symbol>,
911 }
912
913 impl VecTransportSink {
914 fn new() -> Self {
915 Self {
916 symbols: Vec::new(),
917 }
918 }
919 }
920
921 #[derive(Debug)]
922 struct VecTransportStream {
923 symbols: VecDeque<AuthenticatedSymbol>,
924 }
925
926 impl VecTransportStream {
927 fn new(symbols: Vec<Symbol>) -> Self {
928 let symbols = symbols
929 .into_iter()
930 .map(|symbol| AuthenticatedSymbol::from_parts(symbol, AuthenticationTag::zero()))
931 .collect();
932 Self { symbols }
933 }
934 }
935
936 impl SymbolStream for VecTransportStream {
937 fn poll_next(
938 mut self: Pin<&mut Self>,
939 _cx: &mut Context<'_>,
940 ) -> Poll<Option<std::result::Result<AuthenticatedSymbol, StreamError>>> {
941 match self.symbols.pop_front() {
942 Some(symbol) => Poll::Ready(Some(Ok(symbol))),
943 None => Poll::Ready(None),
944 }
945 }
946
947 fn size_hint(&self) -> (usize, Option<usize>) {
948 (self.symbols.len(), Some(self.symbols.len()))
949 }
950
951 fn is_exhausted(&self) -> bool {
952 self.symbols.is_empty()
953 }
954 }
955
956 const TEST_OBJECT_ID: u64 = 0xBD_1A15;
957 const TEST_MAX_BLOCK_SIZE: usize = 64 * 1024;
958 const PACKED_KIND_REPAIR_BIT: u32 = 1_u32 << 31;
959 const PACKED_SBN_SHIFT: u32 = 23;
960 const PACKED_SBN_MASK: u32 = 0xFF;
961 const PACKED_ESI_MASK: u32 = 0x7F_FFFF;
962
963 fn pack_symbol_key(kind: SymbolKind, sbn: u8, esi: u32) -> Result<u32> {
964 if esi > PACKED_ESI_MASK {
965 return Err(FrankenError::OutOfRange {
966 what: "packed symbol esi (must fit 23 bits)".to_owned(),
967 value: esi.to_string(),
968 });
969 }
970
971 let kind_bit = if kind.is_repair() {
972 PACKED_KIND_REPAIR_BIT
973 } else {
974 0
975 };
976 Ok(kind_bit | (u32::from(sbn) << PACKED_SBN_SHIFT) | esi)
977 }
978
979 fn unpack_symbol_key(packed: u32) -> (SymbolKind, u8, u32) {
980 let kind = if packed & PACKED_KIND_REPAIR_BIT == 0 {
981 SymbolKind::Source
982 } else {
983 SymbolKind::Repair
984 };
985 let sbn = ((packed >> PACKED_SBN_SHIFT) & PACKED_SBN_MASK) as u8;
986 let esi = packed & PACKED_ESI_MASK;
987 (kind, sbn, esi)
988 }
989
990 impl SymbolSink for VecTransportSink {
991 fn poll_send(
992 mut self: Pin<&mut Self>,
993 _cx: &mut Context<'_>,
994 symbol: AuthenticatedSymbol,
995 ) -> Poll<std::result::Result<(), SinkError>> {
996 self.symbols.push(symbol.into_symbol());
997 Poll::Ready(Ok(()))
998 }
999
1000 fn poll_flush(
1001 self: Pin<&mut Self>,
1002 _cx: &mut Context<'_>,
1003 ) -> Poll<std::result::Result<(), SinkError>> {
1004 Poll::Ready(Ok(()))
1005 }
1006
1007 fn poll_close(
1008 self: Pin<&mut Self>,
1009 _cx: &mut Context<'_>,
1010 ) -> Poll<std::result::Result<(), SinkError>> {
1011 Poll::Ready(Ok(()))
1012 }
1013
1014 fn poll_ready(
1015 self: Pin<&mut Self>,
1016 _cx: &mut Context<'_>,
1017 ) -> Poll<std::result::Result<(), SinkError>> {
1018 Poll::Ready(Ok(()))
1019 }
1020 }
1021
1022 struct AsupersyncCodec;
1024
1025 impl SymbolCodec for AsupersyncCodec {
1026 fn encode(
1027 &self,
1028 _cx: &Cx,
1029 source_data: &[u8],
1030 symbol_size: u32,
1031 repair_overhead: f64,
1032 ) -> Result<CodecEncodeResult> {
1033 let mut config = RaptorQConfig::default();
1034 config.encoding.symbol_size = symbol_size as u16;
1035 config.encoding.max_block_size = TEST_MAX_BLOCK_SIZE;
1036 config.encoding.repair_overhead = repair_overhead;
1037
1038 let cx = AsCx::for_testing();
1039 let object_id = AsObjectId::new_for_test(TEST_OBJECT_ID);
1040 let mut sender = RaptorQSenderBuilder::new()
1041 .config(config)
1042 .transport(VecTransportSink::new())
1043 .build()
1044 .map_err(|e| FrankenError::Internal(format!("sender build: {e}")))?;
1045
1046 let outcome = sender
1047 .send_object(&cx, object_id, source_data)
1048 .map_err(|e| FrankenError::Internal(format!("send_object: {e}")))?;
1049
1050 let symbols = std::mem::take(&mut sender.transport_mut().symbols);
1051 let k = outcome.source_symbols as u32;
1052
1053 let mut source_symbols = Vec::new();
1054 let mut repair_symbols = Vec::new();
1055 for s in &symbols {
1056 let packed_key = pack_symbol_key(s.kind(), s.sbn(), s.esi())?;
1057 if s.kind().is_source() {
1058 source_symbols.push((packed_key, s.data().to_vec()));
1059 } else {
1060 repair_symbols.push((packed_key, s.data().to_vec()));
1061 }
1062 }
1063
1064 Ok(CodecEncodeResult {
1065 source_symbols,
1066 repair_symbols,
1067 k_source: k,
1068 })
1069 }
1070
1071 fn decode(
1072 &self,
1073 _cx: &Cx,
1074 symbols: &[(u32, Vec<u8>)],
1075 k_source: u32,
1076 symbol_size: u32,
1077 ) -> Result<CodecDecodeResult> {
1078 if symbols.is_empty() {
1079 return Ok(CodecDecodeResult::Failure {
1080 reason: DecodeFailureReason::InsufficientSymbols,
1081 symbols_received: 0,
1082 k_required: k_source,
1083 });
1084 }
1085
1086 let object_id = AsObjectId::new_for_test(TEST_OBJECT_ID);
1087 let mut config = RaptorQConfig::default();
1088 config.encoding.symbol_size = symbol_size as u16;
1089 config.encoding.max_block_size = TEST_MAX_BLOCK_SIZE;
1090
1091 let max_block_symbols = (TEST_MAX_BLOCK_SIZE as u32 / symbol_size.max(1)).max(1);
1095 let source_blocks = k_source.div_ceil(max_block_symbols).max(1) as u16;
1096 let symbols_per_block = k_source.div_ceil(u32::from(source_blocks)).max(1);
1097 let object_size = u64::from(k_source)
1098 .checked_mul(u64::from(symbol_size))
1099 .ok_or_else(|| FrankenError::OutOfRange {
1100 what: "object_size for decode params".to_owned(),
1101 value: format!("{k_source}*{symbol_size}"),
1102 })?;
1103 let params = ObjectParams::new(
1104 object_id,
1105 object_size,
1106 u16::try_from(symbol_size).map_err(|_| FrankenError::OutOfRange {
1107 what: "symbol_size as u16".to_owned(),
1108 value: symbol_size.to_string(),
1109 })?,
1110 source_blocks,
1111 u16::try_from(symbols_per_block).map_err(|_| FrankenError::OutOfRange {
1112 what: "symbols_per_block as u16".to_owned(),
1113 value: symbols_per_block.to_string(),
1114 })?,
1115 );
1116
1117 let mut rebuilt = Vec::with_capacity(symbols.len());
1118 for (packed, data) in symbols {
1119 let (kind, sbn, esi) = unpack_symbol_key(*packed);
1120 rebuilt.push(Symbol::new(
1121 SymbolId::new(object_id, sbn, esi),
1122 data.clone(),
1123 kind,
1124 ));
1125 }
1126
1127 let cx = AsCx::for_testing();
1128 let mut receiver = RaptorQReceiverBuilder::new()
1129 .config(config)
1130 .source(VecTransportStream::new(rebuilt))
1131 .build()
1132 .map_err(|e| FrankenError::Internal(format!("receiver build: {e}")))?;
1133
1134 match receiver.receive_object(&cx, ¶ms) {
1135 Ok(outcome) => Ok(CodecDecodeResult::Success {
1136 data: outcome.data,
1137 symbols_used: outcome.symbols_received as u32,
1138 peeled_count: 0,
1139 inactivated_count: 0,
1140 }),
1141 Err(err) => {
1142 let reason = match err.kind() {
1143 AsErrorKind::InsufficientSymbols => {
1144 DecodeFailureReason::InsufficientSymbols
1145 }
1146 _ => DecodeFailureReason::SingularMatrix,
1147 };
1148 Ok(CodecDecodeResult::Failure {
1149 reason,
1150 symbols_received: symbols.len() as u32,
1151 k_required: k_source,
1152 })
1153 }
1154 }
1155 }
1156 }
1157
1158 fn deterministic_page_data(k: usize, symbol_size: usize, seed: u64) -> Vec<u8> {
1163 let mut state = seed ^ 0x9E37_79B9_7F4A_7C15;
1164 let total = k * symbol_size;
1165 let mut out = Vec::with_capacity(total);
1166 for idx in 0..total {
1167 state ^= state << 7;
1168 state ^= state >> 9;
1169 state = state.wrapping_mul(0xA24B_AED4_963E_E407);
1170 let idx_byte = (idx % 251) as u8;
1171 out.push((state & 0xFF) as u8 ^ idx_byte);
1172 }
1173 out
1174 }
1175
1176 fn test_cx() -> fsqlite_types::cx::Cx {
1177 fsqlite_types::cx::Cx::new()
1178 }
1179
1180 fn default_codec() -> AsupersyncCodec {
1181 AsupersyncCodec
1182 }
1183
1184 fn default_config() -> PipelineConfig {
1185 PipelineConfig::for_page_size(512)
1186 }
1187
1188 #[test]
1193 fn test_pipeline_encode_produces_source_and_repair() {
1194 let config = default_config();
1195 let encoder =
1196 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1197 let cx = test_cx();
1198 let k = 10_usize;
1199 let data = deterministic_page_data(k, config.symbol_size as usize, 0x1234);
1200
1201 let mut sink = VecPageSink::new();
1202 let outcome = encoder
1203 .encode_pages(&cx, &data, &mut sink)
1204 .expect("encode must succeed");
1205
1206 assert_eq!(
1207 outcome.source_count as usize, k,
1208 "bead_id={BEAD_ID} case=encode_source_count"
1209 );
1210 assert!(
1211 outcome.repair_count > 0,
1212 "bead_id={BEAD_ID} case=encode_repair_present"
1213 );
1214 assert_eq!(
1215 outcome.symbol_size, config.symbol_size,
1216 "bead_id={BEAD_ID} case=encode_symbol_size"
1217 );
1218 assert!(sink.flushed, "bead_id={BEAD_ID} case=encode_sink_flushed");
1219
1220 let sym_size = config.symbol_size as usize;
1222 for i in 0..k {
1223 let esi = i as u32;
1224 let expected = &data[i * sym_size..(i + 1) * sym_size];
1225 let actual = sink.symbols.get(&esi);
1226 assert!(actual.is_some(), "source symbol ESI {esi} missing");
1227 let actual = actual.expect("source symbol existence asserted");
1228 assert_eq!(
1229 actual, expected,
1230 "bead_id={BEAD_ID} case=encode_source_symbol_matches esi={esi}"
1231 );
1232 }
1233
1234 info!(
1235 bead_id = BEAD_ID,
1236 source_count = outcome.source_count,
1237 repair_count = outcome.repair_count,
1238 total_written = sink.written_count(),
1239 "test_pipeline_encode complete"
1240 );
1241 }
1242
1243 #[test]
1248 fn test_pipeline_decode_with_extra_symbols() {
1249 let config = default_config();
1250 let encoder =
1251 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1252 let decoder =
1253 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1254 let cx = test_cx();
1255 let k = 10_usize;
1256 let data = deterministic_page_data(k, config.symbol_size as usize, 0x5678);
1257
1258 let mut sink = VecPageSink::new();
1260 let outcome = encoder
1261 .encode_pages(&cx, &data, &mut sink)
1262 .expect("encode must succeed");
1263
1264 let mut source = VecPageSource::from_sink(&sink);
1266 let decode_outcome = decoder
1267 .decode_pages(&cx, &mut source, outcome.source_count)
1268 .expect("decode must succeed");
1269
1270 match decode_outcome {
1271 DecodeOutcome::Success(success) => {
1272 assert_eq!(
1273 success.data, data,
1274 "bead_id={BEAD_ID} case=decode_roundtrip_bytes"
1275 );
1276 assert!(
1277 success.symbols_used >= outcome.source_count,
1278 "bead_id={BEAD_ID} case=decode_symbols_used"
1279 );
1280 info!(
1281 bead_id = BEAD_ID,
1282 symbols_used = success.symbols_used,
1283 peeled = success.peeled_count,
1284 inactivated = success.inactivated_count,
1285 "test_pipeline_decode complete"
1286 );
1287 }
1288 DecodeOutcome::Failure(failure) => unreachable!(
1289 "bead_id={BEAD_ID} case=decode_unexpected_failure reason={:?}",
1290 failure.reason
1291 ),
1292 }
1293 }
1294
1295 #[test]
1300 fn test_pipeline_cancel_safe_encode() {
1301 let config = PipelineConfig {
1302 checkpoint_interval: 2, ..default_config()
1304 };
1305 let encoder =
1306 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1307
1308 let cx = fsqlite_types::cx::Cx::new();
1310 cx.cancel_with_reason(fsqlite_types::cx::CancelReason::UserInterrupt);
1311
1312 let k = 10_usize;
1313 let data = deterministic_page_data(k, config.symbol_size as usize, 0xABCD);
1314 let mut sink = VecPageSink::new();
1315
1316 let result = encoder.encode_pages(&cx, &data, &mut sink);
1317 assert!(
1318 result.is_err(),
1319 "bead_id={BEAD_ID} case=cancel_safe_encode_aborts"
1320 );
1321 assert!(
1322 matches!(result.unwrap_err(), FrankenError::Abort),
1323 "bead_id={BEAD_ID} case=cancel_safe_encode_error_type"
1324 );
1325 assert!(!sink.flushed, "bead_id={BEAD_ID} case=cancel_safe_no_flush");
1327 }
1328
1329 #[test]
1330 fn test_pipeline_cancel_safe_decode() {
1331 let config = PipelineConfig {
1332 checkpoint_interval: 2,
1333 ..default_config()
1334 };
1335 let decoder =
1336 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1337
1338 let cx = fsqlite_types::cx::Cx::new();
1340 cx.cancel_with_reason(fsqlite_types::cx::CancelReason::UserInterrupt);
1341
1342 let mut symbols = BTreeMap::new();
1344 for esi in 0..10_u32 {
1345 symbols.insert(esi, vec![0xAA; config.symbol_size as usize]);
1346 }
1347 let mut source = VecPageSource::from_map(symbols);
1348
1349 let result = decoder.decode_pages(&cx, &mut source, 10);
1350 assert!(
1351 result.is_err(),
1352 "bead_id={BEAD_ID} case=cancel_safe_decode_aborts"
1353 );
1354 assert!(
1355 matches!(result.unwrap_err(), FrankenError::Abort),
1356 "bead_id={BEAD_ID} case=cancel_safe_decode_error_type"
1357 );
1358 }
1359
1360 struct BackpressureSink {
1366 limit: u32,
1367 count: u32,
1368 }
1369
1370 impl BackpressureSink {
1371 fn new(limit: u32) -> Self {
1372 Self { limit, count: 0 }
1373 }
1374 }
1375
1376 impl PageSymbolSink for BackpressureSink {
1377 fn write_symbol(&mut self, _esi: u32, _data: &[u8]) -> Result<()> {
1378 if self.count >= self.limit {
1379 return Err(FrankenError::Busy);
1380 }
1381 self.count += 1;
1382 Ok(())
1383 }
1384
1385 fn flush(&mut self) -> Result<()> {
1386 Ok(())
1387 }
1388
1389 fn written_count(&self) -> u32 {
1390 self.count
1391 }
1392 }
1393
1394 #[test]
1395 fn test_pipeline_backpressure_sink_full() {
1396 let config = default_config();
1397 let encoder =
1398 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1399 let cx = test_cx();
1400 let k = 10_usize;
1401 let data = deterministic_page_data(k, config.symbol_size as usize, 0xEEFF);
1402
1403 let mut sink = BackpressureSink::new(3);
1405 let result = encoder.encode_pages(&cx, &data, &mut sink);
1406
1407 assert!(
1408 result.is_err(),
1409 "bead_id={BEAD_ID} case=backpressure_propagated"
1410 );
1411 assert!(
1412 matches!(result.unwrap_err(), FrankenError::Busy),
1413 "bead_id={BEAD_ID} case=backpressure_error_type"
1414 );
1415 assert_eq!(
1416 sink.written_count(),
1417 3,
1418 "bead_id={BEAD_ID} case=backpressure_partial_write"
1419 );
1420 }
1421
1422 #[test]
1427 fn test_config_validation_zero_symbol_size() {
1428 let config = PipelineConfig {
1429 symbol_size: 0,
1430 ..default_config()
1431 };
1432 assert!(
1433 config.validate().is_err(),
1434 "bead_id={BEAD_ID} case=config_reject_zero_symbol_size"
1435 );
1436 }
1437
1438 #[test]
1439 fn test_config_validation_non_power_of_two() {
1440 let config = PipelineConfig {
1441 symbol_size: 1000,
1442 ..default_config()
1443 };
1444 assert!(
1445 config.validate().is_err(),
1446 "bead_id={BEAD_ID} case=config_reject_non_power_of_two"
1447 );
1448 }
1449
1450 #[test]
1451 fn test_config_validation_below_min() {
1452 let config = PipelineConfig {
1453 symbol_size: 256,
1454 ..default_config()
1455 };
1456 assert!(
1457 config.validate().is_err(),
1458 "bead_id={BEAD_ID} case=config_reject_below_min"
1459 );
1460 }
1461
1462 #[test]
1463 fn test_config_validation_above_max() {
1464 let config = PipelineConfig {
1465 symbol_size: 128 * 1024,
1466 ..default_config()
1467 };
1468 assert!(
1469 config.validate().is_err(),
1470 "bead_id={BEAD_ID} case=config_reject_above_max"
1471 );
1472 }
1473
1474 #[test]
1475 fn test_config_validation_zero_max_block_size() {
1476 let config = PipelineConfig {
1477 max_block_size: 0,
1478 ..default_config()
1479 };
1480 assert!(
1481 config.validate().is_err(),
1482 "bead_id={BEAD_ID} case=config_reject_zero_max_block"
1483 );
1484 }
1485
1486 #[test]
1487 fn test_config_validation_repair_overhead_below_one() {
1488 let config = PipelineConfig {
1489 repair_overhead: 0.5,
1490 ..default_config()
1491 };
1492 assert!(
1493 config.validate().is_err(),
1494 "bead_id={BEAD_ID} case=config_reject_repair_overhead_below_one"
1495 );
1496 }
1497
1498 #[test]
1499 fn test_config_validation_zero_checkpoint_interval() {
1500 let config = PipelineConfig {
1501 checkpoint_interval: 0,
1502 ..default_config()
1503 };
1504 assert!(
1505 config.validate().is_err(),
1506 "bead_id={BEAD_ID} case=config_reject_zero_checkpoint_interval"
1507 );
1508 }
1509
1510 #[test]
1511 fn test_config_validation_valid_configs() {
1512 for symbol_size in [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536] {
1513 let config = PipelineConfig::for_page_size(symbol_size);
1514 assert!(
1515 config.validate().is_ok(),
1516 "bead_id={BEAD_ID} case=config_valid symbol_size={symbol_size}"
1517 );
1518 }
1519 }
1520
1521 #[test]
1526 fn test_decode_failure_insufficient_symbols() {
1527 let config = default_config();
1528 let encoder =
1529 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1530 let decoder =
1531 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1532 let cx = test_cx();
1533 let k = 10_usize;
1534 let data = deterministic_page_data(k, config.symbol_size as usize, 0xDEAD);
1535
1536 let mut sink = VecPageSink::new();
1538 let outcome = encoder
1539 .encode_pages(&cx, &data, &mut sink)
1540 .expect("encode must succeed");
1541
1542 let mut partial = BTreeMap::new();
1544 for esi in 0..((k - 3) as u32) {
1545 if let Some(sym) = sink.symbols.get(&esi) {
1546 partial.insert(esi, sym.clone());
1547 }
1548 }
1549 let mut source = VecPageSource::from_map(partial);
1550
1551 let decode_outcome = decoder
1552 .decode_pages(&cx, &mut source, outcome.source_count)
1553 .expect("decode call itself should not error");
1554
1555 match decode_outcome {
1556 DecodeOutcome::Failure(failure) => {
1557 assert_eq!(
1558 failure.reason,
1559 DecodeFailureReason::InsufficientSymbols,
1560 "bead_id={BEAD_ID} case=decode_failure_reason"
1561 );
1562 assert!(
1563 failure.symbols_received < outcome.source_count,
1564 "bead_id={BEAD_ID} case=decode_failure_symbol_count"
1565 );
1566 assert_eq!(
1567 failure.k_required, outcome.source_count,
1568 "bead_id={BEAD_ID} case=decode_failure_k_required"
1569 );
1570 assert!(
1571 failure.decode_proof.is_none(),
1572 "bead_id={BEAD_ID} case=decode_failure_proof_disabled_by_default"
1573 );
1574 }
1575 DecodeOutcome::Success(_) => {
1576 unreachable!("bead_id={BEAD_ID} case=decode_should_have_failed")
1577 }
1578 }
1579 }
1580
1581 #[test]
1582 fn test_decode_failure_emits_proof_when_enabled() {
1583 let mut config = default_config();
1584 config.decode_proof_policy = DecodeProofEmissionPolicy {
1585 emit_on_decode_failure: true,
1586 emit_on_repair_success: false,
1587 };
1588 let encoder =
1589 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1590 let decoder =
1591 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1592 let cx = test_cx();
1593 let k = 10_usize;
1594 let data = deterministic_page_data(k, config.symbol_size as usize, 0xFA24);
1595
1596 let mut sink = VecPageSink::new();
1597 let outcome = encoder
1598 .encode_pages(&cx, &data, &mut sink)
1599 .expect("encode must succeed");
1600
1601 let mut partial = BTreeMap::new();
1602 for esi in 0..((k - 2) as u32) {
1603 if let Some(sym) = sink.symbols.get(&esi) {
1604 partial.insert(esi, sym.clone());
1605 }
1606 }
1607 let mut source = VecPageSource::from_map(partial);
1608 let decode_outcome = decoder
1609 .decode_pages(&cx, &mut source, outcome.source_count)
1610 .expect("decode call itself should not error");
1611
1612 match decode_outcome {
1613 DecodeOutcome::Failure(failure) => {
1614 let proof = failure
1615 .decode_proof
1616 .expect("bead_id=bd-faz4 case=decode_failure_proof_emitted");
1617 assert!(
1618 !proof.decode_success,
1619 "bead_id=bd-faz4 case=decode_failure_proof_flag"
1620 );
1621 assert!(
1622 proof.is_consistent(),
1623 "bead_id=bd-faz4 case=decode_failure_proof_consistent"
1624 );
1625 }
1626 DecodeOutcome::Success(_) => {
1627 unreachable!("bead_id=bd-faz4 case=decode_failure_expected")
1628 }
1629 }
1630 }
1631
1632 #[test]
1633 fn test_decode_success_with_repair_emits_proof_when_enabled() {
1634 let mut config = default_config();
1635 config.decode_proof_policy = DecodeProofEmissionPolicy {
1636 emit_on_decode_failure: false,
1637 emit_on_repair_success: true,
1638 };
1639 let encoder =
1640 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1641 let decoder =
1642 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1643 let cx = test_cx();
1644 let k = 10_usize;
1645 let data = deterministic_page_data(k, config.symbol_size as usize, 0xF0AA);
1646
1647 let mut sink = VecPageSink::new();
1648 let outcome = encoder
1649 .encode_pages(&cx, &data, &mut sink)
1650 .expect("encode must succeed");
1651 let mut source = VecPageSource::from_sink(&sink);
1652 let decode_outcome = decoder
1653 .decode_pages(&cx, &mut source, outcome.source_count)
1654 .expect("decode must succeed");
1655
1656 match decode_outcome {
1657 DecodeOutcome::Success(success) => {
1658 let proof = success
1659 .decode_proof
1660 .expect("bead_id=bd-faz4 case=repair_success_proof_emitted");
1661 assert!(proof.decode_success);
1662 assert!(proof.is_repair());
1663 assert!(
1664 proof.is_consistent(),
1665 "bead_id=bd-faz4 case=repair_success_proof_consistent"
1666 );
1667 }
1668 DecodeOutcome::Failure(failure) => unreachable!(
1669 "bead_id=bd-faz4 case=repair_success_should_decode reason={:?}",
1670 failure.reason
1671 ),
1672 }
1673 }
1674
1675 #[test]
1680 fn test_e2e_roundtrip_multiple_page_sizes() {
1681 for &symbol_size in &[512_u32, 1024, 4096] {
1682 let config = PipelineConfig::for_page_size(symbol_size);
1683 let encoder =
1684 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1685 let decoder =
1686 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1687 let cx = test_cx();
1688
1689 let k = 8_usize;
1690 let data = deterministic_page_data(k, symbol_size as usize, u64::from(symbol_size));
1691
1692 let mut sink = VecPageSink::new();
1694 let outcome = encoder
1695 .encode_pages(&cx, &data, &mut sink)
1696 .expect("encode must succeed");
1697
1698 let mut source = VecPageSource::from_sink(&sink);
1700 let decode_result = decoder
1701 .decode_pages(&cx, &mut source, outcome.source_count)
1702 .expect("decode must succeed");
1703
1704 match decode_result {
1705 DecodeOutcome::Success(success) => {
1706 assert_eq!(
1707 success.data, data,
1708 "bead_id={BEAD_ID} case=e2e_roundtrip symbol_size={symbol_size}"
1709 );
1710 }
1711 DecodeOutcome::Failure(f) => unreachable!(
1712 "bead_id={BEAD_ID} case=e2e_roundtrip_failure symbol_size={symbol_size} reason={:?}",
1713 f.reason
1714 ),
1715 }
1716 }
1717 }
1718
1719 #[test]
1720 fn test_e2e_roundtrip_64_pages() {
1721 let _serial = crate::connection::fsqlite_core_test_serializer();
1722 let config = PipelineConfig::for_page_size(4096);
1723 let encoder =
1724 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1725 let decoder =
1726 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1727 let cx = test_cx();
1728
1729 let k = 64_usize;
1730 let data = deterministic_page_data(k, config.symbol_size as usize, 0xE2E6_4000);
1731
1732 let mut sink = VecPageSink::new();
1733 let outcome = encoder
1734 .encode_pages(&cx, &data, &mut sink)
1735 .expect("encode must succeed");
1736
1737 assert_eq!(
1738 outcome.source_count as usize, k,
1739 "bead_id={BEAD_ID} case=e2e_64_source_count"
1740 );
1741
1742 let mut source = VecPageSource::from_sink(&sink);
1743 let decode_result = decoder
1744 .decode_pages(&cx, &mut source, outcome.source_count)
1745 .expect("decode must succeed");
1746
1747 match decode_result {
1748 DecodeOutcome::Success(success) => {
1749 assert_eq!(
1750 success.data, data,
1751 "bead_id={BEAD_ID} case=e2e_64_roundtrip_bytes"
1752 );
1753 info!(
1754 bead_id = BEAD_ID,
1755 k,
1756 peeled = success.peeled_count,
1757 inactivated = success.inactivated_count,
1758 "E2E 64-page roundtrip complete"
1759 );
1760 }
1761 DecodeOutcome::Failure(f) => unreachable!(
1762 "bead_id={BEAD_ID} case=e2e_64_failure reason={:?}",
1763 f.reason
1764 ),
1765 }
1766 }
1767
1768 #[test]
1769 fn test_e2e_bd_1hi_5() {
1770 let _serial = crate::connection::fsqlite_core_test_serializer();
1771 let config = PipelineConfig::for_page_size(4096);
1772 let encoder =
1773 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1774 let decoder =
1775 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1776 let cx = test_cx();
1777
1778 let k = 64_usize;
1780 let data = deterministic_page_data(k, config.symbol_size as usize, 0xB1D1_5005);
1781 let mut sink = VecPageSink::new();
1782 let outcome = encoder
1783 .encode_pages(&cx, &data, &mut sink)
1784 .expect("encode must succeed");
1785
1786 let mut dropped = 0_u32;
1788 let mut degraded = BTreeMap::new();
1789 for (packed_key, symbol_bytes) in &sink.symbols {
1790 let (kind, _sbn, esi) = unpack_symbol_key(*packed_key);
1791 if kind.is_source() && esi == 0 {
1792 dropped += 1;
1793 continue;
1794 }
1795 degraded.insert(*packed_key, symbol_bytes.clone());
1796 }
1797 assert!(dropped > 0, "bead_id={BEAD_ID} case=e2e_named_dropped_some");
1798
1799 let mut source = VecPageSource::from_map(degraded);
1800 let decode_result = decoder
1801 .decode_pages(&cx, &mut source, outcome.source_count)
1802 .expect("decode must complete");
1803
1804 match decode_result {
1805 DecodeOutcome::Success(success) => {
1806 assert_eq!(
1807 success.data, data,
1808 "bead_id={BEAD_ID} case=e2e_named_byte_perfect_recovery"
1809 );
1810 }
1811 DecodeOutcome::Failure(f) => unreachable!(
1812 "bead_id={BEAD_ID} case=e2e_named_unexpected_failure reason={:?}",
1813 f.reason
1814 ),
1815 }
1816 }
1817
1818 #[test]
1823 fn test_e2e_retry_after_failure() {
1824 let config = default_config();
1825 let encoder =
1826 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1827 let decoder =
1828 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1829 let cx = test_cx();
1830 let k = 10_usize;
1831 let data = deterministic_page_data(k, config.symbol_size as usize, 0xAE_7121);
1832
1833 let mut sink = VecPageSink::new();
1835 let outcome = encoder
1836 .encode_pages(&cx, &data, &mut sink)
1837 .expect("encode must succeed");
1838
1839 let mut partial = BTreeMap::new();
1841 for esi in 0..((k - 2) as u32) {
1842 if let Some(sym) = sink.symbols.get(&esi) {
1843 partial.insert(esi, sym.clone());
1844 }
1845 }
1846 let mut source_attempt1 = VecPageSource::from_map(partial.clone());
1847 let result1 = decoder
1848 .decode_pages(&cx, &mut source_attempt1, outcome.source_count)
1849 .expect("decode call should not error");
1850 assert!(
1851 matches!(result1, DecodeOutcome::Failure(_)),
1852 "bead_id={BEAD_ID} case=retry_first_attempt_fails"
1853 );
1854
1855 let full = sink.symbols.clone();
1857 let mut source_attempt2 = VecPageSource::from_map(full);
1858 let result2 = decoder
1859 .decode_pages(&cx, &mut source_attempt2, outcome.source_count)
1860 .expect("decode call should not error");
1861 match result2 {
1862 DecodeOutcome::Success(success) => {
1863 assert_eq!(
1864 success.data, data,
1865 "bead_id={BEAD_ID} case=retry_second_attempt_succeeds"
1866 );
1867 }
1868 DecodeOutcome::Failure(f) => unreachable!(
1869 "bead_id={BEAD_ID} case=retry_second_should_succeed reason={:?}",
1870 f.reason
1871 ),
1872 }
1873 }
1874
1875 #[test]
1880 fn test_decode_source_only_exact_k() {
1881 let config = default_config();
1882 let encoder =
1883 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1884 let decoder =
1885 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1886 let cx = test_cx();
1887 let k = 8_usize;
1888 let data = deterministic_page_data(k, config.symbol_size as usize, 0xE4AC7);
1889
1890 let mut sink = VecPageSink::new();
1891 let outcome = encoder
1892 .encode_pages(&cx, &data, &mut sink)
1893 .expect("encode must succeed");
1894
1895 let mut source_only = BTreeMap::new();
1897 for esi in 0..(k as u32) {
1898 if let Some(sym) = sink.symbols.get(&esi) {
1899 source_only.insert(esi, sym.clone());
1900 }
1901 }
1902
1903 let mut source = VecPageSource::from_map(source_only);
1904 let decode_result = decoder
1905 .decode_pages(&cx, &mut source, outcome.source_count)
1906 .expect("decode must not error");
1907
1908 match decode_result {
1909 DecodeOutcome::Success(success) => {
1910 assert_eq!(
1911 success.data, data,
1912 "bead_id={BEAD_ID} case=exact_k_roundtrip"
1913 );
1914 assert_eq!(
1915 success.symbols_used, k as u32,
1916 "bead_id={BEAD_ID} case=exact_k_symbols_used"
1917 );
1918 }
1919 DecodeOutcome::Failure(f) => unreachable!(
1920 "bead_id={BEAD_ID} case=exact_k_should_succeed reason={:?}",
1921 f.reason
1922 ),
1923 }
1924 }
1925
1926 #[test]
1935 fn metrics_struct_encode_counters() {
1936 let m = RaptorQMetrics::new();
1937 m.record_encode(2048, 3);
1938 m.record_encode(4096, 5);
1939
1940 let snap = m.snapshot();
1941 assert_eq!(snap.encode_ops, 2);
1942 assert_eq!(snap.encoded_bytes_total, 6144);
1943 assert_eq!(snap.repair_symbols_generated_total, 8);
1944 assert_eq!(snap.decode_ops, 0);
1945 }
1946
1947 #[test]
1948 fn metrics_struct_decode_counters() {
1949 let m = RaptorQMetrics::new();
1950 m.record_decode_success(4096);
1951 m.record_decode_success(2048);
1952 m.record_decode_failure();
1953
1954 let snap = m.snapshot();
1955 assert_eq!(snap.decode_ops, 3);
1956 assert_eq!(snap.decoded_bytes_total, 6144);
1957 assert_eq!(snap.decode_failures, 1);
1958 assert_eq!(snap.encode_ops, 0);
1959 }
1960
1961 #[test]
1962 fn metrics_snapshot_display() {
1963 let m = RaptorQMetrics::new();
1964 m.record_encode(4096, 2);
1965 m.record_decode_success(4096);
1966 let snap = m.snapshot();
1967 let display = format!("{snap}");
1968 assert!(display.contains("4096"), "encoded bytes in display");
1969 assert!(display.contains("2 repair"), "repair syms in display");
1970 }
1971
1972 #[test]
1973 fn metrics_reset() {
1974 let m = RaptorQMetrics::new();
1975 m.record_encode(1000, 5);
1976 m.record_decode_success(500);
1977 m.record_decode_failure();
1978 m.reset();
1979 let snap = m.snapshot();
1980 assert_eq!(snap.encoded_bytes_total, 0);
1981 assert_eq!(snap.repair_symbols_generated_total, 0);
1982 assert_eq!(snap.encode_ops, 0);
1983 assert_eq!(snap.decode_ops, 0);
1984 assert_eq!(snap.decode_failures, 0);
1985 assert_eq!(snap.decoded_bytes_total, 0);
1986 }
1987
1988 #[test]
1989 fn metrics_global_wired_to_encode_decode() {
1990 let before = GLOBAL_RAPTORQ_METRICS.snapshot();
1994
1995 let config = default_config();
1996 let encoder =
1997 RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1998 let decoder =
1999 RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
2000 let cx = test_cx();
2001 let k = 4_usize;
2002 let data = deterministic_page_data(k, config.symbol_size as usize, 0xF00D);
2003
2004 let mut sink = VecPageSink::new();
2005 let outcome = encoder.encode_pages(&cx, &data, &mut sink).expect("encode");
2006 let mut source = VecPageSource::from_sink(&sink);
2007 let _decode = decoder
2008 .decode_pages(&cx, &mut source, outcome.source_count)
2009 .expect("decode");
2010
2011 let after = GLOBAL_RAPTORQ_METRICS.snapshot();
2012 assert!(
2013 after.encode_ops > before.encode_ops,
2014 "global encode_ops should have increased"
2015 );
2016 assert!(
2017 after.encoded_bytes_total > before.encoded_bytes_total,
2018 "global encoded_bytes should have increased"
2019 );
2020 assert!(
2021 after.decode_ops > before.decode_ops,
2022 "global decode_ops should have increased"
2023 );
2024 assert!(
2025 after.decoded_bytes_total > before.decoded_bytes_total,
2026 "global decoded_bytes should have increased"
2027 );
2028 }
2029}