///|
/// Lifecycle state of a configuration migration.
pub(all) enum MigrationState {
MigrationPlanned
MigrationRunning
MigrationApplied
MigrationRolledBack
MigrationFailed
} derive(Debug, Eq)
///|
/// Operation used by a migration step.
pub(all) enum MigrationOperation {
AddSetting
RemoveSetting
RenameSetting
ReplaceSetting
ValidateSetting
} derive(Debug, Eq)
///|
/// One deterministic configuration change with a rollback value.
pub(all) struct MigrationStep {
id : String
operation : MigrationOperation
key : String
value : String
rollback_value : String?
order : Int
mut applied : Bool
}
///|
pub fn MigrationStep::new(
id : String,
operation : MigrationOperation,
key : String,
value : String,
order : Int,
rollback_value? : String = "",
) -> Result[MigrationStep, ModbusError] {
if id == "" || key == "" || order < 0 {
return Err(InvalidData)
}
let selected_rollback = if rollback_value == "" {
None
} else {
Some(rollback_value)
}
Ok({
id,
operation,
key,
value,
rollback_value: selected_rollback,
order,
applied: false,
})
}
///|
pub fn MigrationStep::id(self : MigrationStep) -> String {
self.id
}
///|
pub fn MigrationStep::operation(self : MigrationStep) -> MigrationOperation {
self.operation
}
///|
pub fn MigrationStep::key(self : MigrationStep) -> String {
self.key
}
///|
pub fn MigrationStep::value(self : MigrationStep) -> String {
self.value
}
///|
pub fn MigrationStep::rollback_value(self : MigrationStep) -> String? {
self.rollback_value
}
///|
pub fn MigrationStep::order(self : MigrationStep) -> Int {
self.order
}
///|
pub fn MigrationStep::applied(self : MigrationStep) -> Bool {
self.applied
}
///|
pub fn MigrationStep::mark_applied(self : MigrationStep) -> Unit {
self.applied = true
}
///|
pub fn MigrationStep::mark_pending(self : MigrationStep) -> Unit {
self.applied = false
}
///|
pub fn migration_operation_name(operation : MigrationOperation) -> String {
match operation {
AddSetting => "add"
RemoveSetting => "remove"
RenameSetting => "rename"
ReplaceSetting => "replace"
ValidateSetting => "validate"
}
}
///|
/// A copy-on-write key/value configuration snapshot.
pub(all) struct MigrationSnapshot {
version : Int
entries : Array[(String, String)]
timestamp : Int
}
///|
pub fn MigrationSnapshot::new(
version : Int,
timestamp : Int,
) -> Result[MigrationSnapshot, ModbusError] {
if version < 0 || timestamp < 0 {
Err(InvalidData)
} else {
Ok({ version, entries: [], timestamp })
}
}
///|
pub fn MigrationSnapshot::version(self : MigrationSnapshot) -> Int {
self.version
}
///|
pub fn MigrationSnapshot::timestamp(self : MigrationSnapshot) -> Int {
self.timestamp
}
///|
pub fn MigrationSnapshot::length(self : MigrationSnapshot) -> Int {
self.entries.length()
}
///|
pub fn MigrationSnapshot::get(
self : MigrationSnapshot,
key : String,
) -> String? {
for entry in self.entries {
if entry.0 == key {
return Some(entry.1)
}
}
None
}
///|
pub fn MigrationSnapshot::put(
self : MigrationSnapshot,
key : String,
value : String,
) -> Result[Unit, ModbusError] {
if key == "" {
return Err(InvalidData)
}
for i in 0.. Result[String, ModbusError] {
for i in 0.. Array[String] {
let result : Array[String] = []
for entry in self.entries {
result.push(entry.0)
}
result
}
///|
/// Ordered migration plan with transactional execution state.
pub(all) struct MigrationPlan {
id : String
from_version : Int
to_version : Int
steps : Array[MigrationStep]
max_steps : Int
mut state : MigrationState
}
///|
pub fn MigrationPlan::new(
id : String,
from_version : Int,
to_version : Int,
max_steps? : Int = 256,
) -> Result[MigrationPlan, ModbusError] {
if id == "" || from_version < 0 || to_version <= from_version || max_steps < 1 {
return Err(InvalidData)
}
Ok({
id,
from_version,
to_version,
steps: [],
max_steps,
state: MigrationPlanned,
})
}
///|
pub fn MigrationPlan::id(self : MigrationPlan) -> String {
self.id
}
///|
pub fn MigrationPlan::from_version(self : MigrationPlan) -> Int {
self.from_version
}
///|
pub fn MigrationPlan::to_version(self : MigrationPlan) -> Int {
self.to_version
}
///|
pub fn MigrationPlan::state(self : MigrationPlan) -> MigrationState {
self.state
}
///|
pub fn MigrationPlan::step_count(self : MigrationPlan) -> Int {
self.steps.length()
}
///|
pub fn MigrationPlan::add_step(
self : MigrationPlan,
step : MigrationStep,
) -> Result[Unit, ModbusError] {
if self.state != MigrationPlanned || self.steps.length() >= self.max_steps {
return Err(Busy)
}
for existing in self.steps {
if existing.id() == step.id() || existing.order() == step.order() {
return Err(InvalidData)
}
}
self.steps.push(step)
Ok(())
}
///|
fn MigrationPlan::ordered_steps(self : MigrationPlan) -> Array[MigrationStep] {
let remaining = self.steps.copy()
let result : Array[MigrationStep] = []
while remaining.length() > 0 {
let mut best = 0
for i in 1.. Result[MigrationSnapshot, ModbusError] {
if self.state != MigrationPlanned || snapshot.version() != self.from_version {
return Err(Busy)
}
self.state = MigrationRunning
for step in self.ordered_steps() {
let result = match step.operation() {
AddSetting | ReplaceSetting | ValidateSetting =>
snapshot.put(step.key(), step.value())
RemoveSetting =>
match snapshot.remove(step.key()) {
Ok(_) => Ok(())
Err(error) => Err(error)
}
RenameSetting =>
match snapshot.remove(step.key()) {
Ok(old) => snapshot.put(step.value(), old)
Err(error) => Err(error)
}
}
match result {
Err(error) => {
self.state = MigrationFailed
return Err(error)
}
Ok(_) => step.mark_applied()
}
}
self.state = MigrationApplied
Ok({
version: self.to_version,
entries: snapshot.entries,
timestamp: snapshot.timestamp,
})
}
///|
/// Roll back applied steps in reverse order where rollback values exist.
pub fn MigrationPlan::rollback(
self : MigrationPlan,
snapshot : MigrationSnapshot,
) -> Result[MigrationSnapshot, ModbusError] {
if self.state != MigrationApplied {
return Err(Busy)
}
let ordered = self.ordered_steps()
let mut index = ordered.length() - 1
while index >= 0 {
let step = ordered[index]
if step.applied() {
match step.rollback_value() {
Some(value) => ignore(snapshot.put(step.key(), value))
None => ignore(snapshot.remove(step.key()))
}
step.mark_pending()
}
index -= 1
}
self.state = MigrationRolledBack
Ok({
version: self.from_version,
entries: snapshot.entries,
timestamp: snapshot.timestamp,
})
}
///|
pub fn MigrationPlan::export_lines(self : MigrationPlan) -> Array[String] {
let lines : Array[String] = []
lines.push(
"migration=" +
self.id +
" from=" +
self.from_version.to_string() +
" to=" +
self.to_version.to_string() +
" state=" +
migration_state_name(self.state),
)
for step in self.steps {
lines.push(
"step=" +
step.id() +
" op=" +
migration_operation_name(step.operation()) +
" key=" +
step.key() +
" order=" +
step.order().to_string() +
" applied=" +
step.applied().to_string(),
)
}
lines
}
///|
pub fn migration_state_name(state : MigrationState) -> String {
match state {
MigrationPlanned => "planned"
MigrationRunning => "running"
MigrationApplied => "applied"
MigrationRolledBack => "rolled-back"
MigrationFailed => "failed"
}
}