1use std::sync::atomic::{AtomicU32, Ordering};
7use std::sync::Arc;
8
9use dashmap::DashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub struct ModuleVersion {
14 pub major: u32,
16 pub minor: u32,
18 pub patch: u32,
20}
21
22impl ModuleVersion {
23 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
25 Self { major, minor, patch }
26 }
27
28 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 pub fn as_str(&self) -> String {
44 format!("{}.{}.{}", self.major, self.minor, self.patch)
45 }
46
47 pub fn is_compatible_with(&self, other: &ModuleVersion) -> bool {
50 self.major == other.major
51 }
52
53 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#[derive(Debug, Clone)]
73pub struct VersionedModule {
74 pub name: String,
76 pub version: ModuleVersion,
78 pub path: String,
80 pub is_active: bool,
82}
83
84#[derive(Debug, Clone)]
86pub struct TrafficRouting {
87 pub new_version_percentage: u32,
89 pub min_requests: u32,
91 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, }
102 }
103}
104
105#[derive(Debug)]
107pub struct VersionManager {
108 module_name: String,
110 versions: DashMap<ModuleVersion, VersionedModule>,
112 active_version: std::sync::Mutex<Option<ModuleVersion>>,
114 routing: std::sync::Mutex<TrafficRouting>,
116 new_version_requests: AtomicU32,
118 new_version_errors: AtomicU32,
120}
121
122impl VersionManager {
123 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 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 pub fn activate_version(&self, version: &ModuleVersion) -> bool {
148 if !self.versions.contains_key(version) {
149 return false;
150 }
151
152 for entry in self.versions.iter() {
154 let mut module = entry.value().clone();
155 module.is_active = false;
156 }
158
159 *self.active_version.lock().unwrap() = Some(version.clone());
161 true
162 }
163
164 pub fn select_version(&self) -> Option<ModuleVersion> {
166 let routing = self.routing.lock().unwrap();
167
168 if routing.new_version_percentage == 0 {
170 return self.active_version.lock().unwrap().clone();
171 }
172
173 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 self.versions
183 .iter()
184 .max_by(|a, b| a.key().cmp(b.key()))
185 .map(|e| e.key().clone())
186 } else {
187 self.active_version.lock().unwrap().clone()
189 }
190 }
191
192 pub fn record_request(&self) {
194 self.new_version_requests.fetch_add(1, Ordering::Relaxed);
195 }
196
197 pub fn record_error(&self) {
199 self.new_version_errors.fetch_add(1, Ordering::Relaxed);
200 }
201
202 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 pub fn all_versions(&self) -> Vec<VersionedModule> {
219 self.versions.iter().map(|e| e.value().clone()).collect()
220 }
221
222 pub fn active_version(&self) -> Option<ModuleVersion> {
224 self.active_version.lock().unwrap().clone()
225 }
226
227 pub fn set_routing(&self, routing: TrafficRouting) {
229 *self.routing.lock().unwrap() = routing;
230 }
231}
232
233#[derive(Debug)]
235pub struct VersionRegistry {
236 managers: DashMap<String, Arc<VersionManager>>,
238}
239
240impl VersionRegistry {
241 pub fn new() -> Self {
243 Self {
244 managers: DashMap::new(),
245 }
246 }
247
248 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 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}