Skip to main content

ipfrs_tensorlogic/
codec_registry.rs

1//! Codec Registry — compression/encoding codec selection and peer negotiation
2//!
3//! Tensor data flows through multiple compression/encoding stages. This module
4//! manages codec selection and negotiation between peers so that both sides
5//! agree on a common codec before data is transmitted.
6//!
7//! # Overview
8//!
9//! [`CodecRegistry`] is the central store of [`CodecDescriptor`] entries, each
10//! keyed by a [`CodecId`].  Seven built-in codecs are pre-registered on
11//! construction:
12//!
13//! | Id | Name       | Speed class  | Ratio est. |
14//! |----|------------|--------------|------------|
15//! | 0  | none       | VeryFast     | 1.00       |
16//! | 1  | zstd       | Balanced     | 0.30       |
17//! | 2  | lz4        | VeryFast     | 0.55       |
18//! | 3  | snappy     | VeryFast     | 0.60       |
19//! | 4  | brotli     | Slow         | 0.25       |
20//! | 10 | arrow_ipc  | Fast         | 0.70       |
21//! | 11 | garw       | Fast         | 0.40       |
22//!
23//! Codec negotiation follows a "first-match" policy: the first codec in the
24//! *local* preference list that is also present in the *remote* list wins.
25//!
26//! # Examples
27//!
28//! ```
29//! use ipfrs_tensorlogic::codec_registry::{CodecId, CodecRegistry};
30//!
31//! let registry = CodecRegistry::new();
32//!
33//! // Look up ZSTD
34//! let desc = registry.get(CodecId::ZSTD).expect("example: should succeed in docs");
35//! assert_eq!(desc.name, "zstd");
36//!
37//! // Negotiate between two peers
38//! let local  = vec![CodecId::ZSTD, CodecId::LZ4, CodecId::NONE];
39//! let remote = vec![CodecId::LZ4,  CodecId::NONE];
40//! let agreed = CodecRegistry::negotiate(&local, &remote);
41//! assert_eq!(agreed, Some(CodecId::LZ4));
42//! ```
43
44use std::collections::HashMap;
45use std::fmt;
46use std::time::Instant;
47use thiserror::Error;
48
49// ─── CodecId ─────────────────────────────────────────────────────────────────
50
51/// Opaque identifier for a compression/encoding codec.
52///
53/// Use the associated constants (`NONE`, `ZSTD`, …) for well-known codecs.
54/// Custom codecs should use ids ≥ 1 000 to avoid conflicts with future
55/// built-in ids.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
57pub struct CodecId(pub u32);
58
59impl CodecId {
60    /// No codec — data is passed through unmodified.
61    pub const NONE: Self = Self(0);
62    /// Zstandard (zstd) compression.
63    pub const ZSTD: Self = Self(1);
64    /// LZ4 block compression.
65    pub const LZ4: Self = Self(2);
66    /// Snappy compression.
67    pub const SNAPPY: Self = Self(3);
68    /// Brotli compression.
69    pub const BROTLI: Self = Self(4);
70    /// Apache Arrow IPC framing/encoding.
71    pub const ARROW_IPC: Self = Self(10);
72    /// GARW (Generic Arrow Row-Wire) encoding.
73    pub const GARW: Self = Self(11);
74
75    /// Return the raw numeric value of this id.
76    #[inline]
77    pub fn value(self) -> u32 {
78        self.0
79    }
80}
81
82impl fmt::Display for CodecId {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "CodecId({})", self.0)
85    }
86}
87
88impl From<u32> for CodecId {
89    fn from(v: u32) -> Self {
90        Self(v)
91    }
92}
93
94impl From<CodecId> for u32 {
95    fn from(c: CodecId) -> Self {
96        c.0
97    }
98}
99
100// ─── SpeedClass ──────────────────────────────────────────────────────────────
101
102/// Qualitative speed tier for a codec.
103///
104/// The ordering is `VeryFast < Fast < Balanced < Slow < VerySlow`, i.e.
105/// a *smaller* variant is *faster*.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
107pub enum SpeedClass {
108    /// Negligible encoding/decoding overhead (e.g. pass-through, LZ4).
109    VeryFast,
110    /// Low overhead; suitable for latency-sensitive paths.
111    Fast,
112    /// Moderate overhead; good balance of speed and compression.
113    Balanced,
114    /// High overhead; use when CPU is not the bottleneck.
115    Slow,
116    /// Very high overhead; only for batch/offline scenarios.
117    VerySlow,
118}
119
120// ─── CodecDescriptor ─────────────────────────────────────────────────────────
121
122/// Metadata about a single codec.
123#[derive(Debug, Clone)]
124pub struct CodecDescriptor {
125    /// Stable numeric identifier.
126    pub id: CodecId,
127    /// Human-readable name (e.g. `"zstd"`).
128    pub name: String,
129    /// Estimated output-to-input byte ratio after compression.
130    ///
131    /// A value of `0.3` means the compressed output is 30 % of the original
132    /// size (70 % reduction).  `1.0` means no compression.
133    pub compression_ratio_estimate: f32,
134    /// Qualitative speed tier.
135    pub speed_class: SpeedClass,
136    /// Whether the codec is bit-for-bit lossless.
137    pub is_lossless: bool,
138}
139
140// ─── CodecError ──────────────────────────────────────────────────────────────
141
142/// Errors that can occur while working with [`CodecRegistry`].
143#[derive(Debug, Error)]
144pub enum CodecError {
145    /// A codec with the given id is already registered.
146    #[error("codec id {0} is already registered")]
147    AlreadyRegistered(u32),
148    /// No codec with the given id exists in the registry.
149    #[error("unknown codec id {0}")]
150    UnknownCodec(u32),
151}
152
153// ─── CodecNegotiationRecord ───────────────────────────────────────────────────
154
155/// Record of a single codec negotiation between the local peer and a remote
156/// peer.
157#[derive(Debug)]
158pub struct CodecNegotiationRecord {
159    /// Codec ids offered by the local side, in preference order.
160    pub local_offered: Vec<CodecId>,
161    /// Codec ids offered by the remote side, in any order.
162    pub remote_offered: Vec<CodecId>,
163    /// The codec that was agreed upon, or `None` if no common codec was found.
164    pub agreed: Option<CodecId>,
165    /// Wall-clock time at which negotiation completed.
166    pub negotiated_at: Instant,
167    /// Duration of the negotiation in milliseconds.
168    pub negotiation_ms: u64,
169}
170
171// ─── CodecRegistry ───────────────────────────────────────────────────────────
172
173/// Central registry of compression/encoding codecs.
174///
175/// On construction the seven built-in codecs are pre-registered.  Custom codecs
176/// can be added via [`CodecRegistry::register`]; duplicate ids are rejected.
177pub struct CodecRegistry {
178    /// Internal map from `CodecId` value to descriptor.
179    codecs: HashMap<u32, CodecDescriptor>,
180}
181
182impl CodecRegistry {
183    /// Construct a new registry with all seven built-in codecs pre-registered.
184    pub fn new() -> Self {
185        let mut registry = Self {
186            codecs: HashMap::new(),
187        };
188
189        let built_ins: &[CodecDescriptor] = &[
190            CodecDescriptor {
191                id: CodecId::NONE,
192                name: "none".to_string(),
193                compression_ratio_estimate: 1.0,
194                speed_class: SpeedClass::VeryFast,
195                is_lossless: true,
196            },
197            CodecDescriptor {
198                id: CodecId::ZSTD,
199                name: "zstd".to_string(),
200                compression_ratio_estimate: 0.30,
201                speed_class: SpeedClass::Balanced,
202                is_lossless: true,
203            },
204            CodecDescriptor {
205                id: CodecId::LZ4,
206                name: "lz4".to_string(),
207                compression_ratio_estimate: 0.55,
208                speed_class: SpeedClass::VeryFast,
209                is_lossless: true,
210            },
211            CodecDescriptor {
212                id: CodecId::SNAPPY,
213                name: "snappy".to_string(),
214                compression_ratio_estimate: 0.60,
215                speed_class: SpeedClass::VeryFast,
216                is_lossless: true,
217            },
218            CodecDescriptor {
219                id: CodecId::BROTLI,
220                name: "brotli".to_string(),
221                compression_ratio_estimate: 0.25,
222                speed_class: SpeedClass::Slow,
223                is_lossless: true,
224            },
225            CodecDescriptor {
226                id: CodecId::ARROW_IPC,
227                name: "arrow_ipc".to_string(),
228                compression_ratio_estimate: 0.70,
229                speed_class: SpeedClass::Fast,
230                is_lossless: true,
231            },
232            CodecDescriptor {
233                id: CodecId::GARW,
234                name: "garw".to_string(),
235                compression_ratio_estimate: 0.40,
236                speed_class: SpeedClass::Fast,
237                is_lossless: true,
238            },
239        ];
240
241        for desc in built_ins {
242            // These are known-unique; insertion cannot fail at construction time.
243            registry.codecs.insert(desc.id.value(), desc.clone());
244        }
245
246        registry
247    }
248
249    /// Register a custom [`CodecDescriptor`].
250    ///
251    /// Returns [`CodecError::AlreadyRegistered`] if a codec with the same id
252    /// already exists in the registry.
253    pub fn register(&mut self, descriptor: CodecDescriptor) -> Result<(), CodecError> {
254        let key = descriptor.id.value();
255        if self.codecs.contains_key(&key) {
256            return Err(CodecError::AlreadyRegistered(key));
257        }
258        self.codecs.insert(key, descriptor);
259        Ok(())
260    }
261
262    /// Look up a codec by its id.
263    ///
264    /// Returns `None` when the id is not registered.
265    pub fn get(&self, id: CodecId) -> Option<&CodecDescriptor> {
266        self.codecs.get(&id.value())
267    }
268
269    /// Return all registered codecs, sorted in ascending [`CodecId`] order.
270    pub fn list_all(&self) -> Vec<&CodecDescriptor> {
271        let mut entries: Vec<&CodecDescriptor> = self.codecs.values().collect();
272        entries.sort_by_key(|d| d.id);
273        entries
274    }
275
276    /// Negotiate a codec between two peers.
277    ///
278    /// Returns the first id in `local` that also appears in `remote`, or `None`
279    /// if there is no common codec.  This is a static method because negotiation
280    /// does not require access to a registry instance — it operates purely on
281    /// the id sets.
282    pub fn negotiate(local: &[CodecId], remote: &[CodecId]) -> Option<CodecId> {
283        // Build a hash-set from the remote list for O(1) lookup.
284        let remote_set: std::collections::HashSet<CodecId> = remote.iter().copied().collect();
285        local.iter().find(|id| remote_set.contains(id)).copied()
286    }
287
288    /// Return a reference to the fastest registered codec.
289    ///
290    /// When multiple codecs share the best speed class the one with the
291    /// numerically smallest [`CodecId`] is returned for determinism.
292    ///
293    /// Returns `None` only if the registry is empty (not possible after
294    /// `new()` unless all entries were somehow replaced).
295    pub fn best_for_speed(&self) -> Option<&CodecDescriptor> {
296        self.codecs.values().min_by(|a, b| {
297            a.speed_class
298                .cmp(&b.speed_class)
299                .then_with(|| a.id.cmp(&b.id))
300        })
301    }
302
303    /// Return a reference to the codec with the lowest
304    /// `compression_ratio_estimate` (best compression).
305    ///
306    /// When multiple codecs share the same ratio the one with the numerically
307    /// smallest [`CodecId`] is returned for determinism.  NaN ratio values are
308    /// sorted last.
309    ///
310    /// Returns `None` only if the registry is empty.
311    pub fn best_for_compression(&self) -> Option<&CodecDescriptor> {
312        self.codecs.values().min_by(|a, b| {
313            a.compression_ratio_estimate
314                .partial_cmp(&b.compression_ratio_estimate)
315                .unwrap_or(std::cmp::Ordering::Equal)
316                .then_with(|| a.id.cmp(&b.id))
317        })
318    }
319}
320
321impl Default for CodecRegistry {
322    fn default() -> Self {
323        Self::new()
324    }
325}
326
327// ─── Tests ───────────────────────────────────────────────────────────────────
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    // ── 1. All pre-registered codecs are present ──────────────────────────────
334
335    #[test]
336    fn test_builtin_codecs_all_present() {
337        let r = CodecRegistry::new();
338        let ids = [
339            CodecId::NONE,
340            CodecId::ZSTD,
341            CodecId::LZ4,
342            CodecId::SNAPPY,
343            CodecId::BROTLI,
344            CodecId::ARROW_IPC,
345            CodecId::GARW,
346        ];
347        for id in &ids {
348            assert!(
349                r.get(*id).is_some(),
350                "codec {} should be pre-registered",
351                id
352            );
353        }
354    }
355
356    // ── 2. Correct codec count ────────────────────────────────────────────────
357
358    #[test]
359    fn test_builtin_codec_count() {
360        let r = CodecRegistry::new();
361        assert_eq!(r.list_all().len(), 7);
362    }
363
364    // ── 3. `get` returns the correct descriptor ────────────────────────────────
365
366    #[test]
367    fn test_get_zstd_descriptor() {
368        let r = CodecRegistry::new();
369        let desc = r.get(CodecId::ZSTD).expect("ZSTD should exist");
370        assert_eq!(desc.id, CodecId::ZSTD);
371        assert_eq!(desc.name, "zstd");
372        assert!(desc.is_lossless);
373        assert_eq!(desc.speed_class, SpeedClass::Balanced);
374    }
375
376    #[test]
377    fn test_get_none_descriptor() {
378        let r = CodecRegistry::new();
379        let desc = r.get(CodecId::NONE).expect("NONE should exist");
380        assert_eq!(desc.compression_ratio_estimate, 1.0);
381        assert_eq!(desc.speed_class, SpeedClass::VeryFast);
382    }
383
384    // ── 4. `get` returns None for unknown id ─────────────────────────────────
385
386    #[test]
387    fn test_get_unknown_returns_none() {
388        let r = CodecRegistry::new();
389        assert!(r.get(CodecId::from(9999)).is_none());
390    }
391
392    // ── 5. `negotiate` finds intersection ────────────────────────────────────
393
394    #[test]
395    fn test_negotiate_finds_common_codec() {
396        let local = vec![CodecId::ZSTD, CodecId::LZ4, CodecId::NONE];
397        let remote = vec![CodecId::LZ4, CodecId::NONE];
398        let agreed = CodecRegistry::negotiate(&local, &remote);
399        assert_eq!(agreed, Some(CodecId::LZ4));
400    }
401
402    #[test]
403    fn test_negotiate_respects_local_preference_order() {
404        // local prefers ZSTD; remote supports all — ZSTD wins
405        let local = vec![CodecId::ZSTD, CodecId::LZ4, CodecId::NONE];
406        let remote = vec![CodecId::NONE, CodecId::LZ4, CodecId::ZSTD];
407        let agreed = CodecRegistry::negotiate(&local, &remote);
408        assert_eq!(agreed, Some(CodecId::ZSTD));
409    }
410
411    // ── 6. `negotiate` returns None when no common codec ──────────────────────
412
413    #[test]
414    fn test_negotiate_no_common_returns_none() {
415        let local = vec![CodecId::ZSTD, CodecId::BROTLI];
416        let remote = vec![CodecId::LZ4, CodecId::SNAPPY];
417        assert_eq!(CodecRegistry::negotiate(&local, &remote), None);
418    }
419
420    #[test]
421    fn test_negotiate_empty_lists_returns_none() {
422        assert_eq!(CodecRegistry::negotiate(&[], &[]), None);
423        assert_eq!(CodecRegistry::negotiate(&[CodecId::ZSTD], &[]), None);
424        assert_eq!(CodecRegistry::negotiate(&[], &[CodecId::ZSTD]), None);
425    }
426
427    // ── 7. `best_for_speed` returns a VeryFast codec ─────────────────────────
428
429    #[test]
430    fn test_best_for_speed_is_very_fast() {
431        let r = CodecRegistry::new();
432        let best = r.best_for_speed().expect("registry is non-empty");
433        assert_eq!(
434            best.speed_class,
435            SpeedClass::VeryFast,
436            "expected VeryFast, got {:?}",
437            best.speed_class
438        );
439    }
440
441    // ── 8. `best_for_compression` returns the lowest ratio ────────────────────
442
443    #[test]
444    fn test_best_for_compression_is_lowest_ratio() {
445        let r = CodecRegistry::new();
446        let best = r.best_for_compression().expect("registry is non-empty");
447        // Brotli has ratio 0.25, which is the lowest among built-ins
448        assert_eq!(
449            best.id,
450            CodecId::BROTLI,
451            "expected BROTLI (0.25), got {} ({})",
452            best.name,
453            best.compression_ratio_estimate
454        );
455    }
456
457    // ── 9. `register` custom codec succeeds ───────────────────────────────────
458
459    #[test]
460    fn test_register_custom_codec() {
461        let mut r = CodecRegistry::new();
462        let custom = CodecDescriptor {
463            id: CodecId::from(1000),
464            name: "my_codec".to_string(),
465            compression_ratio_estimate: 0.45,
466            speed_class: SpeedClass::Fast,
467            is_lossless: true,
468        };
469        r.register(custom)
470            .expect("custom registration should succeed");
471        let found = r
472            .get(CodecId::from(1000))
473            .expect("custom codec should exist");
474        assert_eq!(found.name, "my_codec");
475    }
476
477    // ── 10. Duplicate registration returns AlreadyRegistered error ───────────
478
479    #[test]
480    fn test_register_duplicate_returns_error() {
481        let mut r = CodecRegistry::new();
482        let dup = CodecDescriptor {
483            id: CodecId::ZSTD, // already registered
484            name: "duplicate_zstd".to_string(),
485            compression_ratio_estimate: 0.30,
486            speed_class: SpeedClass::Balanced,
487            is_lossless: true,
488        };
489        let err = r.register(dup).expect_err("duplicate should be rejected");
490        match err {
491            CodecError::AlreadyRegistered(id) => assert_eq!(id, CodecId::ZSTD.value()),
492            other => panic!("unexpected error: {}", other),
493        }
494    }
495
496    // ── 11. `list_all` returns entries in ascending CodecId order ─────────────
497
498    #[test]
499    fn test_list_all_sorted_order() {
500        let r = CodecRegistry::new();
501        let list = r.list_all();
502        let ids: Vec<u32> = list.iter().map(|d| d.id.value()).collect();
503        let mut sorted = ids.clone();
504        sorted.sort_unstable();
505        assert_eq!(ids, sorted, "list_all() must be sorted by CodecId");
506    }
507
508    #[test]
509    fn test_list_all_first_is_none() {
510        let r = CodecRegistry::new();
511        let list = r.list_all();
512        assert_eq!(list[0].id, CodecId::NONE);
513    }
514
515    // ── 12. CodecId constants have correct numeric values ────────────────────
516
517    #[test]
518    fn test_codec_id_constants() {
519        assert_eq!(CodecId::NONE.value(), 0);
520        assert_eq!(CodecId::ZSTD.value(), 1);
521        assert_eq!(CodecId::LZ4.value(), 2);
522        assert_eq!(CodecId::SNAPPY.value(), 3);
523        assert_eq!(CodecId::BROTLI.value(), 4);
524        assert_eq!(CodecId::ARROW_IPC.value(), 10);
525        assert_eq!(CodecId::GARW.value(), 11);
526    }
527
528    // ── 13. CodecId Display ───────────────────────────────────────────────────
529
530    #[test]
531    fn test_codec_id_display() {
532        assert_eq!(CodecId::ZSTD.to_string(), "CodecId(1)");
533        assert_eq!(CodecId::NONE.to_string(), "CodecId(0)");
534    }
535
536    // ── 14. CodecNegotiationRecord stores correct data ────────────────────────
537
538    #[test]
539    fn test_codec_negotiation_record() {
540        let local = vec![CodecId::ZSTD, CodecId::LZ4];
541        let remote = vec![CodecId::LZ4];
542        let start = Instant::now();
543        let agreed = CodecRegistry::negotiate(&local, &remote);
544        let elapsed_ms = start.elapsed().as_millis() as u64;
545
546        let record = CodecNegotiationRecord {
547            local_offered: local.clone(),
548            remote_offered: remote.clone(),
549            agreed,
550            negotiated_at: Instant::now(),
551            negotiation_ms: elapsed_ms,
552        };
553
554        assert_eq!(record.agreed, Some(CodecId::LZ4));
555        assert_eq!(record.local_offered.len(), 2);
556        assert_eq!(record.remote_offered.len(), 1);
557    }
558
559    // ── 15. SpeedClass ordering ────────────────────────────────────────────────
560
561    #[test]
562    fn test_speed_class_ordering() {
563        assert!(SpeedClass::VeryFast < SpeedClass::Fast);
564        assert!(SpeedClass::Fast < SpeedClass::Balanced);
565        assert!(SpeedClass::Balanced < SpeedClass::Slow);
566        assert!(SpeedClass::Slow < SpeedClass::VerySlow);
567    }
568
569    // ── 16. best_for_speed with registry containing only slow codecs ──────────
570
571    #[test]
572    fn test_best_for_speed_custom_only_slow() {
573        let mut r = CodecRegistry::new();
574        // Add an even slower custom codec; the result should still be the
575        // built-in VeryFast codec.
576        let custom = CodecDescriptor {
577            id: CodecId::from(2000),
578            name: "super_slow".to_string(),
579            compression_ratio_estimate: 0.10,
580            speed_class: SpeedClass::VerySlow,
581            is_lossless: true,
582        };
583        r.register(custom).expect("should register");
584        let best = r.best_for_speed().expect("non-empty");
585        assert_eq!(best.speed_class, SpeedClass::VeryFast);
586    }
587
588    // ── 17. UnknownCodec error variant Display ────────────────────────────────
589
590    #[test]
591    fn test_unknown_codec_error_display() {
592        let err = CodecError::UnknownCodec(42);
593        assert!(err.to_string().contains("42"));
594    }
595
596    // ── 18. Default impl matches new() ────────────────────────────────────────
597
598    #[test]
599    fn test_default_matches_new() {
600        let a = CodecRegistry::new();
601        let b = CodecRegistry::default();
602        assert_eq!(a.list_all().len(), b.list_all().len());
603    }
604}