Skip to main content

memlink_runtime/
version.rs

1//! Module versioning and side-by-side execution.
2//!
3//! Supports loading multiple versions of the same module
4//! and gradual migration between versions.
5
6use std::sync::atomic::{AtomicU32, Ordering};
7use std::sync::Arc;
8
9use dashmap::DashMap;
10
11/// Semantic version representation.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub struct ModuleVersion {
14    /// Major version (breaking changes).
15    pub major: u32,
16    /// Minor version (new features).
17    pub minor: u32,
18    /// Patch version (bug fixes).
19    pub patch: u32,
20}
21
22impl ModuleVersion {
23    /// Creates a new version.
24    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
25        Self { major, minor, patch }
26    }
27
28    /// Parses a version string (e.g., "1.2.3").
29    pub fn parse(s: &str) -> Option<Self> {
30        let parts: Vec<&str> = s.split('.').collect();
31        if parts.len() != 3 {
32            return None;
33        }
34
35        Some(Self {
36            major: parts[0].parse().ok()?,
37            minor: parts[1].parse().ok()?,
38            patch: parts[2].parse().ok()?,
39        })
40    }
41
42    /// Returns the version as a string.
43    pub fn as_str(&self) -> String {
44        format!("{}.{}.{}", self.major, self.minor, self.patch)
45    }
46
47    /// Checks if this version is compatible with another.
48    /// Compatible means same major version.
49    pub fn is_compatible_with(&self, other: &ModuleVersion) -> bool {
50        self.major == other.major
51    }
52
53    /// Checks if this version is greater than another.
54    pub fn is_greater_than(&self, other: &ModuleVersion) -> bool {
55        if self.major != other.major {
56            return self.major > other.major;
57        }
58        if self.minor != other.minor {
59            return self.minor > other.minor;
60        }
61        self.patch > other.patch
62    }
63}
64
65impl std::fmt::Display for ModuleVersion {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
68    }
69}
70
71/// Versioned module instance.
72#[derive(Debug, Clone)]
73pub struct VersionedModule {
74    /// Module name.
75    pub name: String,
76    /// Module version.
77    pub version: ModuleVersion,
78    /// Module path.
79    pub path: String,
80    /// Whether this is the active version.
81    pub is_active: bool,
82}
83
84/// Traffic routing configuration for gradual migration.
85#[derive(Debug, Clone)]
86pub struct TrafficRouting {
87    /// Percentage of traffic to new version (0-100).
88    pub new_version_percentage: u32,
89    /// Minimum requests before evaluating.
90    pub min_requests: u32,
91    /// Error rate threshold for rollback (0.0-1.0).
92    pub error_threshold: f32,
93}
94
95impl Default for TrafficRouting {
96    fn default() -> Self {
97        Self {
98            new_version_percentage: 0,
99            min_requests: 100,
100            error_threshold: 0.01, // 1%
101        }
102    }
103}
104
105/// Version manager for a module.
106#[derive(Debug)]
107pub struct VersionManager {
108    /// Module name.
109    module_name: String,
110    /// Loaded versions.
111    versions: DashMap<ModuleVersion, VersionedModule>,
112    /// Active version.
113    active_version: std::sync::Mutex<Option<ModuleVersion>>,
114    /// Traffic routing config.
115    routing: std::sync::Mutex<TrafficRouting>,
116    /// Requests to new version.
117    new_version_requests: AtomicU32,
118    /// Errors from new version.
119    new_version_errors: AtomicU32,
120}
121
122impl VersionManager {
123    /// Creates a new version manager.
124    pub fn new(module_name: String) -> Self {
125        Self {
126            module_name,
127            versions: DashMap::new(),
128            active_version: std::sync::Mutex::new(None),
129            routing: std::sync::Mutex::new(TrafficRouting::default()),
130            new_version_requests: AtomicU32::new(0),
131            new_version_errors: AtomicU32::new(0),
132        }
133    }
134
135    /// Registers a new version.
136    pub fn register_version(&self, version: ModuleVersion, path: String) {
137        let module = VersionedModule {
138            name: self.module_name.clone(),
139            version: version.clone(),
140            path,
141            is_active: false,
142        };
143        self.versions.insert(version, module);
144    }
145
146    /// Activates a version.
147    pub fn activate_version(&self, version: &ModuleVersion) -> bool {
148        if !self.versions.contains_key(version) {
149            return false;
150        }
151
152        // Deactivate current active version
153        for entry in self.versions.iter() {
154            let mut module = entry.value().clone();
155            module.is_active = false;
156            // Note: Can't mutate through DashMap iter, would need different approach
157        }
158
159        // Activate new version - simplified for now
160        *self.active_version.lock().unwrap() = Some(version.clone());
161        true
162    }
163
164    /// Selects a version for a request (for gradual migration).
165    pub fn select_version(&self) -> Option<ModuleVersion> {
166        let routing = self.routing.lock().unwrap();
167
168        // If no traffic to new version, use active
169        if routing.new_version_percentage == 0 {
170            return self.active_version.lock().unwrap().clone();
171        }
172
173        // Determine if this request goes to new version
174        use std::collections::hash_map::RandomState;
175        use std::hash::{BuildHasher, Hasher};
176
177        let hasher = RandomState::new().build_hasher();
178        let roll = hasher.finish() % 100;
179
180        if roll < routing.new_version_percentage as u64 {
181            // Route to new version (highest version)
182            self.versions
183                .iter()
184                .max_by(|a, b| a.key().cmp(b.key()))
185                .map(|e| e.key().clone())
186        } else {
187            // Route to active version
188            self.active_version.lock().unwrap().clone()
189        }
190    }
191
192    /// Records a request to the new version.
193    pub fn record_request(&self) {
194        self.new_version_requests.fetch_add(1, Ordering::Relaxed);
195    }
196
197    /// Records an error from the new version.
198    pub fn record_error(&self) {
199        self.new_version_errors.fetch_add(1, Ordering::Relaxed);
200    }
201
202    /// Checks if migration should continue or rollback.
203    pub fn should_rollback(&self) -> bool {
204        let routing = self.routing.lock().unwrap();
205        let requests = self.new_version_requests.load(Ordering::Relaxed);
206
207        if requests < routing.min_requests {
208            return false;
209        }
210
211        let errors = self.new_version_errors.load(Ordering::Relaxed);
212        let error_rate = errors as f32 / requests as f32;
213
214        error_rate > routing.error_threshold
215    }
216
217    /// Returns all loaded versions.
218    pub fn all_versions(&self) -> Vec<VersionedModule> {
219        self.versions.iter().map(|e| e.value().clone()).collect()
220    }
221
222    /// Returns the active version.
223    pub fn active_version(&self) -> Option<ModuleVersion> {
224        self.active_version.lock().unwrap().clone()
225    }
226
227    /// Sets traffic routing configuration.
228    pub fn set_routing(&self, routing: TrafficRouting) {
229        *self.routing.lock().unwrap() = routing;
230    }
231}
232
233/// Version registry for all modules.
234#[derive(Debug)]
235pub struct VersionRegistry {
236    /// Version managers by module name.
237    managers: DashMap<String, Arc<VersionManager>>,
238}
239
240impl VersionRegistry {
241    /// Creates a new version registry.
242    pub fn new() -> Self {
243        Self {
244            managers: DashMap::new(),
245        }
246    }
247
248    /// Gets or creates a version manager.
249    pub fn get_or_create(&self, module_name: &str) -> Arc<VersionManager> {
250        self.managers
251            .entry(module_name.to_string())
252            .or_insert_with(|| Arc::new(VersionManager::new(module_name.to_string())))
253            .clone()
254    }
255
256    /// Returns the number of managed modules.
257    pub fn module_count(&self) -> usize {
258        self.managers.len()
259    }
260}
261
262impl Default for VersionRegistry {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_version_parsing() {
274        let v = ModuleVersion::parse("1.2.3").unwrap();
275        assert_eq!(v.major, 1);
276        assert_eq!(v.minor, 2);
277        assert_eq!(v.patch, 3);
278    }
279
280    #[test]
281    fn test_version_comparison() {
282        let v1 = ModuleVersion::new(1, 0, 0);
283        let v2 = ModuleVersion::new(1, 0, 1);
284        let v3 = ModuleVersion::new(2, 0, 0);
285
286        assert!(v2.is_greater_than(&v1));
287        assert!(v3.is_greater_than(&v2));
288        assert!(v1.is_compatible_with(&v2));
289        assert!(!v1.is_compatible_with(&v3));
290    }
291
292    #[test]
293    fn test_version_manager() {
294        let manager = VersionManager::new("test".to_string());
295
296        manager.register_version(ModuleVersion::new(1, 0, 0), "/path/v1".to_string());
297        manager.register_version(ModuleVersion::new(2, 0, 0), "/path/v2".to_string());
298
299        manager.activate_version(&ModuleVersion::new(1, 0, 0));
300
301        assert_eq!(manager.active_version(), Some(ModuleVersion::new(1, 0, 0)));
302        assert_eq!(manager.all_versions().len(), 2);
303    }
304}