///|
/// Explicit deployment state machine used by registry and serving layers.
pub(all) enum DeploymentState {
Draft
Staging
Canary
Active
Paused
RolledBack
Retired
} derive(Debug, Eq)
///|
pub fn deployment_state_catalog() -> Array[DeploymentState] {
[Draft, Staging, Canary, Active, Paused, RolledBack, Retired]
}
///|
pub struct Deployment {
model : String
version : String
mut state : DeploymentState
created_at : Int64
mut updated_at : Int64
mut traffic : Double
}
///|
pub fn Deployment::new(
model : String,
version : String,
timestamp : Int64,
) -> Deployment {
{
model,
version,
state: Draft,
created_at: timestamp,
updated_at: timestamp,
traffic: 0.0,
}
}
///|
pub fn Deployment::model(self : Deployment) -> String {
self.model
}
///|
pub fn Deployment::version(self : Deployment) -> String {
self.version
}
///|
pub fn Deployment::state(self : Deployment) -> DeploymentState {
self.state
}
///|
pub fn Deployment::traffic(self : Deployment) -> Double {
self.traffic
}
///|
pub fn Deployment::transition(
self : Deployment,
next : DeploymentState,
timestamp : Int64,
) -> Bool {
let allowed = match (self.state, next) {
(Draft, Staging) => true
(Staging, Canary) => true
(Canary, Active) => true
(Canary, Paused) => true
(Active, Paused) => true
(Active, RolledBack) => true
(Paused, Active) => true
(Paused, RolledBack) => true
(RolledBack, Draft) => true
(Active, Retired) => true
(Paused, Retired) => true
_ => false
}
if allowed {
self.state = next
self.updated_at = timestamp
if next != Canary {
self.traffic = if next == Active { 1.0 } else { 0.0 }
}
true
} else {
false
}
}
///|
pub fn Deployment::set_traffic(self : Deployment, share : Double) -> Bool {
if self.state == Canary || self.state == Active {
self.traffic = clamp(share, 0.0, 1.0)
true
} else {
false
}
}
///|
pub struct RollbackPolicy {
minimum_accuracy : Double
maximum_loss : Double
maximum_error_rate : Double
}
///|
pub fn RollbackPolicy::new(
minimum_accuracy? : Double = 0.5,
maximum_loss? : Double = 1.0,
maximum_error_rate? : Double = 0.5,
) -> RollbackPolicy {
{
minimum_accuracy: clamp(minimum_accuracy, 0.0, 1.0),
maximum_loss: if maximum_loss < 0.0 {
0.0
} else {
maximum_loss
},
maximum_error_rate: clamp(maximum_error_rate, 0.0, 1.0),
}
}
///|
pub fn RollbackPolicy::should_rollback(
self : RollbackPolicy,
accuracy : Double,
loss : Double,
error_rate : Double,
) -> Bool {
accuracy < self.minimum_accuracy ||
loss > self.maximum_loss ||
error_rate > self.maximum_error_rate
}
///|
pub struct DeploymentManager {
deployments : Map[String, Deployment]
policy : RollbackPolicy
mut transitions : Int
mut rollbacks : Int
}
///|
pub fn DeploymentManager::new(
policy? : RollbackPolicy = RollbackPolicy::new(),
) -> DeploymentManager {
{ deployments: {}, policy, transitions: 0, rollbacks: 0 }
}
///|
pub fn DeploymentManager::create(
self : DeploymentManager,
deployment : Deployment,
) -> Bool {
let key = "\{deployment.model()}@\{deployment.version()}"
if self.deployments.contains(key) {
false
} else {
self.deployments[key] = deployment
true
}
}
///|
pub fn DeploymentManager::transition(
self : DeploymentManager,
model : String,
version : String,
next : DeploymentState,
timestamp : Int64,
) -> Bool {
let key = "\{model}@\{version}"
match self.deployments.get(key) {
None => false
Some(deployment) => {
let changed = deployment.transition(next, timestamp)
if changed {
self.transitions += 1
if next == RolledBack {
self.rollbacks += 1
}
}
changed
}
}
}
///|
pub fn DeploymentManager::get(
self : DeploymentManager,
model : String,
version : String,
) -> Deployment? {
self.deployments.get("\{model}@\{version}")
}
///|
pub fn DeploymentManager::healthy(
self : DeploymentManager,
accuracy : Double,
loss : Double,
error_rate : Double,
) -> Bool {
!self.policy.should_rollback(accuracy, loss, error_rate)
}
///|
pub fn DeploymentManager::transitions(self : DeploymentManager) -> Int {
self.transitions
}
///|
pub fn DeploymentManager::rollbacks(self : DeploymentManager) -> Int {
self.rollbacks
}
///|
pub fn DeploymentManager::clear(self : DeploymentManager) -> Unit {
self.deployments.clear()
self.transitions = 0
self.rollbacks = 0
}
///|
pub struct CanaryExperiment {
name : String
baseline : String
candidate : String
target_samples : Int
mut baseline_metric : Double
mut candidate_metric : Double
mut samples : Int
}
///|
pub fn CanaryExperiment::new(
name : String,
baseline : String,
candidate : String,
target_samples? : Int = 100,
) -> CanaryExperiment {
{
name,
baseline,
candidate,
target_samples: if target_samples < 1 {
1
} else {
target_samples
},
baseline_metric: 0.0,
candidate_metric: 0.0,
samples: 0,
}
}
///|
pub fn CanaryExperiment::observe(
self : CanaryExperiment,
baseline : Double,
candidate : Double,
) -> Unit {
self.baseline_metric += baseline
self.candidate_metric += candidate
self.samples += 1
}
///|
pub fn CanaryExperiment::complete(self : CanaryExperiment) -> Bool {
self.samples >= self.target_samples
}
///|
pub fn CanaryExperiment::baseline(self : CanaryExperiment) -> Double {
if self.samples == 0 {
0.0
} else {
self.baseline_metric / self.samples.to_double()
}
}
///|
pub fn CanaryExperiment::candidate(self : CanaryExperiment) -> Double {
if self.samples == 0 {
0.0
} else {
self.candidate_metric / self.samples.to_double()
}
}
///|
pub fn CanaryExperiment::improvement(self : CanaryExperiment) -> Double {
self.candidate() - self.baseline()
}
///|
pub fn CanaryExperiment::samples(self : CanaryExperiment) -> Int {
self.samples
}
///|
pub fn CanaryExperiment::winner(self : CanaryExperiment) -> String {
if self.candidate() >= self.baseline() {
self.candidate
} else {
self.baseline
}
}