memlink-runtime 0.2.0

Dynamic module loading framework with circuit breaker, caching, pooling, health checks, versioning, and auto-discovery
Documentation
//! Module discovery and auto-loading.
//!
//! Watches directories for module files and auto-loads them.

use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;

use dashmap::DashMap;

/// Module file information.
#[derive(Debug, Clone)]
pub struct ModuleFile {
    /// Path to the module file.
    pub path: PathBuf,
    /// Module name (derived from filename).
    pub name: String,
    /// File size in bytes.
    pub size: u64,
    /// Last modified timestamp.
    pub modified: std::time::SystemTime,
}

/// Discovery configuration.
#[derive(Debug, Clone)]
pub struct DiscoveryConfig {
    /// Directories to watch.
    pub watch_dirs: Vec<PathBuf>,
    /// File extensions to look for.
    pub extensions: Vec<String>,
    /// Polling interval for file system.
    pub poll_interval: Duration,
    /// Whether to auto-load discovered modules.
    pub auto_load: bool,
    /// Whether to reload on file changes.
    pub auto_reload: bool,
}

impl Default for DiscoveryConfig {
    fn default() -> Self {
        Self {
            watch_dirs: vec![PathBuf::from("./modules")],
            extensions: vec![
                ".so".to_string(),    // Linux
                ".dll".to_string(),   // Windows
                ".dylib".to_string(), // macOS
            ],
            poll_interval: Duration::from_secs(5),
            auto_load: true,
            auto_reload: false,
        }
    }
}

/// Discovered module status.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleStatus {
    /// Module discovered but not loaded.
    Discovered,
    /// Module is loading.
    Loading,
    /// Module is loaded and active.
    Loaded,
    /// Module failed to load.
    Failed,
    /// Module file was removed.
    Removed,
}

/// Discovered module information.
#[derive(Debug, Clone)]
pub struct DiscoveredModule {
    /// Module file information.
    pub file: ModuleFile,
    /// Current status.
    pub status: ModuleStatus,
    /// Load attempts.
    pub attempts: u32,
    /// Last error message.
    pub last_error: Option<String>,
    /// Discovery timestamp.
    pub discovered_at: std::time::SystemTime,
}

/// Module discovery service.
#[derive(Debug)]
pub struct ModuleDiscovery {
    /// Configuration.
    config: DiscoveryConfig,
    /// Discovered modules.
    modules: DashMap<String, DiscoveredModule>,
    /// Discovery running flag.
    running: AtomicBool,
    /// Total discoveries.
    total_discoveries: AtomicU64,
}

impl ModuleDiscovery {
    /// Creates a new discovery service.
    pub fn new(config: DiscoveryConfig) -> Self {
        Self {
            config,
            modules: DashMap::new(),
            running: AtomicBool::new(false),
            total_discoveries: AtomicU64::new(0),
        }
    }

    /// Creates a discovery service with default configuration.
    pub fn with_defaults() -> Self {
        Self::new(DiscoveryConfig::default())
    }

    /// Scans configured directories for modules.
    pub fn scan(&self) -> Vec<ModuleFile> {
        let mut found = Vec::new();

        for dir in &self.config.watch_dirs {
            if !dir.exists() {
                continue;
            }

            if let Ok(entries) = std::fs::read_dir(dir) {
                for entry in entries.flatten() {
                    let path = entry.path();

                    // Check extension
                    let ext = path.extension()
                        .and_then(|e| e.to_str())
                        .unwrap_or("");

                    if !self.config.extensions.iter().any(|e| e == &format!(".{}", ext)) {
                        continue;
                    }

                    // Get file info
                    if let Ok(metadata) = path.metadata() {
                        let name = path.file_stem()
                            .and_then(|s| s.to_str())
                            .unwrap_or("unknown")
                            .to_string();

                        found.push(ModuleFile {
                            path,
                            name,
                            size: metadata.len(),
                            modified: metadata.modified().unwrap_or(std::time::UNIX_EPOCH),
                        });
                    }
                }
            }
        }

        found
    }

    /// Registers a discovered module.
    pub fn register(&self, file: ModuleFile) {
        let module = DiscoveredModule {
            file,
            status: ModuleStatus::Discovered,
            attempts: 0,
            last_error: None,
            discovered_at: std::time::SystemTime::now(),
        };

        self.modules.insert(module.file.name.clone(), module);
        self.total_discoveries.fetch_add(1, Ordering::Relaxed);
    }

    /// Updates module status.
    pub fn update_status(&self, name: &str, status: ModuleStatus) {
        if let Some(mut module) = self.modules.get_mut(name) {
            module.status = status;
        }
    }

    /// Records a load failure.
    pub fn record_failure(&self, name: &str, error: String) {
        if let Some(mut module) = self.modules.get_mut(name) {
            module.attempts += 1;
            module.last_error = Some(error);
            module.status = ModuleStatus::Failed;
        }
    }

    /// Returns discovered modules.
    pub fn discovered_modules(&self) -> Vec<DiscoveredModule> {
        self.modules.iter().map(|e| e.value().clone()).collect()
    }

    /// Returns modules by status.
    pub fn modules_by_status(&self, status: ModuleStatus) -> Vec<DiscoveredModule> {
        self.modules
            .iter()
            .filter(|e| e.value().status == status)
            .map(|e| e.value().clone())
            .collect()
    }

    /// Returns whether discovery is running.
    pub fn is_running(&self) -> bool {
        self.running.load(Ordering::Relaxed)
    }

    /// Returns total discoveries.
    pub fn total_discoveries(&self) -> u64 {
        self.total_discoveries.load(Ordering::Relaxed)
    }
}

impl Default for ModuleDiscovery {
    fn default() -> Self {
        Self::with_defaults()
    }
}

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

    #[test]
    fn test_discovery_config() {
        let config = DiscoveryConfig::default();
        assert!(!config.watch_dirs.is_empty());
        assert_eq!(config.extensions.len(), 3);
    }

    #[test]
    fn test_discovery_creation() {
        let discovery = ModuleDiscovery::with_defaults();
        assert!(!discovery.is_running());
        assert_eq!(discovery.total_discoveries(), 0);
    }
}