Skip to main content

vortex_core/
plugin.rs

1//! Plugin trait for custom fault injection modules.
2//!
3//! Implement [`VortexPlugin`] to add custom fault types that integrate
4//! with all Vortex layers. Plugins are registered at runtime and evaluated
5//! alongside the built-in fault rules.
6//!
7//! # Example
8//! ```
9//! use vortex_core::plugin::{VortexPlugin, FaultCheck, PluginContext};
10//! use vortex_core::DetRng;
11//!
12//! struct GrpcPlugin;
13//!
14//! impl VortexPlugin for GrpcPlugin {
15//!     fn name(&self) -> &str { "grpc" }
16//!
17//!     fn check_fault(&self, ctx: &mut PluginContext, operation: &str) -> FaultCheck {
18//!         if operation.starts_with("grpc:") && ctx.rng.chance(0.1) {
19//!             FaultCheck::Inject { message: "gRPC unavailable".into() }
20//!         } else {
21//!             FaultCheck::Pass
22//!         }
23//!     }
24//! }
25//! ```
26
27use crate::DetRng;
28
29/// Result of a plugin fault check.
30#[derive(Debug, Clone)]
31pub enum FaultCheck {
32    /// No fault — operation proceeds normally.
33    Pass,
34    /// Inject a fault with the given error message.
35    Inject { message: String },
36    /// Inject a delay (microseconds) before the operation.
37    Delay { us: u64 },
38}
39
40/// Context passed to plugins during fault evaluation.
41pub struct PluginContext<'a> {
42    /// Deterministic RNG for probability-based decisions.
43    pub rng: &'a mut DetRng,
44    /// The current seed.
45    pub seed: u64,
46    /// Arbitrary key-value metadata (e.g., path, address, fd).
47    pub metadata: &'a std::collections::HashMap<String, String>,
48}
49
50/// Trait for custom fault injection plugins.
51///
52/// Implement this to add custom fault types (e.g., gRPC errors, DNS failures,
53/// TLS handshake faults) that integrate with Vortex's simulation layers.
54pub trait VortexPlugin: Send + Sync {
55    /// Plugin name (used for logging and configuration).
56    fn name(&self) -> &str;
57
58    /// Check if a fault should be injected for the given operation.
59    ///
60    /// Called by the simulation engine before each intercepted operation.
61    /// The `operation` string describes what's happening (e.g., "fs:write:/data.wal",
62    /// "net:connect:127.0.0.1:5432", "grpc:call:UserService/GetUser").
63    fn check_fault(&self, ctx: &mut PluginContext<'_>, operation: &str) -> FaultCheck;
64
65    /// Called once when the plugin is registered. Use for setup.
66    fn on_register(&self) {}
67
68    /// Called once when the simulation ends. Use for cleanup/reporting.
69    fn on_shutdown(&self) {}
70}
71
72/// Registry of active plugins.
73pub struct PluginRegistry {
74    plugins: Vec<Box<dyn VortexPlugin>>,
75}
76
77impl PluginRegistry {
78    /// Create an empty registry.
79    pub fn new() -> Self {
80        Self {
81            plugins: Vec::new(),
82        }
83    }
84
85    /// Register a plugin.
86    pub fn register(&mut self, plugin: Box<dyn VortexPlugin>) {
87        plugin.on_register();
88        self.plugins.push(plugin);
89    }
90
91    /// Check all plugins for a fault. Returns the first fault found, or Pass.
92    pub fn check_all(
93        &self,
94        rng: &mut DetRng,
95        seed: u64,
96        operation: &str,
97        metadata: &std::collections::HashMap<String, String>,
98    ) -> FaultCheck {
99        for plugin in &self.plugins {
100            let mut ctx = PluginContext {
101                rng,
102                seed,
103                metadata,
104            };
105            match plugin.check_fault(&mut ctx, operation) {
106                FaultCheck::Pass => continue,
107                fault => return fault,
108            }
109        }
110        FaultCheck::Pass
111    }
112
113    /// Shutdown all plugins.
114    pub fn shutdown(&self) {
115        for plugin in &self.plugins {
116            plugin.on_shutdown();
117        }
118    }
119
120    /// Number of registered plugins.
121    pub fn len(&self) -> usize {
122        self.plugins.len()
123    }
124
125    /// Whether the registry is empty.
126    pub fn is_empty(&self) -> bool {
127        self.plugins.is_empty()
128    }
129}
130
131impl Default for PluginRegistry {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    struct TestPlugin {
142        probability: f64,
143    }
144
145    impl VortexPlugin for TestPlugin {
146        fn name(&self) -> &str {
147            "test"
148        }
149
150        fn check_fault(&self, ctx: &mut PluginContext<'_>, _operation: &str) -> FaultCheck {
151            if ctx.rng.chance(self.probability) {
152                FaultCheck::Inject {
153                    message: "test fault".into(),
154                }
155            } else {
156                FaultCheck::Pass
157            }
158        }
159    }
160
161    #[test]
162    fn test_plugin_registry() {
163        let mut registry = PluginRegistry::new();
164        assert!(registry.is_empty());
165
166        registry.register(Box::new(TestPlugin { probability: 1.0 }));
167        assert_eq!(registry.len(), 1);
168
169        let mut rng = DetRng::new(42);
170        let metadata = std::collections::HashMap::new();
171        let result = registry.check_all(&mut rng, 42, "test:op", &metadata);
172        assert!(matches!(result, FaultCheck::Inject { .. }));
173    }
174
175    #[test]
176    fn test_plugin_pass() {
177        let mut registry = PluginRegistry::new();
178        registry.register(Box::new(TestPlugin { probability: 0.0 }));
179
180        let mut rng = DetRng::new(42);
181        let metadata = std::collections::HashMap::new();
182        let result = registry.check_all(&mut rng, 42, "test:op", &metadata);
183        assert!(matches!(result, FaultCheck::Pass));
184    }
185
186    #[test]
187    fn test_multiple_plugins() {
188        let mut registry = PluginRegistry::new();
189        registry.register(Box::new(TestPlugin { probability: 0.0 })); // always pass
190        registry.register(Box::new(TestPlugin { probability: 1.0 })); // always inject
191
192        let mut rng = DetRng::new(42);
193        let metadata = std::collections::HashMap::new();
194        let result = registry.check_all(&mut rng, 42, "test:op", &metadata);
195        // First plugin passes, second injects
196        assert!(matches!(result, FaultCheck::Inject { .. }));
197    }
198}