vortex-core 0.1.0

Core types and deterministic scheduler for Vortex simulation engine
Documentation
//! Plugin trait for custom fault injection modules.
//!
//! Implement [`VortexPlugin`] to add custom fault types that integrate
//! with all Vortex layers. Plugins are registered at runtime and evaluated
//! alongside the built-in fault rules.
//!
//! # Example
//! ```
//! use vortex_core::plugin::{VortexPlugin, FaultCheck, PluginContext};
//! use vortex_core::DetRng;
//!
//! struct GrpcPlugin;
//!
//! impl VortexPlugin for GrpcPlugin {
//!     fn name(&self) -> &str { "grpc" }
//!
//!     fn check_fault(&self, ctx: &mut PluginContext, operation: &str) -> FaultCheck {
//!         if operation.starts_with("grpc:") && ctx.rng.chance(0.1) {
//!             FaultCheck::Inject { message: "gRPC unavailable".into() }
//!         } else {
//!             FaultCheck::Pass
//!         }
//!     }
//! }
//! ```

use crate::DetRng;

/// Result of a plugin fault check.
#[derive(Debug, Clone)]
pub enum FaultCheck {
    /// No fault — operation proceeds normally.
    Pass,
    /// Inject a fault with the given error message.
    Inject { message: String },
    /// Inject a delay (microseconds) before the operation.
    Delay { us: u64 },
}

/// Context passed to plugins during fault evaluation.
pub struct PluginContext<'a> {
    /// Deterministic RNG for probability-based decisions.
    pub rng: &'a mut DetRng,
    /// The current seed.
    pub seed: u64,
    /// Arbitrary key-value metadata (e.g., path, address, fd).
    pub metadata: &'a std::collections::HashMap<String, String>,
}

/// Trait for custom fault injection plugins.
///
/// Implement this to add custom fault types (e.g., gRPC errors, DNS failures,
/// TLS handshake faults) that integrate with Vortex's simulation layers.
pub trait VortexPlugin: Send + Sync {
    /// Plugin name (used for logging and configuration).
    fn name(&self) -> &str;

    /// Check if a fault should be injected for the given operation.
    ///
    /// Called by the simulation engine before each intercepted operation.
    /// The `operation` string describes what's happening (e.g., "fs:write:/data.wal",
    /// "net:connect:127.0.0.1:5432", "grpc:call:UserService/GetUser").
    fn check_fault(&self, ctx: &mut PluginContext<'_>, operation: &str) -> FaultCheck;

    /// Called once when the plugin is registered. Use for setup.
    fn on_register(&self) {}

    /// Called once when the simulation ends. Use for cleanup/reporting.
    fn on_shutdown(&self) {}
}

/// Registry of active plugins.
pub struct PluginRegistry {
    plugins: Vec<Box<dyn VortexPlugin>>,
}

impl PluginRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self {
            plugins: Vec::new(),
        }
    }

    /// Register a plugin.
    pub fn register(&mut self, plugin: Box<dyn VortexPlugin>) {
        plugin.on_register();
        self.plugins.push(plugin);
    }

    /// Check all plugins for a fault. Returns the first fault found, or Pass.
    pub fn check_all(
        &self,
        rng: &mut DetRng,
        seed: u64,
        operation: &str,
        metadata: &std::collections::HashMap<String, String>,
    ) -> FaultCheck {
        for plugin in &self.plugins {
            let mut ctx = PluginContext {
                rng,
                seed,
                metadata,
            };
            match plugin.check_fault(&mut ctx, operation) {
                FaultCheck::Pass => continue,
                fault => return fault,
            }
        }
        FaultCheck::Pass
    }

    /// Shutdown all plugins.
    pub fn shutdown(&self) {
        for plugin in &self.plugins {
            plugin.on_shutdown();
        }
    }

    /// Number of registered plugins.
    pub fn len(&self) -> usize {
        self.plugins.len()
    }

    /// Whether the registry is empty.
    pub fn is_empty(&self) -> bool {
        self.plugins.is_empty()
    }
}

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

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

    struct TestPlugin {
        probability: f64,
    }

    impl VortexPlugin for TestPlugin {
        fn name(&self) -> &str {
            "test"
        }

        fn check_fault(&self, ctx: &mut PluginContext<'_>, _operation: &str) -> FaultCheck {
            if ctx.rng.chance(self.probability) {
                FaultCheck::Inject {
                    message: "test fault".into(),
                }
            } else {
                FaultCheck::Pass
            }
        }
    }

    #[test]
    fn test_plugin_registry() {
        let mut registry = PluginRegistry::new();
        assert!(registry.is_empty());

        registry.register(Box::new(TestPlugin { probability: 1.0 }));
        assert_eq!(registry.len(), 1);

        let mut rng = DetRng::new(42);
        let metadata = std::collections::HashMap::new();
        let result = registry.check_all(&mut rng, 42, "test:op", &metadata);
        assert!(matches!(result, FaultCheck::Inject { .. }));
    }

    #[test]
    fn test_plugin_pass() {
        let mut registry = PluginRegistry::new();
        registry.register(Box::new(TestPlugin { probability: 0.0 }));

        let mut rng = DetRng::new(42);
        let metadata = std::collections::HashMap::new();
        let result = registry.check_all(&mut rng, 42, "test:op", &metadata);
        assert!(matches!(result, FaultCheck::Pass));
    }

    #[test]
    fn test_multiple_plugins() {
        let mut registry = PluginRegistry::new();
        registry.register(Box::new(TestPlugin { probability: 0.0 })); // always pass
        registry.register(Box::new(TestPlugin { probability: 1.0 })); // always inject

        let mut rng = DetRng::new(42);
        let metadata = std::collections::HashMap::new();
        let result = registry.check_all(&mut rng, 42, "test:op", &metadata);
        // First plugin passes, second injects
        assert!(matches!(result, FaultCheck::Inject { .. }));
    }
}