use serde::Serialize;
use crate::core::Column as _;
use crate::sql::{Auto, FetcherPool, Pool, UpdaterPool};
use super::error::TenancyError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunState {
Pending,
Running,
Succeeded,
Failed,
}
impl RunState {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Succeeded => "succeeded",
Self::Failed => "failed",
}
}
#[must_use]
pub fn parse(raw: &str) -> Self {
match raw {
"pending" => Self::Pending,
"running" => Self::Running,
"succeeded" => Self::Succeeded,
_ => Self::Failed,
}
}
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(self, Self::Succeeded | Self::Failed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunKind {
Provision,
Migrate,
}
impl RunKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Provision => "provision",
Self::Migrate => "migrate",
}
}
#[must_use]
pub fn parse(raw: &str) -> Self {
match raw {
"migrate" => Self::Migrate,
_ => Self::Provision,
}
}
}
#[derive(crate::Model, Debug, Clone, Serialize)]
#[rustango(
table = "rustango_provisioning_runs",
scope = "registry",
display = "slug",
admin(
list_display = "slug, state, started_at, finished_at",
ordering = "-started_at",
)
)]
pub struct ProvisioningRun {
#[rustango(primary_key)]
pub id: crate::sql::Auto<i64>,
#[rustango(max_length = 16, index, default = "'provision'")]
pub kind: String,
#[rustango(max_length = 100, index)]
pub slug: String,
pub org_id: Option<i64>,
#[rustango(max_length = 16, index)]
pub state: String,
#[rustango(max_length = 16)]
pub storage_mode: String,
#[rustango(max_length = 16)]
pub backend_kind: String,
#[rustango(max_length = 500)]
pub database_url: Option<String>,
#[rustango(max_length = 150)]
pub requested_by: Option<String>,
#[rustango(max_length = 200, unique)]
pub idempotency_key: Option<String>,
pub error: Option<String>,
#[rustango(auto_now_add)]
pub started_at: crate::sql::Auto<chrono::DateTime<chrono::Utc>>,
pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(crate::Model, Debug, Clone, Serialize)]
#[rustango(
table = "rustango_provisioning_events",
scope = "registry",
display = "message",
admin(list_display = "run_id, seq, step, status", ordering = "run_id, seq")
)]
pub struct ProvisioningEvent {
#[rustango(primary_key)]
pub id: crate::sql::Auto<i64>,
#[rustango(fk = "rustango_provisioning_runs", on = "id", index)]
pub run_id: i64,
pub seq: i64,
#[rustango(max_length = 32)]
pub step: String,
#[rustango(max_length = 16)]
pub status: String,
pub message: String,
#[rustango(auto_now_add)]
pub at: crate::sql::Auto<chrono::DateTime<chrono::Utc>>,
}
pub async fn open_run(
registry: &Pool,
slug: &str,
storage_mode: &str,
backend_kind: &str,
database_url: Option<&str>,
requested_by: Option<&str>,
idempotency_key: Option<&str>,
) -> Result<ProvisioningRun, TenancyError> {
let mut run = ProvisioningRun {
id: Auto::default(),
kind: RunKind::Provision.as_str().to_owned(),
slug: slug.to_owned(),
org_id: None,
state: RunState::Running.as_str().to_owned(),
storage_mode: storage_mode.to_owned(),
backend_kind: backend_kind.to_owned(),
database_url: database_url.map(crate::sql::connect_diagnosis::redact),
requested_by: requested_by.map(ToOwned::to_owned),
idempotency_key: idempotency_key.map(ToOwned::to_owned),
error: None,
started_at: Auto::default(),
finished_at: None,
};
run.insert_pool(registry).await?;
Ok(run)
}
pub async fn append_event(
registry: &Pool,
run_id: i64,
seq: i64,
step: &str,
status: &str,
message: &str,
) -> Result<(), TenancyError> {
let mut event = ProvisioningEvent {
id: Auto::default(),
run_id,
seq,
step: step.to_owned(),
status: status.to_owned(),
message: message.to_owned(),
at: Auto::default(),
};
event.insert_pool(registry).await?;
Ok(())
}
pub async fn attach_org(registry: &Pool, run_id: i64, org_id: i64) -> Result<(), TenancyError> {
ProvisioningRun::objects()
.where_(ProvisioningRun::id.eq(run_id))
.update()
.set("org_id", org_id)
.execute_pool(registry)
.await?;
Ok(())
}
pub async fn finish_run(
registry: &Pool,
run_id: i64,
state: RunState,
error: Option<&str>,
) -> Result<(), TenancyError> {
ProvisioningRun::objects()
.where_(ProvisioningRun::id.eq(run_id))
.where_(ProvisioningRun::state.eq(RunState::Running.as_str().to_owned()))
.update()
.set("state", state.as_str())
.set("error", error)
.set("finished_at", chrono::Utc::now())
.execute_pool(registry)
.await?;
Ok(())
}
pub async fn run_by_id(
registry: &Pool,
run_id: i64,
) -> Result<Option<ProvisioningRun>, TenancyError> {
Ok(ProvisioningRun::objects()
.where_(ProvisioningRun::id.eq(run_id))
.fetch(registry)
.await?
.into_iter()
.next())
}
pub async fn run_by_idempotency_key(
registry: &Pool,
key: &str,
) -> Result<Option<ProvisioningRun>, TenancyError> {
Ok(ProvisioningRun::objects()
.where_(ProvisioningRun::idempotency_key.eq(Some(key.to_owned())))
.fetch(registry)
.await?
.into_iter()
.next())
}
pub async fn events_since(
registry: &Pool,
run_id: i64,
after_seq: i64,
) -> Result<Vec<ProvisioningEvent>, TenancyError> {
Ok(ProvisioningEvent::objects()
.where_(ProvisioningEvent::run_id.eq(run_id))
.where_(ProvisioningEvent::seq.gt(after_seq))
.order_by(&[("seq", false)])
.fetch(registry)
.await?)
}
pub async fn open_migrate_run(
registry: &Pool,
slug: Option<&str>,
requested_by: Option<&str>,
) -> Result<ProvisioningRun, TenancyError> {
let mut run = ProvisioningRun {
id: Auto::default(),
kind: RunKind::Migrate.as_str().to_owned(),
slug: slug.unwrap_or_default().to_owned(),
org_id: None,
state: RunState::Running.as_str().to_owned(),
storage_mode: String::new(),
backend_kind: String::new(),
database_url: None,
requested_by: requested_by.map(ToOwned::to_owned),
idempotency_key: None,
error: None,
started_at: Auto::default(),
finished_at: None,
};
run.insert_pool(registry).await?;
Ok(run)
}
pub async fn recent_runs(
registry: &Pool,
limit: i64,
offset: i64,
) -> Result<Vec<ProvisioningRun>, TenancyError> {
recent_runs_filtered(registry, None, None, limit, offset).await
}
pub async fn recent_runs_filtered(
registry: &Pool,
kind: Option<&str>,
state: Option<&str>,
limit: i64,
offset: i64,
) -> Result<Vec<ProvisioningRun>, TenancyError> {
let mut q = ProvisioningRun::objects();
if let Some(k) = kind {
q = q.where_(ProvisioningRun::kind.eq(k.to_owned()));
}
if let Some(s) = state {
q = q.where_(ProvisioningRun::state.eq(s.to_owned()));
}
Ok(q
.order_by(&[("id", true)])
.limit(limit)
.offset(offset)
.fetch(registry)
.await?)
}
pub const STALE_RUN_AFTER: std::time::Duration = std::time::Duration::from_secs(3600);
async fn is_stale(
registry: &Pool,
run: &ProvisioningRun,
after: std::time::Duration,
) -> Result<bool, TenancyError> {
if RunState::parse(&run.state) != RunState::Running {
return Ok(false);
}
let Some(id) = run.id.get().copied() else {
return Ok(false);
};
let last_event = ProvisioningEvent::objects()
.where_(ProvisioningEvent::run_id.eq(id))
.order_by(&[("seq", true)])
.limit(1)
.fetch(registry)
.await?
.into_iter()
.next()
.and_then(|e| e.at.get().copied());
let Some(last) = last_event.or_else(|| run.started_at.get().copied()) else {
return Ok(false);
};
let after = chrono::Duration::from_std(after).unwrap_or(chrono::Duration::MAX);
Ok(chrono::Utc::now() - last > after)
}
async fn close_interrupted(registry: &Pool, id: i64) -> Result<bool, TenancyError> {
let n = ProvisioningRun::objects()
.where_(ProvisioningRun::id.eq(id))
.where_(ProvisioningRun::state.eq(RunState::Running.as_str().to_owned()))
.update()
.set("state", RunState::Failed.as_str())
.set(
"error",
Some("interrupted: the process running it stopped before it finished"),
)
.set("finished_at", chrono::Utc::now())
.set("idempotency_key", None::<String>)
.execute_pool(registry)
.await?;
Ok(n > 0)
}
pub async fn reap_stale_runs(
registry: &Pool,
after: std::time::Duration,
) -> Result<usize, TenancyError> {
let running: Vec<ProvisioningRun> = ProvisioningRun::objects()
.where_(ProvisioningRun::state.eq(RunState::Running.as_str().to_owned()))
.fetch(registry)
.await?;
let mut closed = 0;
for run in running {
if is_stale(registry, &run, after).await?
&& close_interrupted(registry, run.id.get().copied().unwrap_or_default()).await?
{
closed += 1;
}
}
Ok(closed)
}
pub async fn release_for_retry(
registry: &Pool,
run: &ProvisioningRun,
stale_after: std::time::Duration,
) -> Result<bool, TenancyError> {
let Some(id) = run.id.get().copied() else {
return Ok(false);
};
if is_stale(registry, run, stale_after).await? {
return close_interrupted(registry, id).await;
}
if RunState::parse(&run.state) != RunState::Failed {
return Ok(false);
}
let n = ProvisioningRun::objects()
.where_(ProvisioningRun::id.eq(id))
.where_(ProvisioningRun::state.eq(RunState::Failed.as_str().to_owned()))
.update()
.set("idempotency_key", None::<String>)
.execute_pool(registry)
.await?;
Ok(n > 0)
}
pub async fn org_left_by_failed_run(registry: &Pool, org_id: i64) -> Result<bool, TenancyError> {
let creator = ProvisioningRun::objects()
.where_(ProvisioningRun::org_id.eq(Some(org_id)))
.where_(ProvisioningRun::kind.eq(RunKind::Provision.as_str().to_owned()))
.order_by(&[("id", false)])
.limit(1)
.fetch(registry)
.await?
.into_iter()
.next();
Ok(creator.is_some_and(|r| RunState::parse(&r.state) == RunState::Failed))
}
pub async fn prune_runs(
registry: &Pool,
cutoff: chrono::DateTime<chrono::Utc>,
) -> Result<usize, TenancyError> {
let stale: Vec<ProvisioningRun> = ProvisioningRun::objects()
.where_(ProvisioningRun::finished_at.lt(Some(cutoff)))
.fetch(registry)
.await?;
let mut removed = 0;
for run in stale {
let Some(id) = run.id.get().copied() else {
continue;
};
let events: Vec<ProvisioningEvent> = ProvisioningEvent::objects()
.where_(ProvisioningEvent::run_id.eq(id))
.fetch(registry)
.await?;
for event in events {
event.delete_pool(registry).await?;
}
run.delete_pool(registry).await?;
removed += 1;
}
Ok(removed)
}