memlink-runtime 0.2.0

Dynamic module loading framework with circuit breaker, caching, pooling, health checks, versioning, and auto-discovery
Documentation
//! Module versioning and side-by-side execution.
//!
//! Supports loading multiple versions of the same module
//! and gradual migration between versions.

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;

use dashmap::DashMap;

/// Semantic version representation.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModuleVersion {
    /// Major version (breaking changes).
    pub major: u32,
    /// Minor version (new features).
    pub minor: u32,
    /// Patch version (bug fixes).
    pub patch: u32,
}

impl ModuleVersion {
    /// Creates a new version.
    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
        Self { major, minor, patch }
    }

    /// Parses a version string (e.g., "1.2.3").
    pub fn parse(s: &str) -> Option<Self> {
        let parts: Vec<&str> = s.split('.').collect();
        if parts.len() != 3 {
            return None;
        }

        Some(Self {
            major: parts[0].parse().ok()?,
            minor: parts[1].parse().ok()?,
            patch: parts[2].parse().ok()?,
        })
    }

    /// Returns the version as a string.
    pub fn as_str(&self) -> String {
        format!("{}.{}.{}", self.major, self.minor, self.patch)
    }

    /// Checks if this version is compatible with another.
    /// Compatible means same major version.
    pub fn is_compatible_with(&self, other: &ModuleVersion) -> bool {
        self.major == other.major
    }

    /// Checks if this version is greater than another.
    pub fn is_greater_than(&self, other: &ModuleVersion) -> bool {
        if self.major != other.major {
            return self.major > other.major;
        }
        if self.minor != other.minor {
            return self.minor > other.minor;
        }
        self.patch > other.patch
    }
}

impl std::fmt::Display for ModuleVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

/// Versioned module instance.
#[derive(Debug, Clone)]
pub struct VersionedModule {
    /// Module name.
    pub name: String,
    /// Module version.
    pub version: ModuleVersion,
    /// Module path.
    pub path: String,
    /// Whether this is the active version.
    pub is_active: bool,
}

/// Traffic routing configuration for gradual migration.
#[derive(Debug, Clone)]
pub struct TrafficRouting {
    /// Percentage of traffic to new version (0-100).
    pub new_version_percentage: u32,
    /// Minimum requests before evaluating.
    pub min_requests: u32,
    /// Error rate threshold for rollback (0.0-1.0).
    pub error_threshold: f32,
}

impl Default for TrafficRouting {
    fn default() -> Self {
        Self {
            new_version_percentage: 0,
            min_requests: 100,
            error_threshold: 0.01, // 1%
        }
    }
}

/// Version manager for a module.
#[derive(Debug)]
pub struct VersionManager {
    /// Module name.
    module_name: String,
    /// Loaded versions.
    versions: DashMap<ModuleVersion, VersionedModule>,
    /// Active version.
    active_version: std::sync::Mutex<Option<ModuleVersion>>,
    /// Traffic routing config.
    routing: std::sync::Mutex<TrafficRouting>,
    /// Requests to new version.
    new_version_requests: AtomicU32,
    /// Errors from new version.
    new_version_errors: AtomicU32,
}

impl VersionManager {
    /// Creates a new version manager.
    pub fn new(module_name: String) -> Self {
        Self {
            module_name,
            versions: DashMap::new(),
            active_version: std::sync::Mutex::new(None),
            routing: std::sync::Mutex::new(TrafficRouting::default()),
            new_version_requests: AtomicU32::new(0),
            new_version_errors: AtomicU32::new(0),
        }
    }

    /// Registers a new version.
    pub fn register_version(&self, version: ModuleVersion, path: String) {
        let module = VersionedModule {
            name: self.module_name.clone(),
            version: version.clone(),
            path,
            is_active: false,
        };
        self.versions.insert(version, module);
    }

    /// Activates a version.
    pub fn activate_version(&self, version: &ModuleVersion) -> bool {
        if !self.versions.contains_key(version) {
            return false;
        }

        // Deactivate current active version
        for entry in self.versions.iter() {
            let mut module = entry.value().clone();
            module.is_active = false;
            // Note: Can't mutate through DashMap iter, would need different approach
        }

        // Activate new version - simplified for now
        *self.active_version.lock().unwrap() = Some(version.clone());
        true
    }

