fix(openapi): harden story 2.1 production lifecycle
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::{ArtifactSourceId, ImportJobSourceEnvelope};
|
||||
use crank_artifacts::ArtifactRef;
|
||||
|
||||
const APPLICATION_RESULT_KEY: &str = "_crank_application_result";
|
||||
const IMPORT_JOB_CLEANUP_BATCH: i64 = 128;
|
||||
const IMPORT_JOB_CLEANUP_BATCH: u32 = 128;
|
||||
const DANGLING_OPENAPI_SOURCE_GRACE: time::Duration = time::Duration::minutes(5);
|
||||
|
||||
impl PostgresRegistry {
|
||||
@@ -143,7 +143,16 @@ impl PostgresRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_expired_import_jobs(&self) -> Result<u64, RegistryError> {
|
||||
/// Runs a single bounded cleanup pass suitable for a periodic worker.
|
||||
///
|
||||
/// A corrupt historic payload is not trusted for source detachment, but it
|
||||
/// must not pin every later cleanup pass: the expired job is deleted and
|
||||
/// its source is subsequently eligible for the bounded dangling sweep.
|
||||
pub async fn cleanup_expired_import_jobs(
|
||||
&self,
|
||||
limit: u32,
|
||||
) -> Result<ImportJobCleanupReport, RegistryError> {
|
||||
let limit = i64::from(limit.clamp(1, IMPORT_JOB_CLEANUP_BATCH));
|
||||
let mut transaction = self.pool.begin().await?;
|
||||
let now = sqlx::query_scalar::<_, OffsetDateTime>("select now()")
|
||||
.fetch_one(&mut *transaction)
|
||||
@@ -156,22 +165,28 @@ impl PostgresRegistry {
|
||||
limit $1
|
||||
for update skip locked",
|
||||
)
|
||||
.bind(IMPORT_JOB_CLEANUP_BATCH)
|
||||
.bind(limit)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
let mut expired_ids = Vec::with_capacity(expired.len());
|
||||
let mut detached_sources = 0;
|
||||
for row in &expired {
|
||||
expired_ids.push(row.try_get::<String, _>("id")?);
|
||||
let workspace_id = WorkspaceId::new(row.try_get::<String, _>("workspace_id")?);
|
||||
let payload = row.try_get::<Value, _>("preview_payload")?;
|
||||
if let Some(source) = source_from_payload(&payload)? {
|
||||
detach_source_in_transaction(
|
||||
// The job itself is expired regardless of whether a legacy or
|
||||
// corrupt payload can be decoded. Do not let one bad row roll
|
||||
// back cleanup for every tenant.
|
||||
if let Ok(Some(source)) = source_from_payload(&payload)
|
||||
&& detach_source_in_transaction(
|
||||
&mut transaction,
|
||||
&workspace_id,
|
||||
&source.source_id,
|
||||
now,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
detached_sources += 1;
|
||||
}
|
||||
}
|
||||
let deleted = if expired_ids.is_empty() {
|
||||
@@ -206,17 +221,42 @@ impl PostgresRegistry {
|
||||
for update of s skip locked",
|
||||
)
|
||||
.bind(dangling_cutoff)
|
||||
.bind(IMPORT_JOB_CLEANUP_BATCH)
|
||||
.bind(limit)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
let has_more_dangling = dangling.len() == limit as usize;
|
||||
for row in dangling {
|
||||
let workspace_id = WorkspaceId::new(row.try_get::<String, _>("workspace_id")?);
|
||||
let source_id = ArtifactSourceId::new(row.try_get::<String, _>("source_id")?);
|
||||
detach_source_in_transaction(&mut transaction, &workspace_id, &source_id, now).await?;
|
||||
if detach_source_in_transaction(&mut transaction, &workspace_id, &source_id, now)
|
||||
.await?
|
||||
{
|
||||
detached_sources += 1;
|
||||
}
|
||||
}
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(deleted)
|
||||
Ok(ImportJobCleanupReport {
|
||||
deleted_jobs: deleted,
|
||||
detached_sources,
|
||||
more_work: expired.len() == limit as usize || has_more_dangling,
|
||||
})
|
||||
}
|
||||
|
||||
/// Exhausts currently visible cleanup work for startup and request paths.
|
||||
/// Periodic workers should use [`Self::cleanup_expired_import_jobs`] to
|
||||
/// keep each tick bounded.
|
||||
pub async fn delete_expired_import_jobs(&self) -> Result<u64, RegistryError> {
|
||||
let mut deleted_jobs = 0;
|
||||
loop {
|
||||
let report = self
|
||||
.cleanup_expired_import_jobs(IMPORT_JOB_CLEANUP_BATCH)
|
||||
.await?;
|
||||
deleted_jobs += report.deleted_jobs;
|
||||
if !report.more_work {
|
||||
return Ok(deleted_jobs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +326,7 @@ async fn apply_import_job_transaction(
|
||||
|
||||
let mut result = ImportJobApplyResult {
|
||||
application_key: request.application_key.to_owned(),
|
||||
skipped: request.pre_skipped.to_vec(),
|
||||
..ImportJobApplyResult::default()
|
||||
};
|
||||
for draft in request.operations {
|
||||
@@ -360,7 +401,7 @@ async fn apply_import_job_transaction(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
detach_source_in_transaction(
|
||||
let _ = detach_source_in_transaction(
|
||||
tx,
|
||||
request.workspace_id,
|
||||
&source.source_id,
|
||||
@@ -430,8 +471,8 @@ async fn detach_source_in_transaction(
|
||||
workspace_id: &WorkspaceId,
|
||||
source_id: &ArtifactSourceId,
|
||||
detached_at: OffsetDateTime,
|
||||
) -> Result<(), RegistryError> {
|
||||
sqlx::query(
|
||||
) -> Result<bool, RegistryError> {
|
||||
let detached = sqlx::query(
|
||||
"update artifact_sources
|
||||
set lifecycle = 'detached', updated_at = $1, detached_at = $1
|
||||
where workspace_id = $2 and source_id = $3
|
||||
@@ -441,8 +482,9 @@ async fn detach_source_in_transaction(
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(source_id.as_str())
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
Ok(())
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(detached == 1)
|
||||
}
|
||||
|
||||
fn stored_application_result(
|
||||
|
||||
Reference in New Issue
Block a user