trustformers_core/versioning/
mod.rs1mod deployment;
11mod integration;
12mod lifecycle;
13mod metadata;
14mod registry;
15mod storage;
16
17pub use deployment::{
18 ActiveDeployment, DeploymentConfig, DeploymentEvent, DeploymentEventType, DeploymentManager,
19 DeploymentStatistics, DeploymentStatus, DeploymentStrategy, Environment, HealthStatus,
20};
21pub use integration::{
22 ModelRoutingResult, PromotionResult, VersionExperimentConfig, VersionExperimentResult,
23 VersionMetricType, VersionedABTestManager, VersionedExperiment, VersionedExperimentStatus,
24};
25pub use lifecycle::{
26 LifecycleEvent, LifecyclePolicies, LifecycleStatistics, VersionLifecycle, VersionStatus,
27 VersionTransition,
28};
29pub use metadata::{ModelMetadata, ModelSource, ModelTag, VersionedModel};
30pub use registry::{
31 DateRange, ModelRegistry, RegistryStatistics, SortBy, SortOrder, TagMatchMode, VersionFilter,
32 VersionQuery,
33};
34pub use storage::{Artifact, ArtifactType, FileSystemStorage, InMemoryStorage, ModelStorage};
35
36use anyhow::Result;
37use std::sync::Arc;
38use uuid::Uuid;
39
40pub struct ModelVersionManager {
42 registry: Arc<ModelRegistry>,
43 storage: Arc<dyn ModelStorage>,
44 deployment_manager: Arc<DeploymentManager>,
45 lifecycle: Arc<VersionLifecycle>,
46}
47
48impl ModelVersionManager {
49 pub fn new(storage: Arc<dyn ModelStorage>) -> Self {
51 Self {
52 registry: Arc::new(ModelRegistry::new()),
53 storage: storage.clone(),
54 deployment_manager: Arc::new(DeploymentManager::new(storage)),
55 lifecycle: Arc::new(VersionLifecycle::new()),
56 }
57 }
58
59 pub async fn register_version(
61 &self,
62 model_name: &str,
63 version: &str,
64 metadata: ModelMetadata,
65 artifacts: Vec<Artifact>,
66 ) -> Result<Uuid> {
67 let artifact_ids = self.storage.store_artifacts(&artifacts).await?;
69
70 let versioned_model = VersionedModel::new(
72 model_name.to_string(),
73 version.to_string(),
74 metadata,
75 artifact_ids,
76 );
77
78 let version_id = self.registry.register(versioned_model).await?;
80
81 self.lifecycle.initialize_version(version_id).await?;
83
84 tracing::info!(
85 "Registered model version: {}:{} ({})",
86 model_name,
87 version,
88 version_id
89 );
90 Ok(version_id)
91 }
92
93 pub async fn get_version(&self, version_id: Uuid) -> Result<Option<VersionedModel>> {
95 self.registry.get_version(version_id).await
96 }
97
98 pub async fn get_version_by_name(
100 &self,
101 model_name: &str,
102 version: &str,
103 ) -> Result<Option<VersionedModel>> {
104 self.registry.get_version_by_name(model_name, version).await
105 }
106
107 pub async fn list_versions(&self, model_name: &str) -> Result<Vec<VersionedModel>> {
109 self.registry.list_versions(model_name).await
110 }
111
112 pub async fn query_versions(&self, query: VersionQuery) -> Result<Vec<VersionedModel>> {
114 self.registry.query_versions(query).await
115 }
116
117 pub async fn promote_to_production(&self, version_id: Uuid) -> Result<()> {
119 let current_status = self.lifecycle.get_status(version_id).await?;
121 if current_status != VersionStatus::Staging {
122 anyhow::bail!("Can only promote versions from staging to production");
123 }
124
125 self.lifecycle.transition(version_id, VersionTransition::Promote).await?;
127
128 let version = self
130 .registry
131 .get_version(version_id)
132 .await?
133 .ok_or_else(|| anyhow::anyhow!("Version not found"))?;
134
135 self.deployment_manager.deploy_to_production(version_id, &version).await?;
136
137 tracing::info!("Promoted version {} to production", version_id);
138 Ok(())
139 }
140
141 pub async fn rollback_to_version(&self, model_name: &str, target_version: &str) -> Result<()> {
143 let version =
144 self.get_version_by_name(model_name, target_version).await?.ok_or_else(|| {
145 anyhow::anyhow!("Version not found: {}:{}", model_name, target_version)
146 })?;
147
148 let status = self.lifecycle.get_status(version.id()).await?;
150 if status != VersionStatus::Production && status != VersionStatus::Staging {
151 anyhow::bail!("Can only rollback to production or staging versions");
152 }
153
154 self.deployment_manager.rollback(model_name, version.id()).await?;
156
157 tracing::info!("Rolled back {} to version {}", model_name, target_version);
158 Ok(())
159 }
160
161 pub async fn archive_version(&self, version_id: Uuid) -> Result<()> {
163 let status = self.lifecycle.get_status(version_id).await?;
165 if status == VersionStatus::Production {
166 anyhow::bail!("Cannot archive production version");
167 }
168
169 self.lifecycle.transition(version_id, VersionTransition::Archive).await?;
171
172 self.storage.archive_version(version_id).await?;
174
175 tracing::info!("Archived version {}", version_id);
176 Ok(())
177 }
178
179 pub async fn delete_version(&self, version_id: Uuid) -> Result<()> {
181 let status = self.lifecycle.get_status(version_id).await?;
183 if status == VersionStatus::Production {
184 anyhow::bail!("Cannot delete production version");
185 }
186
187 self.storage.delete_version(version_id).await?;
189
190 self.registry.remove_version(version_id).await?;
192
193 self.lifecycle.cleanup_version(version_id).await?;
195
196 tracing::info!("Deleted version {}", version_id);
197 Ok(())
198 }
199
200 pub async fn get_version_stats(&self, model_name: &str) -> Result<VersionStats> {
202 let versions = self.list_versions(model_name).await?;
203
204 let mut stats = VersionStats {
205 model_name: model_name.to_string(),
206 total_versions: versions.len(),
207 production_versions: 0,
208 staging_versions: 0,
209 development_versions: 0,
210 archived_versions: 0,
211 latest_version: None,
212 oldest_version: None,
213 };
214
215 if !versions.is_empty() {
216 stats.latest_version = versions
218 .iter()
219 .max_by_key(|v| v.metadata().created_at)
220 .map(|v| v.version().to_string());
221
222 stats.oldest_version = versions
223 .iter()
224 .min_by_key(|v| v.metadata().created_at)
225 .map(|v| v.version().to_string());
226
227 for version in &versions {
229 let status = self.lifecycle.get_status(version.id()).await?;
230 match status {
231 VersionStatus::Production => stats.production_versions += 1,
232 VersionStatus::Staging => stats.staging_versions += 1,
233 VersionStatus::Development => stats.development_versions += 1,
234 VersionStatus::Archived => stats.archived_versions += 1,
235 _ => {},
236 }
237 }
238 }
239
240 Ok(stats)
241 }
242
243 pub fn registry(&self) -> Arc<ModelRegistry> {
245 self.registry.clone()
246 }
247
248 pub fn storage(&self) -> Arc<dyn ModelStorage> {
250 self.storage.clone()
251 }
252
253 pub fn deployment_manager(&self) -> Arc<DeploymentManager> {
255 self.deployment_manager.clone()
256 }
257
258 pub fn lifecycle(&self) -> Arc<VersionLifecycle> {
260 self.lifecycle.clone()
261 }
262}
263
264#[derive(Debug, Clone)]
266pub struct VersionStats {
267 pub model_name: String,
268 pub total_versions: usize,
269 pub production_versions: usize,
270 pub staging_versions: usize,
271 pub development_versions: usize,
272 pub archived_versions: usize,
273 pub latest_version: Option<String>,
274 pub oldest_version: Option<String>,
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use std::path::PathBuf;
281
282 struct MockStorage;
284
285 #[async_trait::async_trait]
286 impl ModelStorage for MockStorage {
287 async fn store_artifacts(&self, _artifacts: &[Artifact]) -> Result<Vec<Uuid>> {
288 Ok(vec![Uuid::new_v4()])
289 }
290
291 async fn get_artifact(&self, _artifact_id: Uuid) -> Result<Option<Artifact>> {
292 Ok(None)
293 }
294
295 async fn delete_artifacts(&self, _artifact_ids: &[Uuid]) -> Result<()> {
296 Ok(())
297 }
298
299 async fn archive_version(&self, _version_id: Uuid) -> Result<()> {
300 Ok(())
301 }
302
303 async fn delete_version(&self, _version_id: Uuid) -> Result<()> {
304 Ok(())
305 }
306
307 async fn list_artifacts(&self, _version_id: Uuid) -> Result<Vec<Artifact>> {
308 Ok(vec![])
309 }
310 }
311
312 #[tokio::test]
313 async fn test_version_registration() {
314 let storage = Arc::new(MockStorage);
315 let manager = ModelVersionManager::new(storage);
316
317 let metadata = ModelMetadata::builder()
318 .description("Test model".to_string())
319 .created_by("test_user".to_string())
320 .model_type("transformer".to_string())
321 .build();
322
323 let artifacts = vec![Artifact::new(
324 ArtifactType::Model,
325 PathBuf::from("model.bin"),
326 vec![1, 2, 3],
327 )];
328
329 let version_id = manager
330 .register_version("test_model", "1.0.0", metadata, artifacts)
331 .await
332 .expect("operation failed in test");
333 assert!(!version_id.is_nil());
334
335 let retrieved = manager.get_version(version_id).await.expect("async operation failed");
336 assert!(retrieved.is_some());
337 assert_eq!(
338 retrieved.expect("operation failed in test").version(),
339 "1.0.0"
340 );
341 }
342
343 #[tokio::test]
344 async fn test_version_lifecycle() {
345 let storage = Arc::new(MockStorage);
346 let manager = ModelVersionManager::new(storage);
347
348 let metadata = ModelMetadata::builder()
349 .description("Test model".to_string())
350 .created_by("test_user".to_string())
351 .model_type("transformer".to_string())
352 .build();
353
354 let version_id = manager
355 .register_version("test_model", "1.0.0", metadata, vec![])
356 .await
357 .expect("async operation failed");
358
359 let status =
361 manager.lifecycle.get_status(version_id).await.expect("async operation failed");
362 assert_eq!(status, VersionStatus::Development);
363
364 manager
366 .lifecycle
367 .transition(version_id, VersionTransition::ToStaging)
368 .await
369 .expect("operation failed in test");
370 let status =
371 manager.lifecycle.get_status(version_id).await.expect("async operation failed");
372 assert_eq!(status, VersionStatus::Staging);
373 }
374}