    /// Selects a version for a request (for gradual migration).
    pub fn select_version(&self) -> Option<ModuleVersion> {
        let routing = self.routing.lock().unwrap();

        // If no traffic to new version, use active
        if routing.new_version_percentage == 0 {
            return self.active_version.lock().unwrap().clone();
        }

        // Determine if this request goes to new version
        use std::collections::hash_map::RandomState;
        use std::hash::{BuildHasher, Hasher};

        let hasher = RandomState::new().build_hasher();
        let roll = hasher.finish() % 100;

        if roll < routing.new_version_percentage as u64 {
            // Route to new version (highest version)
            self.versions
                .iter()
                .max_by(|a, b| a.key().cmp(b.key()))
                .map(|e| e.key().clone())
        } else {
            // Route to active version
            self.active_version.lock().unwrap().clone()
        }
    }

    /// Records a request to the new version.
    pub fn record_request(&self) {
        self.new_version_requests.fetch_add(1, Ordering::Relaxed);
    }

    /// Records an error from the new version.
    pub fn record_error(&self) {
        self.new_version_errors.fetch_add(1, Ordering::Relaxed);
    }

    /// Checks if migration should continue or rollback.
    pub fn should_rollback(&self) -> bool {
        let routing = self.routing.lock().unwrap();
        let requests = self.new_version_requests.load(Ordering::Relaxed);

        if requests < routing.min_requests {
            return false;
        }

        let errors = self.new_version_errors.load(Ordering::Relaxed);
        let error_rate = errors as f32 / requests as f32;

        error_rate > routing.error_threshold
    }

    /// Returns all loaded versions.
    pub fn all_versions(&self) -> Vec<VersionedModule> {
        self.versions.iter().map(|e| e.value().clone()).collect()
    }

    /// Returns the active version.
    pub fn active_version(&self) -> Option<ModuleVersion> {
        self.active_version.lock().unwrap().clone()
    }

    /// Sets traffic routing configuration.
    pub fn set_routing(&self, routing: TrafficRouting) {
        *self.routing.lock().unwrap() = routing;
    }
}

/// Version registry for all modules.
#[derive(Debug)]
pub struct VersionRegistry {
    /// Version managers by module name.
    managers: DashMap<String, Arc<VersionManager>>,
}

impl VersionRegistry {
    /// Creates a new version registry.
    pub fn new() -> Self {
        Self {
            managers: DashMap::new(),
        }
    }

    /// Gets or creates a version manager.
    pub fn get_or_create(&self, module_name: &str) -> Arc<VersionManager> {
        self.managers
            .entry(module_name.to_string())
            .or_insert_with(|| Arc::new(VersionManager::new(module_name.to_string())))
            .clone()
    }

    /// Returns the number of managed modules.
    pub fn module_count(&self) -> usize {
        self.managers.len()
    }
}

impl Default for VersionRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_version_parsing() {
        let v = ModuleVersion::parse("1.2.3").unwrap();
        assert_eq!(v.major, 1);
        assert_eq!(v.minor, 2);
        assert_eq!(v.patch, 3);
    }

    #[test]
    fn test_version_comparison() {
        let v1 = ModuleVersion::new(1, 0, 0);
        let v2 = ModuleVersion::new(1, 0, 1);
        let v3 = ModuleVersion::new(2, 0, 0);

        assert!(v2.is_greater_than(&v1));
        assert!(v3.is_greater_than(&v2));
        assert!(v1.is_compatible_with(&v2));
        assert!(!v1.is_compatible_with(&v3));
    }

    #[test]
    fn test_version_manager() {
        let manager = VersionManager::new("test".to_string());

        manager.register_version(ModuleVersion::new(1, 0, 0), "/path/v1".to_string());
        manager.register_version(ModuleVersion::new(2, 0, 0), "/path/v2".to_string());

        manager.activate_version(&ModuleVersion::new(1, 0, 0));

        assert_eq!(manager.active_version(), Some(ModuleVersion::new(1, 0, 0)));
        assert_eq!(manager.all_versions().len(), 2);
    }
}