use crate::context::DjogiContext;
use crate::live_migrate::compose::StepResult;
use crate::live_migrate::plan::{LivePlan, Step, StepKind};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ExecutorError {
#[error("plan file error: {0}")]
PlanFile(#[from] crate::live_migrate::plan_file::PlanFileError),
#[error("database error: {0}")]
Db(crate::DjogiError),
#[error("step {ordinal} failed: {reason}")]
StepFailed { ordinal: u32, reason: String },
#[error("backfill concurrency conflict: {0}")]
ConcurrencyConflict(String),
#[error("plan already completed with status: {0}")]
AlreadyCompleted(String),
#[error("step {ordinal} requires operator gate")]
OperatorGate { ordinal: u32, step_kind: String },
#[error("plan contains a destructive step but the destructive gate was not satisfied: {0}")]
DestructiveGateRefused(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
pub struct ExecutionContext<'a> {
pub ctx: &'a mut DjogiContext,
pub plan: &'a LivePlan,
pub current_step: &'a Step,
pub step_ordinal: u32,
pub is_resume: bool,
}
pub async fn run_plan(
ctx: &mut DjogiContext,
plan_path: std::path::PathBuf,
start_index: u32,
is_resume: bool,
allow_destructive: bool,
justify: Option<&str>,
) -> Result<StepResult, ExecutorError> {
let plan = crate::live_migrate::plan_file::read_plan(&plan_path)?;
if plan.has_destructive_steps() {
let justify_present = justify.map(|s| !s.trim().is_empty()).unwrap_or(false);
if !allow_destructive || !justify_present {
return Err(ExecutorError::DestructiveGateRefused(
"pass --allow-destructive with a non-empty --justify \"<reason>\" \
to execute the plan's DROP / TRUNCATE-class steps"
.to_string(),
));
}
}
let mut last_result = StepResult::Completed;
for (idx, step) in plan.steps.iter().enumerate() {
if (idx as u32) < start_index {
continue; }
super::state::update_step_index(
ctx,
plan.header.plan_id,
&plan.header.target_database,
&plan.header.app_label,
idx as i32,
Some(step.kind.as_db_str()),
)
.await
.map_err(ExecutorError::Db)?;
let exec = ExecutionContext {
ctx,
plan: &plan,
current_step: step,
step_ordinal: step.ordinal,
is_resume,
};
match execute_step(exec).await {
Ok(result) => {
last_result = result;
if matches!(last_result, StepResult::Paused) {
return Ok(StepResult::Paused);
}
if matches!(last_result, StepResult::Partial { .. }) {
return Ok(last_result);
}
}
Err(e) => {
let error_msg = e.to_string();
super::state::record_failure(
ctx,
plan.header.plan_id,
&plan.header.target_database,
&plan.header.app_label,
error_msg,
false, )
.await
.map_err(ExecutorError::Db)?;
return Err(e);
}
}
}
if matches!(last_result, StepResult::Completed) {
super::state::stamp_completed_at(
ctx,
plan.header.plan_id,
&plan.header.target_database,
&plan.header.app_label,
)
.await
.map_err(ExecutorError::Db)?;
super::state::update_status(
ctx,
plan.header.plan_id,
&plan.header.target_database,
&plan.header.app_label,
super::state::PlanStatus::Complete,
)
.await
.map_err(ExecutorError::Db)?;
}
Ok(last_result)
}
pub async fn execute_step(exec: ExecutionContext<'_>) -> Result<StepResult, ExecutorError> {
match exec.current_step.kind {
StepKind::ExpandSchema
| StepKind::FinalizeConstraints
| StepKind::CleanupLegacyState
| StepKind::RunReversibleSchemaOp => execute_ddl_step(exec).await,
StepKind::BackfillChunked => execute_backfill_step(exec).await,
StepKind::BeginCompatibilityWindow => Ok(StepResult::Completed),
StepKind::ValidateBackfill | StepKind::CutoverReads | StepKind::CutoverWrites => {
handle_operator_gate(exec).await
}
}
}
pub async fn execute_backfill_step(
exec: ExecutionContext<'_>,
) -> Result<StepResult, ExecutorError> {
use crate::live_migrate::backfill::{BackfillError, execute_backfill, resume_backfill};
use crate::live_migrate::plan::StepParameters;
let StepParameters::BackfillChunked {
table,
predicate_template,
chunk_size,
} = &exec.current_step.parameters
else {
return Err(ExecutorError::StepFailed {
ordinal: exec.step_ordinal,
reason: format!(
"expected BackfillChunked parameters, got {:?}",
exec.current_step.kind
),
});
};
let _chunks = if exec.is_resume {
resume_backfill(
exec.ctx,
exec.plan.header.plan_id,
table,
predicate_template,
*chunk_size,
false, )
.await
} else {
execute_backfill(
exec.ctx,
exec.plan.header.plan_id,
table,
predicate_template,
*chunk_size,
false, )
.await
}
.map_err(|e| match e {
BackfillError::Database(db_err) => ExecutorError::Db(db_err),
other => ExecutorError::StepFailed {
ordinal: exec.step_ordinal,
reason: other.to_string(),
},
})?;
Ok(StepResult::Completed)
}
pub async fn execute_ddl_step(exec: ExecutionContext<'_>) -> Result<StepResult, ExecutorError> {
use crate::live_migrate::plan::StepParameters;
let sql_segments = match &exec.current_step.parameters {
StepParameters::ExpandSchema { sql_segments } => sql_segments.clone(),
StepParameters::FinalizeConstraints { sql_segments } => sql_segments.clone(),
StepParameters::CleanupLegacyState { sql_segments } => sql_segments.clone(),
StepParameters::RunReversibleSchemaOp { up_sql, .. } => vec![up_sql.clone()],
_ => {
return Err(ExecutorError::StepFailed {
ordinal: exec.step_ordinal,
reason: format!("expected DDL parameters, got {:?}", exec.current_step.kind),
});
}
};
crate::transaction::atomic(exec.ctx, |tx_ctx| {
let segments = sql_segments.clone();
Box::pin(async move {
for segment in &segments {
tx_ctx.execute(segment, &[]).await?;
}
Ok::<_, crate::DjogiError>(StepResult::Completed)
})
})
.await
.map_err(ExecutorError::Db)
}
pub async fn handle_operator_gate(exec: ExecutionContext<'_>) -> Result<StepResult, ExecutorError> {
use crate::live_migrate::state::{PlanStatus, update_status};
let status = match exec.current_step.kind {
crate::live_migrate::plan::StepKind::ValidateBackfill => PlanStatus::Validating,
crate::live_migrate::plan::StepKind::CutoverReads
| crate::live_migrate::plan::StepKind::CutoverWrites => PlanStatus::Cutover,
_ => PlanStatus::Paused,
};
update_status(
exec.ctx,
exec.plan.header.plan_id,
&exec.plan.header.target_database,
&exec.plan.header.app_label,
status,
)
.await
.map_err(ExecutorError::Db)?;
Ok(StepResult::Paused)
}