mod deployment;
mod integration;
mod lifecycle;
mod metadata;
mod registry;
mod storage;
pub use deployment::{
ActiveDeployment, DeploymentConfig, DeploymentEvent, DeploymentEventType, DeploymentManager,
DeploymentStatistics, DeploymentStatus, DeploymentStrategy, Environment, HealthStatus,
};
pub use integration::{
ModelRoutingResult, PromotionResult, VersionExperimentConfig, VersionExperimentResult,
VersionMetricType, VersionedABTestManager, VersionedExperiment, VersionedExperimentStatus,
};
pub use lifecycle::{
LifecycleEvent, LifecyclePolicies, LifecycleStatistics, VersionLifecycle, VersionStatus,
VersionTransition,
};
pub use metadata::{ModelMetadata, ModelSource, ModelTag, VersionedModel};
pub use registry::{
DateRange, ModelRegistry, RegistryStatistics, SortBy, SortOrder, TagMatchMode, VersionFilter,
VersionQuery,
};
pub use storage::{Artifact, ArtifactType, FileSystemStorage, InMemoryStorage, ModelStorage};
use anyhow::Result;
use std::sync::Arc;
use uuid::Uuid;
pub struct ModelVersionManager {
registry: Arc<ModelRegistry>,
storage: Arc<dyn ModelStorage>,
deployment_manager: Arc<DeploymentManager>,
lifecycle: Arc<VersionLifecycle>,
}
impl ModelVersionManager {
pub fn new(storage: Arc<dyn ModelStorage>) -> Self {
Self {
registry: Arc::new(ModelRegistry::new()),
storage: storage.clone(),
deployment_manager: Arc::new(DeploymentManager::new(storage)),
lifecycle: Arc::new(VersionLifecycle::new()),
}
}
pub async fn register_version(
&self,
model_name: &str,
version: &str,
metadata: ModelMetadata,
artifacts: Vec<Artifact>,
) -> Result<Uuid> {
let artifact_ids = self.storage.store_artifacts(&artifacts).await?;
let versioned_model = VersionedModel::new(
model_name.to_string(),
version.to_string(),
metadata,
artifact_ids,
);
let version_id = self.registry.register(versioned_model).await?;
self.lifecycle.initialize_version(version_id).await?;
tracing::info!(
"Registered model version: {}:{} ({})",
model_name,
version,
version_id
);
Ok(version_id)
}
pub async fn get_version(&self, version_id: Uuid) -> Result<Option<VersionedModel>> {
self.registry.get_version(version_id).await
}
pub async fn get_version_by_name(
&self,
model_name: &str,
version: &str,
) -> Result<Option<VersionedModel>> {
self.registry.get_version_by_name(model_name, version).await
}
pub async fn list_versions(&self, model_name: &str) -> Result<Vec<VersionedModel>> {
self.registry.list_versions(model_name).await
}
pub async fn query_versions(&self, query: VersionQuery) -> Result<Vec<VersionedModel>> {
self.registry.query_versions(query).await
}
pub async fn promote_to_production(&self, version_id: Uuid) -> Result<()> {
let current_status = self.lifecycle.get_status(version_id).await?;
if current_status != VersionStatus::Staging {
anyhow::bail!("Can only promote versions from staging to production");
}
self.lifecycle.transition(version_id, VersionTransition::Promote).await?;
let version = self
.registry
.get_version(version_id)
.await?
.ok_or_else(|| anyhow::anyhow!("Version not found"))?;
self.deployment_manager.deploy_to_production(version_id, &version).await?;
tracing::info!("Promoted version {} to production", version_id);
Ok(())
}
pub async fn rollback_to_version(&self, model_name: &str, target_version: &str) -> Result<()> {
let version =
self.get_version_by_name(model_name, target_version).await?.ok_or_else(|| {
anyhow::anyhow!("Version not found: {}:{}", model_name, target_version)
})?;
let status = self.lifecycle.get_status(version.id()).await?;
if status != VersionStatus::Production && status != VersionStatus::Staging {
anyhow::bail!("Can only rollback to production or staging versions");
}
self.deployment_manager.rollback(model_name, version.id()).await?;
tracing::info!("Rolled back {} to version {}", model_name, target_version);
Ok(())
}
pub async fn archive_version(&self, version_id: Uuid) -> Result<()> {
let status = self.lifecycle.get_status(version_id).await?;
if status == VersionStatus::Production {
anyhow::bail!("Cannot archive production version");
}
self.lifecycle.transition(version_id, VersionTransition::Archive).await?;
self.storage.archive_version(version_id).await?;
tracing::info!("Archived version {}", version_id);
Ok(())
}
pub async fn delete_version(&self, version_id: Uuid) -> Result<()> {
let status = self.lifecycle.get_status(version_id).await?;
if status == VersionStatus::Production {
anyhow::bail!("Cannot delete production version");
}
self.storage.delete_version(version_id).await?;
self.registry.remove_version(version_id).await?;
self.lifecycle.cleanup_version(version_id).await?;
tracing::info!("Deleted version {}", version_id);
Ok(())
}
pub async fn get_version_stats(&self, model_name: &str) -> Result<VersionStats> {
let versions = self.list_versions(model_name).await?;
let mut stats = VersionStats {
model_name: model_name.to_string(),
total_versions: versions.len(),
production_versions: 0,
staging_versions: 0,
development_versions: 0,
archived_versions: 0,
latest_version: None,
oldest_version: None,
};
if !versions.is_empty() {
stats.latest_version = versions
.iter()
.max_by_key(|v| v.metadata().created_at)
.map(|v| v.version().to_string());
stats.oldest_version = versions
.iter()
.min_by_key(|v| v.metadata().created_at)
.map(|v| v.version().to_string());
for version in &versions {
let status = self.lifecycle.get_status(version.id()).await?;
match status {
VersionStatus::Production => stats.production_versions += 1,
VersionStatus::Staging => stats.staging_versions += 1,
VersionStatus::Development => stats.development_versions += 1,
VersionStatus::Archived => stats.archived_versions += 1,
_ => {},
}
}
}
Ok(stats)
}
pub fn registry(&self) -> Arc<ModelRegistry> {
self.registry.clone()
}
pub fn storage(&self) -> Arc<dyn ModelStorage> {
self.storage.clone()
}
pub fn deployment_manager(&self) -> Arc<DeploymentManager> {
self.deployment_manager.clone()
}
pub fn lifecycle(&self) -> Arc<VersionLifecycle> {
self.lifecycle.clone()
}
}
#[derive(Debug, Clone)]
pub struct VersionStats {
pub model_name: String,
pub total_versions: usize,
pub production_versions: usize,
pub staging_versions: usize,
pub development_versions: usize,
pub archived_versions: usize,
pub latest_version: Option<String>,
pub oldest_version: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
struct MockStorage;
#[async_trait::async_trait]
impl ModelStorage for MockStorage {
async fn store_artifacts(&self, _artifacts: &[Artifact]) -> Result<Vec<Uuid>> {
Ok(vec![Uuid::new_v4()])
}
async fn get_artifact(&self, _artifact_id: Uuid) -> Result<Option<Artifact>> {
Ok(None)
}
async fn delete_artifacts(&self, _artifact_ids: &[Uuid]) -> Result<()> {
Ok(())
}
async fn archive_version(&self, _version_id: Uuid) -> Result<()> {
Ok(())
}
async fn delete_version(&self, _version_id: Uuid) -> Result<()> {
Ok(())
}
async fn list_artifacts(&self, _version_id: Uuid) -> Result<Vec<Artifact>> {
Ok(vec![])
}
}
#[tokio::test]
async fn test_version_registration() {
let storage = Arc::new(MockStorage);
let manager = ModelVersionManager::new(storage);
let metadata = ModelMetadata::builder()
.description("Test model".to_string())
.created_by("test_user".to_string())
.model_type("transformer".to_string())
.build();
let artifacts = vec![Artifact::new(
ArtifactType::Model,
PathBuf::from("model.bin"),
vec![1, 2, 3],
)];
let version_id = manager
.register_version("test_model", "1.0.0", metadata, artifacts)
.await
.expect("operation failed in test");
assert!(!version_id.is_nil());
let retrieved = manager.get_version(version_id).await.expect("async operation failed");
assert!(retrieved.is_some());
assert_eq!(
retrieved.expect("operation failed in test").version(),
"1.0.0"
);
}
#[tokio::test]
async fn test_version_lifecycle() {
let storage = Arc::new(MockStorage);
let manager = ModelVersionManager::new(storage);
let metadata = ModelMetadata::builder()
.description("Test model".to_string())
.created_by("test_user".to_string())
.model_type("transformer".to_string())
.build();
let version_id = manager
.register_version("test_model", "1.0.0", metadata, vec![])
.await
.expect("async operation failed");
let status =
manager.lifecycle.get_status(version_id).await.expect("async operation failed");
assert_eq!(status, VersionStatus::Development);
manager
.lifecycle
.transition(version_id, VersionTransition::ToStaging)
.await
.expect("operation failed in test");
let status =
manager.lifecycle.get_status(version_id).await.expect("async operation failed");
assert_eq!(status, VersionStatus::Staging);
}
}