Skip to main content

asupersync/migration/
mod.rs

1//! Migration path and backward compatibility layer.
2//!
3//! Allows gradual adoption of RaptorQ symbol-native operations while
4//! maintaining compatibility with existing traditional code paths.
5//! Features can be enabled individually via [`MigrationBuilder`].
6
7use std::collections::{HashMap, HashSet};
8use std::marker::PhantomData;
9
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12
13use crate::config::EncodingConfig;
14use crate::types::symbol::ObjectId;
15
16// ============================================================================
17// MigrationMode
18// ============================================================================
19
20/// Controls how operations handle dual-mode values.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub enum MigrationMode {
23    /// Only use traditional mode (no RaptorQ).
24    TraditionalOnly,
25    /// Default to traditional, RaptorQ opt-in.
26    #[default]
27    PreferTraditional,
28    /// Use RaptorQ when beneficial, fall back to traditional.
29    Adaptive,
30    /// Default to RaptorQ, traditional opt-in.
31    PreferSymbolNative,
32    /// Only use RaptorQ (errors on traditional-only operations).
33    SymbolNativeOnly,
34}
35
36impl MigrationMode {
37    /// Whether to use RaptorQ for a given operation.
38    ///
39    /// Explicit hints always override the mode. In `Adaptive` mode,
40    /// payloads larger than 1024 bytes default to RaptorQ.
41    #[must_use]
42    pub fn should_use_raptorq(&self, hint: Option<bool>, data_size: usize) -> bool {
43        match (self, hint) {
44            // Explicit hints always win; otherwise prefer symbol-native modes.
45            (_, Some(true)) | (Self::SymbolNativeOnly | Self::PreferSymbolNative, None) => true,
46            (_, Some(false)) | (Self::TraditionalOnly | Self::PreferTraditional, None) => false,
47            // Adaptive mode uses size heuristic
48            (Self::Adaptive, None) => data_size > 1024,
49        }
50    }
51}
52
53// ============================================================================
54// MigrationFeature
55// ============================================================================
56
57/// Individual features that can be toggled during migration.
58#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
59pub enum MigrationFeature {
60    /// Enable RaptorQ for join operations.
61    JoinEncoding,
62    /// Enable RaptorQ for race operations.
63    RaceEncoding,
64    /// Enable distributed region encoding.
65    DistributedRegions,
66    /// Enable symbol-based cancellation.
67    SymbolCancellation,
68    /// Enable symbol-based tracing.
69    SymbolTracing,
70    /// Enable epoch barriers.
71    EpochBarriers,
72}
73
74impl MigrationFeature {
75    /// Returns an iterator over all features.
76    pub fn all() -> impl Iterator<Item = Self> {
77        [
78            Self::JoinEncoding,
79            Self::RaceEncoding,
80            Self::DistributedRegions,
81            Self::SymbolCancellation,
82            Self::SymbolTracing,
83            Self::EpochBarriers,
84        ]
85        .into_iter()
86    }
87}
88
89// ============================================================================
90// MigrationConfig
91// ============================================================================
92
93/// Active migration configuration.
94#[derive(Debug, Clone, Default)]
95pub struct MigrationConfig {
96    /// Enabled features.
97    features: HashSet<MigrationFeature>,
98    /// Global migration mode.
99    mode: MigrationMode,
100    /// Per-operation overrides.
101    overrides: HashMap<String, MigrationMode>,
102}
103
104impl MigrationConfig {
105    /// Returns true if a feature is enabled.
106    #[must_use]
107    pub fn is_enabled(&self, feature: MigrationFeature) -> bool {
108        self.features.contains(&feature)
109    }
110
111    /// Returns the global migration mode.
112    #[must_use]
113    pub fn mode(&self) -> MigrationMode {
114        self.mode
115    }
116
117    /// Returns the set of enabled features.
118    #[must_use]
119    pub fn enabled_features(&self) -> &HashSet<MigrationFeature> {
120        &self.features
121    }
122
123    /// Returns the mode override for a specific operation, if set.
124    #[must_use]
125    pub fn mode_for(&self, operation: &str) -> MigrationMode {
126        self.overrides.get(operation).copied().unwrap_or(self.mode)
127    }
128}
129
130// ============================================================================
131// MigrationBuilder
132// ============================================================================
133
134/// Builder for [`MigrationConfig`].
135#[derive(Debug, Default)]
136pub struct MigrationBuilder {
137    /// Features to enable.
138    features: HashSet<MigrationFeature>,
139    /// Global mode.
140    mode: MigrationMode,
141    /// Per-operation overrides.
142    overrides: HashMap<String, MigrationMode>,
143}
144
145impl MigrationBuilder {
146    /// Creates a new builder with defaults.
147    #[must_use]
148    pub fn new() -> Self {
149        Self::default()
150    }
151
152    /// Enable a specific migration feature.
153    #[must_use]
154    pub fn enable(mut self, feature: MigrationFeature) -> Self {
155        self.features.insert(feature);
156        self
157    }
158
159    /// Disable a specific feature.
160    #[must_use]
161    pub fn disable(mut self, feature: MigrationFeature) -> Self {
162        self.features.remove(&feature);
163        self
164    }
165
166    /// Set the global migration mode.
167    #[must_use]
168    pub fn with_mode(mut self, mode: MigrationMode) -> Self {
169        self.mode = mode;
170        self
171    }
172
173    /// Override the mode for a specific operation.
174    #[must_use]
175    pub fn override_operation(mut self, operation: impl Into<String>, mode: MigrationMode) -> Self {
176        self.overrides.insert(operation.into(), mode);
177        self
178    }
179
180    /// Enable all features (full RaptorQ mode).
181    #[must_use]
182    pub fn full_raptorq(mut self) -> Self {
183        self.features = MigrationFeature::all().collect();
184        self.mode = MigrationMode::SymbolNativeOnly;
185        self
186    }
187
188    /// Build the migration configuration.
189    #[must_use]
190    pub fn build(self) -> MigrationConfig {
191        MigrationConfig {
192            features: self.features,
193            mode: self.mode,
194            overrides: self.overrides,
195        }
196    }
197}
198
199/// Entry point for configuring migration.
200#[must_use]
201pub fn configure_migration() -> MigrationBuilder {
202    MigrationBuilder::new()
203}
204
205// ============================================================================
206// DualValueError
207// ============================================================================
208
209/// Errors from [`DualValue`] operations.
210#[derive(Debug)]
211pub enum DualValueError {
212    /// Serialization to symbol encoding failed.
213    SerializationFailed(String),
214    /// Deserialization from symbol encoding failed.
215    DeserializationFailed(String),
216}
217
218impl std::fmt::Display for DualValueError {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        match self {
221            Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
222            Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
223        }
224    }
225}
226
227impl std::error::Error for DualValueError {}
228
229// ============================================================================
230// DualValue
231// ============================================================================
232
233/// A value that can be held in either traditional or symbol-encoded form.
234///
235/// In traditional mode, the value is stored directly. In symbol-encoded mode,
236/// the value is serialized and can be transmitted as symbols. Both forms
237/// support retrieving the underlying value via [`get`][DualValue::get].
238pub enum DualValue<T> {
239    /// Traditional direct value.
240    Traditional(T),
241    /// Symbol-encoded value with serialized bytes and metadata.
242    SymbolNative {
243        /// Serialized representation.
244        serialized: Vec<u8>,
245        /// Object identifier.
246        object_id: ObjectId,
247        /// Type marker.
248        _phantom: PhantomData<T>,
249    },
250}
251
252impl<T> DualValue<T> {
253    /// Returns true if this value is in symbol-encoded form.
254    #[must_use]
255    pub fn uses_raptorq(&self) -> bool {
256        matches!(self, Self::SymbolNative { .. })
257    }
258
259    /// Returns true if this value is in traditional form.
260    #[must_use]
261    pub fn is_traditional(&self) -> bool {
262        matches!(self, Self::Traditional(_))
263    }
264}
265
266impl<T: Clone + Serialize + DeserializeOwned> DualValue<T> {
267    /// Retrieves the underlying value, deserializing if necessary.
268    pub fn get(&self) -> Result<T, DualValueError> {
269        match self {
270            Self::Traditional(v) => Ok(v.clone()),
271            Self::SymbolNative { serialized, .. } => serde_json::from_slice(serialized)
272                .map_err(|e| DualValueError::DeserializationFailed(e.to_string())),
273        }
274    }
275
276    /// Converts to symbol-encoded form if not already.
277    ///
278    /// The `_config` parameter is reserved for future use with actual
279    /// RaptorQ encoding configuration.
280    pub fn ensure_symbols(&mut self, _config: &EncodingConfig) {
281        if let Self::Traditional(v) = self {
282            let serialized =
283                serde_json::to_vec(v).expect("serialization of existing value should succeed");
284            let object_id = ObjectId::new_for_test(0);
285            *self = Self::SymbolNative {
286                serialized,
287                object_id,
288                _phantom: PhantomData,
289            };
290        }
291    }
292}
293
294impl<T: std::fmt::Debug> std::fmt::Debug for DualValue<T> {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        match self {
297            Self::Traditional(v) => f.debug_tuple("Traditional").field(v).finish(),
298            Self::SymbolNative {
299                serialized,
300                object_id,
301                ..
302            } => f
303                .debug_struct("SymbolNative")
304                .field("bytes", &serialized.len())
305                .field("object_id", object_id)
306                .finish(),
307        }
308    }
309}
310
311// ============================================================================
312// Tests
313// ============================================================================
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_dual_value_traditional() {
321        let value = DualValue::Traditional(42i32);
322        assert_eq!(value.get().unwrap(), 42);
323        assert!(!value.uses_raptorq());
324    }
325
326    #[test]
327    fn test_dual_value_conversion() {
328        let mut value = DualValue::Traditional("hello".to_string());
329        let config = EncodingConfig::default();
330
331        // Convert to symbol-native
332        value.ensure_symbols(&config);
333        assert!(matches!(value, DualValue::SymbolNative { .. }));
334
335        // Still get same value
336        assert_eq!(value.get().unwrap(), "hello".to_string());
337    }
338
339    #[test]
340    fn test_migration_mode_decisions() {
341        // Traditional only never uses RaptorQ
342        assert!(!MigrationMode::TraditionalOnly.should_use_raptorq(None, 10000));
343
344        // Symbol-native only always uses RaptorQ
345        assert!(MigrationMode::SymbolNativeOnly.should_use_raptorq(None, 10));
346
347        // Hints override mode
348        assert!(MigrationMode::TraditionalOnly.should_use_raptorq(Some(true), 10));
349        assert!(!MigrationMode::SymbolNativeOnly.should_use_raptorq(Some(false), 10));
350
351        // Adaptive uses heuristics
352        assert!(!MigrationMode::Adaptive.should_use_raptorq(None, 100));
353        assert!(MigrationMode::Adaptive.should_use_raptorq(None, 10000));
354    }
355
356    #[test]
357    fn test_migration_builder() {
358        let config = configure_migration()
359            .enable(MigrationFeature::JoinEncoding)
360            .enable(MigrationFeature::RaceEncoding)
361            .build();
362
363        assert!(config.is_enabled(MigrationFeature::JoinEncoding));
364        assert!(config.is_enabled(MigrationFeature::RaceEncoding));
365        assert!(!config.is_enabled(MigrationFeature::DistributedRegions));
366    }
367
368    #[test]
369    fn test_full_raptorq_mode() {
370        let config = configure_migration().full_raptorq().build();
371
372        for feature in MigrationFeature::all() {
373            assert!(config.is_enabled(feature));
374        }
375    }
376
377    #[test]
378    fn test_migration_mode_default() {
379        let mode = MigrationMode::default();
380        assert_eq!(mode, MigrationMode::PreferTraditional);
381    }
382
383    #[test]
384    fn test_migration_builder_disable() {
385        let config = configure_migration()
386            .full_raptorq()
387            .disable(MigrationFeature::SymbolTracing)
388            .build();
389
390        assert!(config.is_enabled(MigrationFeature::JoinEncoding));
391        assert!(!config.is_enabled(MigrationFeature::SymbolTracing));
392    }
393
394    #[test]
395    fn test_per_operation_override() {
396        let config = configure_migration()
397            .with_mode(MigrationMode::PreferTraditional)
398            .override_operation("heavy_join", MigrationMode::PreferSymbolNative)
399            .build();
400
401        assert_eq!(config.mode(), MigrationMode::PreferTraditional);
402        assert_eq!(
403            config.mode_for("heavy_join"),
404            MigrationMode::PreferSymbolNative
405        );
406        assert_eq!(
407            config.mode_for("other_op"),
408            MigrationMode::PreferTraditional
409        );
410    }
411
412    // ---- DualValueError ----
413
414    #[test]
415    fn dual_value_error_display_serialization() {
416        let err = DualValueError::SerializationFailed("bad input".into());
417        assert_eq!(err.to_string(), "serialization failed: bad input");
418    }
419
420    #[test]
421    fn dual_value_error_display_deserialization() {
422        let err = DualValueError::DeserializationFailed("unexpected EOF".into());
423        assert_eq!(err.to_string(), "deserialization failed: unexpected EOF");
424    }
425
426    #[test]
427    fn dual_value_error_source_is_none() {
428        use std::error::Error;
429        let err = DualValueError::SerializationFailed("x".into());
430        assert!(err.source().is_none());
431    }
432
433    // ---- DualValue predicates ----
434
435    #[test]
436    fn dual_value_is_traditional() {
437        let val = DualValue::Traditional(100u32);
438        assert!(val.is_traditional());
439        assert!(!val.uses_raptorq());
440    }
441
442    #[test]
443    fn dual_value_uses_raptorq_after_ensure_symbols() {
444        let mut val = DualValue::Traditional(42u32);
445        let config = EncodingConfig::default();
446        val.ensure_symbols(&config);
447        assert!(val.uses_raptorq());
448        assert!(!val.is_traditional());
449    }
450
451    #[test]
452    fn dual_value_ensure_symbols_idempotent() {
453        let mut val = DualValue::Traditional(42u32);
454        let config = EncodingConfig::default();
455        val.ensure_symbols(&config);
456        assert!(val.uses_raptorq());
457        // Second call should be a no-op (already SymbolNative)
458        val.ensure_symbols(&config);
459        assert!(val.uses_raptorq());
460        assert_eq!(val.get().unwrap(), 42u32);
461    }
462
463    #[test]
464    fn dual_value_get_deserialization_failure() {
465        // Construct a SymbolNative with garbage bytes that won't parse as u32
466        let bad = DualValue::<u32>::SymbolNative {
467            serialized: b"not valid json".to_vec(),
468            object_id: ObjectId::new_for_test(0),
469            _phantom: PhantomData,
470        };
471        let err = bad.get().unwrap_err();
472        assert!(matches!(err, DualValueError::DeserializationFailed(_)));
473    }
474
475    #[test]
476    fn dual_value_debug_traditional() {
477        let val = DualValue::Traditional(99i32);
478        let dbg = format!("{val:?}");
479        assert!(dbg.contains("Traditional"), "{dbg}");
480        assert!(dbg.contains("99"), "{dbg}");
481    }
482
483    #[test]
484    fn dual_value_debug_symbol_native() {
485        let mut val = DualValue::Traditional("hello".to_string());
486        let config = EncodingConfig::default();
487        val.ensure_symbols(&config);
488        let dbg = format!("{val:?}");
489        assert!(dbg.contains("SymbolNative"), "{dbg}");
490        assert!(dbg.contains("bytes"), "{dbg}");
491    }
492
493    // ---- MigrationConfig ----
494
495    #[test]
496    fn migration_config_enabled_features_returns_set() {
497        let config = configure_migration()
498            .enable(MigrationFeature::EpochBarriers)
499            .enable(MigrationFeature::SymbolCancellation)
500            .build();
501
502        let features = config.enabled_features();
503        assert_eq!(features.len(), 2);
504        assert!(features.contains(&MigrationFeature::EpochBarriers));
505        assert!(features.contains(&MigrationFeature::SymbolCancellation));
506    }
507
508    #[test]
509    fn migration_config_default_has_no_features() {
510        let config = MigrationConfig::default();
511        assert!(config.enabled_features().is_empty());
512        assert_eq!(config.mode(), MigrationMode::PreferTraditional);
513    }
514
515    // ---- MigrationMode::Adaptive boundary ----
516
517    #[test]
518    fn adaptive_mode_boundary_at_1024() {
519        // Exactly 1024 should NOT trigger RaptorQ (condition is > 1024)
520        assert!(!MigrationMode::Adaptive.should_use_raptorq(None, 1024));
521        // 1025 should trigger it
522        assert!(MigrationMode::Adaptive.should_use_raptorq(None, 1025));
523    }
524
525    #[test]
526    fn prefer_symbol_native_without_hint() {
527        assert!(MigrationMode::PreferSymbolNative.should_use_raptorq(None, 0));
528        assert!(MigrationMode::PreferSymbolNative.should_use_raptorq(None, 9999));
529    }
530
531    // ---- MigrationFeature ----
532
533    #[test]
534    fn migration_feature_all_has_six_items() {
535        assert_eq!(MigrationFeature::all().count(), 6);
536    }
537
538    #[test]
539    fn migration_feature_all_roundtrip_via_full_raptorq() {
540        let config = configure_migration().full_raptorq().build();
541        assert_eq!(config.mode(), MigrationMode::SymbolNativeOnly);
542        for feature in MigrationFeature::all() {
543            assert!(
544                config.is_enabled(feature),
545                "full_raptorq should enable {feature:?}"
546            );
547        }
548    }
549
550    // ---- MigrationBuilder ----
551
552    #[test]
553    fn migration_builder_with_mode() {
554        let config = MigrationBuilder::new()
555            .with_mode(MigrationMode::Adaptive)
556            .build();
557        assert_eq!(config.mode(), MigrationMode::Adaptive);
558    }
559
560    #[test]
561    fn migration_builder_multiple_overrides() {
562        let config = configure_migration()
563            .override_operation("op_a", MigrationMode::SymbolNativeOnly)
564            .override_operation("op_b", MigrationMode::TraditionalOnly)
565            .build();
566        assert_eq!(config.mode_for("op_a"), MigrationMode::SymbolNativeOnly);
567        assert_eq!(config.mode_for("op_b"), MigrationMode::TraditionalOnly);
568        // Fallback to global default
569        assert_eq!(config.mode_for("op_c"), MigrationMode::PreferTraditional);
570    }
571
572    #[test]
573    fn migration_mode_debug_clone_copy_default_eq() {
574        let m = MigrationMode::Adaptive;
575        let dbg = format!("{m:?}");
576        assert!(dbg.contains("Adaptive"), "{dbg}");
577        let copied: MigrationMode = m;
578        let cloned = m;
579        assert_eq!(copied, cloned);
580        assert_eq!(MigrationMode::default(), MigrationMode::PreferTraditional);
581        assert_ne!(m, MigrationMode::TraditionalOnly);
582    }
583
584    #[test]
585    fn migration_feature_debug_clone_copy_hash_eq() {
586        use std::collections::HashSet;
587        let f = MigrationFeature::JoinEncoding;
588        let dbg = format!("{f:?}");
589        assert!(dbg.contains("JoinEncoding"), "{dbg}");
590        let copied: MigrationFeature = f;
591        let cloned = f;
592        assert_eq!(copied, cloned);
593
594        let mut set = HashSet::new();
595        set.insert(MigrationFeature::JoinEncoding);
596        set.insert(MigrationFeature::RaceEncoding);
597        set.insert(MigrationFeature::DistributedRegions);
598        assert_eq!(set.len(), 3);
599    }
600
601    #[test]
602    fn migration_config_debug_clone_default() {
603        let c = MigrationConfig::default();
604        let dbg = format!("{c:?}");
605        assert!(dbg.contains("MigrationConfig"), "{dbg}");
606        let cloned = c;
607        assert_eq!(format!("{cloned:?}"), dbg);
608    }
609}