///|
/// Action selected by a production rollout guard.
pub(all) enum ProductionGuardrailAction {
GuardrailAllow
GuardrailObserve
GuardrailWarn
GuardrailBlock
GuardrailRollback
}
///|
pub fn production_guardrail_action_name(
action : ProductionGuardrailAction,
) -> String {
match action {
GuardrailAllow => "allow"
GuardrailObserve => "observe"
GuardrailWarn => "warn"
GuardrailBlock => "block"
GuardrailRollback => "rollback"
}
}
///|
/// Operational signal monitored during a canary rollout.
pub(all) enum ProductionGuardrailMetricKind {
GuardrailErrorRate
GuardrailFalsePositiveRate
GuardrailDetectionDelay
GuardrailDataQuality
GuardrailThroughput
GuardrailLatency
GuardrailCoverage
GuardrailDriftScore
}
///|
pub fn production_guardrail_metric_name(
metric : ProductionGuardrailMetricKind,
) -> String {
match metric {
GuardrailErrorRate => "error-rate"
GuardrailFalsePositiveRate => "false-positive-rate"
GuardrailDetectionDelay => "detection-delay"
GuardrailDataQuality => "data-quality"
GuardrailThroughput => "throughput"
GuardrailLatency => "latency"
GuardrailCoverage => "coverage"
GuardrailDriftScore => "drift-score"
}
}
///|
/// Direction of an operational threshold.
pub(all) enum ProductionGuardrailDirection {
GuardrailAbove
GuardrailBelow
GuardrailOutside
}
///|
pub fn production_guardrail_direction_name(
direction : ProductionGuardrailDirection,
) -> String {
match direction {
GuardrailAbove => "above"
GuardrailBelow => "below"
GuardrailOutside => "outside"
}
}
///|
/// A single rollout policy for one service-level signal.
pub struct ProductionGuardrailRule {
name : String
metric : ProductionGuardrailMetricKind
direction : ProductionGuardrailDirection
warning_threshold : Double
blocking_threshold : Double
rollback_threshold : Double
minimum_samples : Int
window_size : Int
consecutive_failures : Int
recovery_samples : Int
weight : Double
enabled : Bool
}
///|
pub fn ProductionGuardrailRule::new(
name : String,
metric? : ProductionGuardrailMetricKind = GuardrailErrorRate,
direction? : ProductionGuardrailDirection = GuardrailAbove,
warning_threshold? : Double = 0.05,
blocking_threshold? : Double = 0.10,
rollback_threshold? : Double = 0.25,
minimum_samples? : Int = 20,
window_size? : Int = 100,
consecutive_failures? : Int = 3,
recovery_samples? : Int = 5,
weight? : Double = 1.0,
enabled? : Bool = true,
) -> ProductionGuardrailRule {
let warning = if warning_threshold < 0.0 { 0.0 } else { warning_threshold }
let blocking = if blocking_threshold < warning {
warning
} else {
blocking_threshold
}
let rollback = if rollback_threshold < blocking {
blocking
} else {
rollback_threshold
}
{
name,
metric,
direction,
warning_threshold: warning,
blocking_threshold: blocking,
rollback_threshold: rollback,
minimum_samples: if minimum_samples < 1 {
1
} else {
minimum_samples
},
window_size: if window_size < 1 {
1
} else {
window_size
},
consecutive_failures: if consecutive_failures < 1 {
1
} else {
consecutive_failures
},
recovery_samples: if recovery_samples < 1 {
1
} else {
recovery_samples
},
weight: if weight < 0.0 {
0.0
} else {
weight
},
enabled,
}
}
///|
pub fn ProductionGuardrailRule::name(self : ProductionGuardrailRule) -> String {
self.name
}
///|
pub fn ProductionGuardrailRule::metric(
self : ProductionGuardrailRule,
) -> ProductionGuardrailMetricKind {
self.metric
}
///|
pub fn ProductionGuardrailRule::direction(
self : ProductionGuardrailRule,
) -> ProductionGuardrailDirection {
self.direction
}
///|
pub fn ProductionGuardrailRule::warning_threshold(
self : ProductionGuardrailRule,
) -> Double {
self.warning_threshold
}
///|
pub fn ProductionGuardrailRule::blocking_threshold(
self : ProductionGuardrailRule,
) -> Double {
self.blocking_threshold
}
///|
pub fn ProductionGuardrailRule::rollback_threshold(
self : ProductionGuardrailRule,
) -> Double {
self.rollback_threshold
}
///|
pub fn ProductionGuardrailRule::minimum_samples(
self : ProductionGuardrailRule,
) -> Int {
self.minimum_samples
}
///|
pub fn ProductionGuardrailRule::window_size(
self : ProductionGuardrailRule,
) -> Int {
self.window_size
}
///|
pub fn ProductionGuardrailRule::consecutive_failures(
self : ProductionGuardrailRule,
) -> Int {
self.consecutive_failures
}
///|
pub fn ProductionGuardrailRule::recovery_samples(
self : ProductionGuardrailRule,
) -> Int {
self.recovery_samples
}
///|
pub fn ProductionGuardrailRule::weight(
self : ProductionGuardrailRule,
) -> Double {
self.weight
}
///|
pub fn ProductionGuardrailRule::enabled(self : ProductionGuardrailRule) -> Bool {
self.enabled
}
///|
pub fn ProductionGuardrailRule::with_enabled(
self : ProductionGuardrailRule,
enabled : Bool,
) -> ProductionGuardrailRule {
{
name: self.name,
metric: self.metric,
direction: self.direction,
warning_threshold: self.warning_threshold,
blocking_threshold: self.blocking_threshold,
rollback_threshold: self.rollback_threshold,
minimum_samples: self.minimum_samples,
window_size: self.window_size,
consecutive_failures: self.consecutive_failures,
recovery_samples: self.recovery_samples,
weight: self.weight,
enabled,
}
}
///|
/// Observed value supplied to a rollout guard.
pub struct ProductionGuardrailObservation {
timestamp : Int64
metric : ProductionGuardrailMetricKind
value : Double
baseline : Double
sample_count : Int
confidence : Double
source : String
}
///|
pub fn ProductionGuardrailObservation::new(
timestamp : Int64,
metric : ProductionGuardrailMetricKind,
value : Double,
sample_count? : Int = 0,
baseline? : Double = 0.0,
confidence? : Double = 1.0,
source? : String = "runtime",
) -> ProductionGuardrailObservation {
{
timestamp,
metric,
value,
baseline,
sample_count: if sample_count < 0 {
0
} else {
sample_count
},
confidence: clamp_probability(confidence),
source,
}
}
///|
pub fn ProductionGuardrailObservation::timestamp(
self : ProductionGuardrailObservation,
) -> Int64 {
self.timestamp
}
///|
pub fn ProductionGuardrailObservation::metric(
self : ProductionGuardrailObservation,
) -> ProductionGuardrailMetricKind {
self.metric
}
///|
pub fn ProductionGuardrailObservation::value(
self : ProductionGuardrailObservation,
) -> Double {
self.value
}
///|
pub fn ProductionGuardrailObservation::baseline(
self : ProductionGuardrailObservation,
) -> Double {
self.baseline
}
///|
pub fn ProductionGuardrailObservation::sample_count(
self : ProductionGuardrailObservation,
) -> Int {
self.sample_count
}
///|
pub fn ProductionGuardrailObservation::confidence(
self : ProductionGuardrailObservation,
) -> Double {
self.confidence
}
///|
pub fn ProductionGuardrailObservation::source(
self : ProductionGuardrailObservation,
) -> String {
self.source
}
///|
pub fn ProductionGuardrailObservation::relative_change(
self : ProductionGuardrailObservation,
) -> Double {
if self.baseline == 0.0 {
if self.value == 0.0 {
0.0
} else {
self.value
}
} else {
(self.value - self.baseline) / self.baseline
}
}
///|
pub fn ProductionGuardrailObservation::is_finite(
self : ProductionGuardrailObservation,
) -> Bool {
self.value == self.value && self.value > -1.0e308 && self.value < 1.0e308
}
///|
/// Decision emitted by a guard after evaluating an observation.
pub struct ProductionGuardrailDecision {
action : ProductionGuardrailAction
rule_name : String
metric : ProductionGuardrailMetricKind
value : Double
threshold : Double
score : Double
triggered : Bool
failure_streak : Int
recovery_streak : Int
sequence : Int64
reason : String
}
///|
pub fn ProductionGuardrailDecision::action(
self : ProductionGuardrailDecision,
) -> ProductionGuardrailAction {
self.action
}
///|
pub fn ProductionGuardrailDecision::rule_name(
self : ProductionGuardrailDecision,
) -> String {
self.rule_name
}
///|
pub fn ProductionGuardrailDecision::metric(
self : ProductionGuardrailDecision,
) -> ProductionGuardrailMetricKind {
self.metric
}
///|
pub fn ProductionGuardrailDecision::value(
self : ProductionGuardrailDecision,
) -> Double {
self.value
}
///|
pub fn ProductionGuardrailDecision::threshold(
self : ProductionGuardrailDecision,
) -> Double {
self.threshold
}
///|
pub fn ProductionGuardrailDecision::score(
self : ProductionGuardrailDecision,
) -> Double {
self.score
}
///|
pub fn ProductionGuardrailDecision::triggered(
self : ProductionGuardrailDecision,
) -> Bool {
self.triggered
}
///|
pub fn ProductionGuardrailDecision::failure_streak(
self : ProductionGuardrailDecision,
) -> Int {
self.failure_streak
}
///|
pub fn ProductionGuardrailDecision::recovery_streak(
self : ProductionGuardrailDecision,
) -> Int {
self.recovery_streak
}
///|
pub fn ProductionGuardrailDecision::sequence(
self : ProductionGuardrailDecision,
) -> Int64 {
self.sequence
}
///|
pub fn ProductionGuardrailDecision::reason(
self : ProductionGuardrailDecision,
) -> String {
self.reason
}
///|
pub fn ProductionGuardrailDecision::summary(
self : ProductionGuardrailDecision,
) -> String {
production_guardrail_action_name(self.action) +
":" +
self.rule_name +
" value=" +
self.value.to_string() +
" score=" +
self.score.to_string() +
" " +
self.reason
}
///|
fn production_guardrail_compare(
direction : ProductionGuardrailDirection,
value : Double,
threshold : Double,
) -> Bool {
match direction {
GuardrailAbove => value >= threshold
GuardrailBelow => value <= threshold
GuardrailOutside => value >= threshold || value <= -threshold
}
}
///|
fn production_guardrail_distance(
direction : ProductionGuardrailDirection,
value : Double,
threshold : Double,
) -> Double {
match direction {
GuardrailAbove => value - threshold
GuardrailBelow => threshold - value
GuardrailOutside => {
let above = value - threshold
let below = -threshold - value
if above > below {
above
} else {
below
}
}
}
}
///|
fn production_guardrail_score(
rule : ProductionGuardrailRule,
value : Double,
) -> Double {
if production_guardrail_compare(
rule.direction(),
value,
rule.warning_threshold(),
) {
let distance = production_guardrail_distance(
rule.direction(),
value,
rule.warning_threshold(),
)
let span = rule.rollback_threshold() - rule.warning_threshold()
if span <= 0.0 {
rule.weight()
} else {
rule.weight() * (1.0 + distance / span)
}
} else {
0.0
}
}
///|
fn production_guardrail_find_rule(
rules : Array[ProductionGuardrailRule],
metric : ProductionGuardrailMetricKind,
) -> Int {
for i = 0; i < rules.length(); i = i + 1 {
if production_guardrail_metric_matches(rules[i].metric(), metric) &&
rules[i].enabled() {
return i
}
}
-1
}
///|
fn production_guardrail_metric_matches(
left : ProductionGuardrailMetricKind,
right : ProductionGuardrailMetricKind,
) -> Bool {
match left {
GuardrailErrorRate => right is GuardrailErrorRate
GuardrailFalsePositiveRate => right is GuardrailFalsePositiveRate
GuardrailDetectionDelay => right is GuardrailDetectionDelay
GuardrailDataQuality => right is GuardrailDataQuality
GuardrailThroughput => right is GuardrailThroughput
GuardrailLatency => right is GuardrailLatency
GuardrailCoverage => right is GuardrailCoverage
GuardrailDriftScore => right is GuardrailDriftScore
}
}
///|
/// Stateful canary and rollout gate.
pub struct ProductionGuardrailController {
mut rules : Array[ProductionGuardrailRule]
mut history : Array[ProductionGuardrailDecision]
mut failure_streak : Int
mut recovery_streak : Int
mut sequence : Int64
mut blocked : Bool
mut paused : Bool
mut last_timestamp : Int64
mut evaluated : Int
mut triggered : Int
}
///|
pub fn ProductionGuardrailController::new() -> ProductionGuardrailController {
{
rules: [],
history: [],
failure_streak: 0,
recovery_streak: 0,
sequence: 0L,
blocked: false,
paused: false,
last_timestamp: 0L,
evaluated: 0,
triggered: 0,
}
}
///|
pub fn ProductionGuardrailController::register(
self : ProductionGuardrailController,
rule : ProductionGuardrailRule,
) -> Bool {
for i = 0; i < self.rules.length(); i = i + 1 {
if self.rules[i].name() == rule.name() {
self.rules[i] = rule
return false
}
}
self.rules.push(rule)
true
}
///|
pub fn ProductionGuardrailController::remove(
self : ProductionGuardrailController,
name : String,
) -> Bool {
let mut found = false
let remaining : Array[ProductionGuardrailRule] = []
for rule in self.rules {
if rule.name() == name {
found = true
} else {
remaining.push(rule)
}
}
if found {
self.rules = remaining
}
found
}
///|
pub fn ProductionGuardrailController::rule_count(
self : ProductionGuardrailController,
) -> Int {
self.rules.length()
}
///|
pub fn ProductionGuardrailController::rules(
self : ProductionGuardrailController,
) -> Array[ProductionGuardrailRule] {
self.rules[:].to_owned()
}
///|
pub fn ProductionGuardrailController::history(
self : ProductionGuardrailController,
) -> Array[ProductionGuardrailDecision] {
self.history[:].to_owned()
}
///|
pub fn ProductionGuardrailController::is_blocked(
self : ProductionGuardrailController,
) -> Bool {
self.blocked
}
///|
pub fn ProductionGuardrailController::is_paused(
self : ProductionGuardrailController,
) -> Bool {
self.paused
}
///|
pub fn ProductionGuardrailController::failure_streak(
self : ProductionGuardrailController,
) -> Int {
self.failure_streak
}
///|
pub fn ProductionGuardrailController::recovery_streak(
self : ProductionGuardrailController,
) -> Int {
self.recovery_streak
}
///|
pub fn ProductionGuardrailController::evaluated(
self : ProductionGuardrailController,
) -> Int {
self.evaluated
}
///|
pub fn ProductionGuardrailController::triggered(
self : ProductionGuardrailController,
) -> Int {
self.triggered
}
///|
pub fn ProductionGuardrailController::pause(
self : ProductionGuardrailController,
) -> Unit {
self.paused = true
}
///|
pub fn ProductionGuardrailController::resume_guardrail(
self : ProductionGuardrailController,
) -> Unit {
self.paused = false
}
///|
pub fn ProductionGuardrailController::unblock(
self : ProductionGuardrailController,
) -> Unit {
self.blocked = false
self.failure_streak = 0
self.recovery_streak = 0
}
///|
fn production_guardrail_make_decision(
rule : ProductionGuardrailRule,
observation : ProductionGuardrailObservation,
action : ProductionGuardrailAction,
score : Double,
failure_streak : Int,
recovery_streak : Int,
sequence : Int64,
reason : String,
) -> ProductionGuardrailDecision {
let threshold = match action {
GuardrailAllow => rule.warning_threshold()
GuardrailObserve => rule.warning_threshold()
GuardrailWarn => rule.warning_threshold()
GuardrailBlock => rule.blocking_threshold()
GuardrailRollback => rule.rollback_threshold()
}
{
action,
rule_name: rule.name(),
metric: observation.metric(),
value: observation.value(),
threshold,
score,
triggered: action is GuardrailWarn ||
action is GuardrailBlock ||
action is GuardrailRollback,
failure_streak,
recovery_streak,
sequence,
reason,
}
}
///|
fn production_guardrail_invalid_decision(
observation : ProductionGuardrailObservation,
sequence : Int64,
) -> ProductionGuardrailDecision {
{
action: GuardrailBlock,
rule_name: "input-validation",
metric: observation.metric(),
value: observation.value(),
threshold: 0.0,
score: 1.0,
triggered: true,
failure_streak: 1,
recovery_streak: 0,
sequence,
reason: "observation is non-finite",
}
}
///|
/// Evaluate one signal and update the controller state.
pub fn ProductionGuardrailController::evaluate(
self : ProductionGuardrailController,
observation : ProductionGuardrailObservation,
) -> ProductionGuardrailDecision {
self.sequence = self.sequence + 1L
self.evaluated = self.evaluated + 1
self.last_timestamp = observation.timestamp()
let invalid = !observation.is_finite()
if invalid {
self.failure_streak = self.failure_streak + 1
self.recovery_streak = 0
self.blocked = true
let decision = production_guardrail_invalid_decision(
observation,
self.sequence,
)
self.history.push(decision)
self.triggered = self.triggered + 1
return decision
}
let rule_index = production_guardrail_find_rule(
self.rules,
observation.metric(),
)
if rule_index < 0 {
let decision = {
action: if self.paused {
GuardrailObserve
} else {
GuardrailAllow
},
rule_name: "unconfigured",
metric: observation.metric(),
value: observation.value(),
threshold: 0.0,
score: 0.0,
triggered: false,
failure_streak: self.failure_streak,
recovery_streak: self.recovery_streak,
sequence: self.sequence,
reason: "no enabled rule for metric",
}
self.history.push(decision)
return decision
}
let rule = self.rules[rule_index]
if observation.sample_count() < rule.minimum_samples() {
let decision = production_guardrail_make_decision(
rule,
observation,
GuardrailObserve,
0.0,
self.failure_streak,
self.recovery_streak,
self.sequence,
"sample count is below the minimum",
)
self.history.push(decision)
return decision
}
let score = production_guardrail_score(rule, observation.value())
let rollback = production_guardrail_compare(
rule.direction(),
observation.value(),
rule.rollback_threshold(),
)
let block = production_guardrail_compare(
rule.direction(),
observation.value(),
rule.blocking_threshold(),
)
let warning = production_guardrail_compare(
rule.direction(),
observation.value(),
rule.warning_threshold(),
)
let action = if rollback {
self.failure_streak = self.failure_streak + 1
self.recovery_streak = 0
if self.failure_streak >= rule.consecutive_failures() {
self.blocked = true
GuardrailRollback
} else {
GuardrailBlock
}
} else if block {
self.failure_streak = self.failure_streak + 1
self.recovery_streak = 0
if self.failure_streak >= rule.consecutive_failures() {
self.blocked = true
GuardrailBlock
} else {
GuardrailWarn
}
} else if warning {
self.failure_streak = self.failure_streak + 1
self.recovery_streak = 0
GuardrailWarn
} else {
self.failure_streak = 0
self.recovery_streak = self.recovery_streak + 1
if self.blocked && self.recovery_streak >= rule.recovery_samples() {
self.blocked = false
}
if self.paused {
GuardrailObserve
} else {
GuardrailAllow
}
}
let reason = match action {
GuardrailAllow => "signal is within rollout limits"
GuardrailObserve => "rollout is paused or input is warming up"
GuardrailWarn => "signal crossed the warning limit"
GuardrailBlock => "signal crossed the blocking limit"
GuardrailRollback => "signal crossed the rollback limit"
}
let decision = production_guardrail_make_decision(
rule,
observation,
action,
score,
self.failure_streak,
self.recovery_streak,
self.sequence,
reason,
)
self.history.push(decision)
if decision.triggered() {
self.triggered = self.triggered + 1
}
decision
}
///|
/// Evaluate aligned observations and return one decision per signal.
pub fn ProductionGuardrailController::evaluate_batch(
self : ProductionGuardrailController,
observations : Array[ProductionGuardrailObservation],
) -> Array[ProductionGuardrailDecision] {
let decisions : Array[ProductionGuardrailDecision] = []
for observation in observations {
decisions.push(self.evaluate(observation))
}
decisions
}
///|
pub fn ProductionGuardrailController::latest(
self : ProductionGuardrailController,
) -> ProductionGuardrailDecision? {
if self.history.length() == 0 {
None
} else {
Some(self.history[self.history.length() - 1])
}
}
///|
pub fn ProductionGuardrailController::clear_history(
self : ProductionGuardrailController,
) -> Unit {
self.history = []
}
///|
/// Compact rollout health summary.
pub struct ProductionGuardrailSummary {
evaluated : Int
triggered : Int
blocked : Bool
paused : Bool
failure_streak : Int
recovery_streak : Int
risk_score : Double
}
///|
pub fn ProductionGuardrailSummary::evaluated(
self : ProductionGuardrailSummary,
) -> Int {
self.evaluated
}
///|
pub fn ProductionGuardrailSummary::triggered(
self : ProductionGuardrailSummary,
) -> Int {
self.triggered
}
///|
pub fn ProductionGuardrailSummary::blocked(
self : ProductionGuardrailSummary,
) -> Bool {
self.blocked
}
///|
pub fn ProductionGuardrailSummary::paused(
self : ProductionGuardrailSummary,
) -> Bool {
self.paused
}
///|
pub fn ProductionGuardrailSummary::failure_streak(
self : ProductionGuardrailSummary,
) -> Int {
self.failure_streak
}
///|
pub fn ProductionGuardrailSummary::recovery_streak(
self : ProductionGuardrailSummary,
) -> Int {
self.recovery_streak
}
///|
pub fn ProductionGuardrailSummary::risk_score(
self : ProductionGuardrailSummary,
) -> Double {
self.risk_score
}
///|
pub fn ProductionGuardrailController::summary(
self : ProductionGuardrailController,
) -> ProductionGuardrailSummary {
let mut score = 0.0
let start = if self.history.length() > 20 {
self.history.length() - 20
} else {
0
}
for i = start; i < self.history.length(); i = i + 1 {
score = score + self.history[i].score()
}
let denominator = if self.history.length() - start == 0 {
1.0
} else {
(self.history.length() - start).to_double()
}
{
evaluated: self.evaluated,
triggered: self.triggered,
blocked: self.blocked,
paused: self.paused,
failure_streak: self.failure_streak,
recovery_streak: self.recovery_streak,
risk_score: score / denominator,
}
}
///|
/// A named canary stage with an explicit exposure range.
pub struct ProductionRolloutStage {
name : String
exposure : Double
minimum_duration : Int64
maximum_duration : Int64
required_health : Double
automatic_promotion : Bool
}
///|
pub fn ProductionRolloutStage::new(
name : String,
exposure? : Double = 0.1,
minimum_duration? : Int64 = 300L,
maximum_duration? : Int64 = 3600L,
required_health? : Double = 0.99,
automatic_promotion? : Bool = false,
) -> ProductionRolloutStage {
let min_duration = if minimum_duration < 0L { 0L } else { minimum_duration }
{
name,
exposure: clamp_probability(exposure),
minimum_duration: min_duration,
maximum_duration: if maximum_duration < min_duration {
min_duration
} else {
maximum_duration
},
required_health: clamp_probability(required_health),
automatic_promotion,
}
}
///|
pub fn ProductionRolloutStage::name(self : ProductionRolloutStage) -> String {
self.name
}
///|
pub fn ProductionRolloutStage::exposure(
self : ProductionRolloutStage,
) -> Double {
self.exposure
}
///|
pub fn ProductionRolloutStage::minimum_duration(
self : ProductionRolloutStage,
) -> Int64 {
self.minimum_duration
}
///|
pub fn ProductionRolloutStage::maximum_duration(
self : ProductionRolloutStage,
) -> Int64 {
self.maximum_duration
}
///|
pub fn ProductionRolloutStage::required_health(
self : ProductionRolloutStage,
) -> Double {
self.required_health
}
///|
pub fn ProductionRolloutStage::automatic_promotion(
self : ProductionRolloutStage,
) -> Bool {
self.automatic_promotion
}
///|
/// Ordered rollout plan used by deployment orchestration.
pub struct ProductionRolloutPlan {
name : String
stages : Array[ProductionRolloutStage]
mut active_stage : Int
mut started_at : Int64
mut completed : Bool
mut aborted : Bool
}
///|
pub fn ProductionRolloutPlan::new(name : String) -> ProductionRolloutPlan {
{
name,
stages: [],
active_stage: 0,
started_at: 0L,
completed: false,
aborted: false,
}
}
///|
pub fn ProductionRolloutPlan::name(self : ProductionRolloutPlan) -> String {
self.name
}
///|
pub fn ProductionRolloutPlan::add_stage(
self : ProductionRolloutPlan,
stage : ProductionRolloutStage,
) -> Unit {
if !self.completed && !self.aborted {
self.stages.push(stage)
}
}
///|
pub fn ProductionRolloutPlan::stage_count(self : ProductionRolloutPlan) -> Int {
self.stages.length()
}
///|
pub fn ProductionRolloutPlan::stages(
self : ProductionRolloutPlan,
) -> Array[ProductionRolloutStage] {
self.stages[:].to_owned()
}
///|
pub fn ProductionRolloutPlan::active_stage(self : ProductionRolloutPlan) -> Int {
self.active_stage
}
///|
pub fn ProductionRolloutPlan::current_stage(
self : ProductionRolloutPlan,
) -> ProductionRolloutStage? {
if self.stages.length() == 0 || self.active_stage >= self.stages.length() {
None
} else {
Some(self.stages[self.active_stage])
}
}
///|
pub fn ProductionRolloutPlan::start(
self : ProductionRolloutPlan,
timestamp : Int64,
) -> Bool {
if self.stages.length() == 0 || self.completed || self.aborted {
false
} else {
self.started_at = timestamp
self.active_stage = 0
true
}
}
///|
pub fn ProductionRolloutPlan::started_at(self : ProductionRolloutPlan) -> Int64 {
self.started_at
}
///|
pub fn ProductionRolloutPlan::is_completed(
self : ProductionRolloutPlan,
) -> Bool {
self.completed
}
///|
pub fn ProductionRolloutPlan::is_aborted(self : ProductionRolloutPlan) -> Bool {
self.aborted
}
///|
pub fn ProductionRolloutPlan::promote(
self : ProductionRolloutPlan,
timestamp : Int64,
health : Double,
) -> Bool {
if self.completed || self.aborted || self.stages.length() == 0 {
false
} else {
let stage = self.stages[self.active_stage]
let elapsed = timestamp - self.started_at
if elapsed < stage.minimum_duration() || health < stage.required_health() {
false
} else if self.active_stage + 1 >= self.stages.length() {
self.completed = true
true
} else {
self.active_stage = self.active_stage + 1
self.started_at = timestamp
true
}
}
}
///|
pub fn ProductionRolloutPlan::abort(self : ProductionRolloutPlan) -> Unit {
if !self.completed {
self.aborted = true
}
}
///|
/// Validate a plan before it is handed to a deployment service.
pub fn ProductionRolloutPlan::validate(
self : ProductionRolloutPlan,
) -> Array[String] {
let issues : Array[String] = []
if self.name.length() == 0 {
issues.push("rollout name is empty")
}
if self.stages.length() == 0 {
issues.push("rollout has no stages")
}
for i = 0; i < self.stages.length(); i = i + 1 {
let stage = self.stages[i]
if stage.name().length() == 0 {
issues.push("stage " + i.to_string() + " has no name")
}
if i > 0 {
let previous = self.stages[i - 1]
if stage.exposure() < previous.exposure() {
issues.push("stage exposure decreases at " + i.to_string())
}
}
if stage.minimum_duration() > stage.maximum_duration() {
issues.push("stage duration bounds are invalid at " + i.to_string())
}
}
issues
}
///|
/// Stable text representation for deployment audit logs.
pub fn ProductionRolloutPlan::describe(self : ProductionRolloutPlan) -> String {
let pieces : Array[String] = []
let completed_text = if self.completed { "true" } else { "false" }
let aborted_text = if self.aborted { "true" } else { "false" }
pieces.push("plan=" + self.name)
pieces.push("active=" + self.active_stage.to_string())
pieces.push("completed=" + completed_text)
pieces.push("aborted=" + aborted_text)
for i = 0; i < self.stages.length(); i = i + 1 {
let stage = self.stages[i]
pieces.push(
"stage" +
i.to_string() +
"=" +
stage.name() +
"@" +
stage.exposure().to_string(),
)
}
pieces.join(" ")
}
///|
/// Bounded canary tracker that joins guard decisions with rollout stages.
pub struct ProductionCanaryTracker {
plan : ProductionRolloutPlan
guardrail : ProductionGuardrailController
mut observations : Int
mut healthy_observations : Int
mut blocked_observations : Int
mut last_decision : ProductionGuardrailDecision?
}
///|
pub fn ProductionCanaryTracker::new(
plan : ProductionRolloutPlan,
guardrail : ProductionGuardrailController,
) -> ProductionCanaryTracker {
{
plan,
guardrail,
observations: 0,
healthy_observations: 0,
blocked_observations: 0,
last_decision: None,
}
}
///|
pub fn ProductionCanaryTracker::plan(
self : ProductionCanaryTracker,
) -> ProductionRolloutPlan {
self.plan
}
///|
pub fn ProductionCanaryTracker::guardrail(
self : ProductionCanaryTracker,
) -> ProductionGuardrailController {
self.guardrail
}
///|
pub fn ProductionCanaryTracker::observations(
self : ProductionCanaryTracker,
) -> Int {
self.observations
}
///|
pub fn ProductionCanaryTracker::healthy_observations(
self : ProductionCanaryTracker,
) -> Int {
self.healthy_observations
}
///|
pub fn ProductionCanaryTracker::blocked_observations(
self : ProductionCanaryTracker,
) -> Int {
self.blocked_observations
}
///|
pub fn ProductionCanaryTracker::health(
self : ProductionCanaryTracker,
) -> Double {
if self.observations == 0 {
1.0
} else {
self.healthy_observations.to_double() / self.observations.to_double()
}
}
///|
pub fn ProductionCanaryTracker::observe(
self : ProductionCanaryTracker,
observation : ProductionGuardrailObservation,
) -> ProductionGuardrailDecision {
let decision = self.guardrail.evaluate(observation)
self.observations = self.observations + 1
self.last_decision = Some(decision)
if decision.action() is GuardrailAllow ||
decision.action() is GuardrailObserve {
self.healthy_observations = self.healthy_observations + 1
}
if decision.action() is GuardrailBlock ||
decision.action() is GuardrailRollback {
self.blocked_observations = self.blocked_observations + 1
}
decision
}
///|
pub fn ProductionCanaryTracker::try_promote(
self : ProductionCanaryTracker,
timestamp : Int64,
) -> Bool {
self.plan.promote(timestamp, self.health())
}
///|
pub fn ProductionCanaryTracker::last_decision(
self : ProductionCanaryTracker,
) -> ProductionGuardrailDecision? {
self.last_decision
}
///|
pub fn ProductionCanaryTracker::reset(self : ProductionCanaryTracker) -> Unit {
self.observations = 0
self.healthy_observations = 0
self.blocked_observations = 0
self.last_decision = None
self.guardrail.clear_history()
}
///|
pub fn production_guardrail_action_is_failure(
action : ProductionGuardrailAction,
) -> Bool {
action is GuardrailBlock || action is GuardrailRollback
}
///|
pub fn production_guardrail_action_is_terminal(
action : ProductionGuardrailAction,
) -> Bool {
action is GuardrailRollback
}
///|
pub fn production_guardrail_health_from_actions(
actions : Array[ProductionGuardrailAction],
) -> Double {
if actions.length() == 0 {
1.0
} else {
let mut healthy = 0
for action in actions {
if !production_guardrail_action_is_failure(action) {
healthy = healthy + 1
}
}
healthy.to_double() / actions.length().to_double()
}
}
///|
pub fn production_guardrail_decisions_json(
decisions : Array[ProductionGuardrailDecision],
) -> String {
let entries : Array[String] = []
for decision in decisions {
entries.push(
"{\"action\":\"" +
production_guardrail_action_name(decision.action()) +
"\",\"metric\":\"" +
production_guardrail_metric_name(decision.metric()) +
"\",\"value\":" +
decision.value().to_string() +
",\"score\":" +
decision.score().to_string() +
"}",
)
}
"[" + entries.join(",") + "]"
}