Split orchestration by durable use case

This commit is contained in:
ms2k 2026-07-19 01:33:14 +09:00
commit fa81cb7f21
2 changed files with 609 additions and 513 deletions

View file

@ -21,6 +21,8 @@ use crate::{
TargetRegistry,
};
mod lifecycle;
#[derive(Clone)]
pub struct DeletionService {
pool: PgPool,
@ -46,156 +48,6 @@ impl DeletionService {
})
}
pub async fn soft_delete(
&self,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let mut tx = self.pool.begin().await?;
let outcome = Self::soft_delete_in_tx(&mut tx, principal, job_id).await?;
tx.commit().await?;
Ok(outcome)
}
pub async fn soft_delete_in_tx(
tx: &mut PgTx<'_>,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let row = lock_job(tx, job_id).await?;
authorize(principal, &row, ManagementOperation::SoftDelete)?;
require_terminal(&row.phase)?;
if row.deletion_operation_id.is_some() {
return Err(DeletionError::HardDeleteStarted);
}
if row.lifecycle == "soft_deleted" {
return Ok(LifecycleMutation::Unchanged);
}
let now = OffsetDateTime::now_utc();
sqlx::query(
"UPDATE jobs
SET lifecycle = 'soft_deleted', revision = revision + 1, updated_at = $2
WHERE job_id = $1",
)
.bind(job_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
enqueue(tx, "job.updated", json!({ "job_id": job_id })).await?;
Ok(LifecycleMutation::Changed)
}
pub async fn restore(
&self,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let mut tx = self.pool.begin().await?;
let outcome = Self::restore_in_tx(&mut tx, principal, job_id).await?;
tx.commit().await?;
Ok(outcome)
}
pub async fn restore_in_tx(
tx: &mut PgTx<'_>,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let row = lock_job(tx, job_id).await?;
authorize(principal, &row, ManagementOperation::Restore)?;
require_terminal(&row.phase)?;
if row.deletion_operation_id.is_some() {
return Err(DeletionError::HardDeleteStarted);
}
if row.lifecycle == "visible" {
return Ok(LifecycleMutation::Unchanged);
}
let now = OffsetDateTime::now_utc();
sqlx::query(
"UPDATE jobs
SET lifecycle = 'visible', revision = revision + 1, updated_at = $2
WHERE job_id = $1",
)
.bind(job_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
enqueue(tx, "job.updated", json!({ "job_id": job_id })).await?;
Ok(LifecycleMutation::Changed)
}
pub async fn hard_delete(
&self,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<HardDeleteOutcome, DeletionError> {
let mut tx = self.pool.begin().await?;
let outcome = Self::hard_delete_in_tx(&mut tx, principal, job_id).await?;
tx.commit().await?;
Ok(outcome)
}
pub async fn hard_delete_in_tx(
tx: &mut PgTx<'_>,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<HardDeleteOutcome, DeletionError> {
let operation_id = new_id('d')?;
let row = lock_job(tx, job_id).await?;
authorize(principal, &row, ManagementOperation::HardDelete)?;
require_terminal(&row.phase)?;
if let Some(existing) = row.deletion_operation_id {
let operation = load_operation_in_tx(tx, &existing).await?;
return Ok(HardDeleteOutcome {
mutation: LifecycleMutation::Unchanged,
operation,
});
}
validate_committed_inventory(tx, &row).await?;
validate_deletion_identities(tx, &row).await?;
let now = OffsetDateTime::now_utc();
sqlx::query(
"INSERT INTO deletion_operations (
operation_id, job_id, state, created_at, updated_at
) VALUES ($1, $2, 'queued', $3, $3)",
)
.bind(operation_id.as_str())
.bind(job_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
sqlx::query(
"UPDATE jobs
SET lifecycle = 'hard_deleting', deletion_operation_id = $2,
hard_delete_started_at = $3, revision = revision + 1, updated_at = $3
WHERE job_id = $1",
)
.bind(job_id.as_str())
.bind(operation_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
materialize_items(tx, job_id, &operation_id, now).await?;
finalize_if_empty(tx, job_id, &operation_id, now).await?;
enqueue(
tx,
"job.updated",
json!({ "job_id": job_id, "deletion_operation_id": operation_id }),
)
.await?;
enqueue(
tx,
"deletion.updated",
json!({ "job_id": job_id, "operation_id": operation_id }),
)
.await?;
let operation = load_operation_in_tx(tx, operation_id.as_str()).await?;
Ok(HardDeleteOutcome {
mutation: LifecycleMutation::Changed,
operation,
})
}
pub async fn operation(
&self,
principal: &Principal,

View file

@ -0,0 +1,153 @@
use super::*;
impl DeletionService {
pub async fn soft_delete(
&self,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let mut tx = self.pool.begin().await?;
let outcome = Self::soft_delete_in_tx(&mut tx, principal, job_id).await?;
tx.commit().await?;
Ok(outcome)
}
pub async fn soft_delete_in_tx(
tx: &mut PgTx<'_>,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let row = lock_job(tx, job_id).await?;
authorize(principal, &row, ManagementOperation::SoftDelete)?;
require_terminal(&row.phase)?;
if row.deletion_operation_id.is_some() {
return Err(DeletionError::HardDeleteStarted);
}
if row.lifecycle == "soft_deleted" {
return Ok(LifecycleMutation::Unchanged);
}
let now = OffsetDateTime::now_utc();
sqlx::query(
"UPDATE jobs
SET lifecycle = 'soft_deleted', revision = revision + 1, updated_at = $2
WHERE job_id = $1",
)
.bind(job_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
enqueue(tx, "job.updated", json!({ "job_id": job_id })).await?;
Ok(LifecycleMutation::Changed)
}
pub async fn restore(
&self,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let mut tx = self.pool.begin().await?;
let outcome = Self::restore_in_tx(&mut tx, principal, job_id).await?;
tx.commit().await?;
Ok(outcome)
}
pub async fn restore_in_tx(
tx: &mut PgTx<'_>,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<LifecycleMutation, DeletionError> {
let row = lock_job(tx, job_id).await?;
authorize(principal, &row, ManagementOperation::Restore)?;
require_terminal(&row.phase)?;
if row.deletion_operation_id.is_some() {
return Err(DeletionError::HardDeleteStarted);
}
if row.lifecycle == "visible" {
return Ok(LifecycleMutation::Unchanged);
}
let now = OffsetDateTime::now_utc();
sqlx::query(
"UPDATE jobs
SET lifecycle = 'visible', revision = revision + 1, updated_at = $2
WHERE job_id = $1",
)
.bind(job_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
enqueue(tx, "job.updated", json!({ "job_id": job_id })).await?;
Ok(LifecycleMutation::Changed)
}
pub async fn hard_delete(
&self,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<HardDeleteOutcome, DeletionError> {
let mut tx = self.pool.begin().await?;
let outcome = Self::hard_delete_in_tx(&mut tx, principal, job_id).await?;
tx.commit().await?;
Ok(outcome)
}
pub async fn hard_delete_in_tx(
tx: &mut PgTx<'_>,
principal: &Principal,
job_id: &OpaqueId,
) -> Result<HardDeleteOutcome, DeletionError> {
let operation_id = new_id('d')?;
let row = lock_job(tx, job_id).await?;
authorize(principal, &row, ManagementOperation::HardDelete)?;
require_terminal(&row.phase)?;
if let Some(existing) = row.deletion_operation_id {
let operation = load_operation_in_tx(tx, &existing).await?;
return Ok(HardDeleteOutcome {
mutation: LifecycleMutation::Unchanged,
operation,
});
}
validate_committed_inventory(tx, &row).await?;
validate_deletion_identities(tx, &row).await?;
let now = OffsetDateTime::now_utc();
sqlx::query(
"INSERT INTO deletion_operations (
operation_id, job_id, state, created_at, updated_at
) VALUES ($1, $2, 'queued', $3, $3)",
)
.bind(operation_id.as_str())
.bind(job_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
sqlx::query(
"UPDATE jobs
SET lifecycle = 'hard_deleting', deletion_operation_id = $2,
hard_delete_started_at = $3, revision = revision + 1, updated_at = $3
WHERE job_id = $1",
)
.bind(job_id.as_str())
.bind(operation_id.as_str())
.bind(now)
.execute(tx.as_mut())
.await?;
materialize_items(tx, job_id, &operation_id, now).await?;
finalize_if_empty(tx, job_id, &operation_id, now).await?;
enqueue(
tx,
"job.updated",
json!({ "job_id": job_id, "deletion_operation_id": operation_id }),
)
.await?;
enqueue(
tx,
"deletion.updated",
json!({ "job_id": job_id, "operation_id": operation_id }),
)
.await?;
let operation = load_operation_in_tx(tx, operation_id.as_str()).await?;
Ok(HardDeleteOutcome {
mutation: LifecycleMutation::Changed,
operation,
})
}
}