Recipe: Feature Flag Progressive Rollout

Canary release statechart: ramp up percentage traffic safely with health checks and instant automated rollback.


🎯 Statechart Topology

stateDiagram-v2
    [*] --> Off
    Off --> InternalOnly: ENABLE_INTERNAL
    InternalOnly --> Canary10: RAMP_10
    Canary10 --> Canary50: RAMP_50 [errorRateLow]
    Canary50 --> FullRollout: RAMP_100 [errorRateLow]
    
    Canary10 --> Rollback: HEALTH_ALERT
    Canary50 --> Rollback: HEALTH_ALERT
    FullRollout --> Rollback: HEALTH_ALERT

    Rollback --> Off: RESET

🛠️ Complete Rust Implementation

use xstate_statemachine::prelude::*;

const ROLLOUT_MACHINE: &str = r#"{
    "id": "canary_rollout",
    "initial": "off",
    "context": {
        "percentage": 0,
        "error_rate": 0.001
    },
    "states": {
        "off": {
            "on": {"ENABLE_INTERNAL": "internal_only"}
        },
        "internal_only": {
            "on": {
                "RAMP_10": {
                    "target": "canary_10",
                    "actions": "set10"
                }
            }
        },
        "canary_10": {
            "on": {
                "RAMP_50": {
                    "target": "canary_50",
                    "guard": "isHealthGood",
                    "actions": "set50"
                },
                "ALERT": "rollback"
            }
        },
        "canary_50": {
            "on": {
                "RAMP_100": {
                    "target": "full_rollout",
                    "guard": "isHealthGood",
                    "actions": "set100"
                },
                "ALERT": "rollback"
            }
        },
        "full_rollout": {
            "on": {"ALERT": "rollback"}
        },
        "rollback": {
            "entry": "emergencyShedLoad",
            "on": {"RESET": "off"}
        }
    }
}"#;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let machine = Machine::from_json(ROLLOUT_MACHINE)?;

    let logic = MachineLogic::dynamic()
        .guard("isHealthGood", |ctx, _event, _params| {
            let error_rate = ctx["error_rate"].as_f64().unwrap_or(0.0);
            Ok(error_rate < 0.01) // Error rate under 1%
        })
        .action("set10", |scope| {
            scope.context["percentage"] = 10.into();
            Ok(())
        })
        .action("set50", |scope| {
            scope.context["percentage"] = 50.into();
            Ok(())
        })
        .action("set100", |scope| {
            scope.context["percentage"] = 100.into();
            Ok(())
        })
        .action("emergencyShedLoad", |scope| {
            scope.context["percentage"] = 0.into();
            eprintln!("🚨 ALERT TRIGGERED: Feature rolled back to 0% traffic!");
            Ok(())
        });

    let mut interp = SyncInterpreter::new(machine, logic);
    interp.start()?;

    interp.send("ENABLE_INTERNAL")?;
    interp.send("RAMP_10")?;
    assert_eq!(interp.context()["percentage"], 10);

    // Sudden error spike triggers instant automated rollback!
    interp.send("ALERT")?;
    assert_eq!(interp.state_ids(), ["canary_rollout.rollback"]);
    assert_eq!(interp.context()["percentage"], 0);

    Ok(())
}