наблюдаемость: завершить базовый контур Community
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
const APPLICATION_RESULT_KEY: &str = "_crank_application_result";
|
||||
|
||||
impl PostgresRegistry {
|
||||
pub async fn create_import_job(
|
||||
&self,
|
||||
@@ -97,6 +99,43 @@ impl PostgresRegistry {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply_import_job(
|
||||
&self,
|
||||
request: ApplyImportJobRequest<'_>,
|
||||
) -> Result<ImportJobApplyResult, RegistryError> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let applied = apply_import_job_transaction(&mut tx, &request).await;
|
||||
|
||||
match applied {
|
||||
Ok(result) => {
|
||||
tx.commit().await?;
|
||||
Ok(result)
|
||||
}
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
let error_text = error.to_string();
|
||||
let _ = sqlx::query(
|
||||
"update import_jobs
|
||||
set status = $3,
|
||||
error_text = $4,
|
||||
finished_at = $5::timestamptz
|
||||
where id = $1
|
||||
and workspace_id = $2
|
||||
and status <> $6",
|
||||
)
|
||||
.bind(request.id.as_str())
|
||||
.bind(request.workspace_id.as_str())
|
||||
.bind(serialize_enum_text(&ImportJobStatus::Failed, "status")?)
|
||||
.bind(error_text)
|
||||
.bind(request.finished_at)
|
||||
.bind(serialize_enum_text(&ImportJobStatus::Completed, "status")?)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_expired_import_jobs(&self) -> Result<u64, RegistryError> {
|
||||
let result = sqlx::query("delete from import_jobs where expires_at < now()")
|
||||
.execute(&self.pool)
|
||||
@@ -105,3 +144,160 @@ impl PostgresRegistry {
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_import_job_transaction(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
request: &ApplyImportJobRequest<'_>,
|
||||
) -> Result<ImportJobApplyResult, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select status, preview_payload
|
||||
from import_jobs
|
||||
where id = $1 and workspace_id = $2
|
||||
for update",
|
||||
)
|
||||
.bind(request.id.as_str())
|
||||
.bind(request.workspace_id.as_str())
|
||||
.fetch_optional(&mut **tx)
|
||||
.await?
|
||||
.ok_or_else(|| RegistryError::ImportJobNotFound {
|
||||
job_id: request.id.as_str().to_owned(),
|
||||
})?;
|
||||
let status = deserialize_enum_text::<ImportJobStatus>(row.try_get("status")?, "status")?;
|
||||
let mut preview_payload = row.try_get::<Value, _>("preview_payload")?;
|
||||
|
||||
if status == ImportJobStatus::Completed
|
||||
&& let Some(result) = stored_application_result(&preview_payload)?
|
||||
{
|
||||
if result.application_key != request.application_key {
|
||||
return Err(RegistryError::ImportJobAlreadyApplied {
|
||||
job_id: request.id.as_str().to_owned(),
|
||||
});
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
sqlx::query("select id from workspaces where id = $1 for update")
|
||||
.bind(request.workspace_id.as_str())
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
|
||||
let mut result = ImportJobApplyResult {
|
||||
application_key: request.application_key.to_owned(),
|
||||
..ImportJobApplyResult::default()
|
||||
};
|
||||
for draft in request.operations {
|
||||
if draft.operation.version != 1 {
|
||||
return Err(RegistryError::InvalidInitialVersion {
|
||||
operation_id: draft.operation.id.as_str().to_owned(),
|
||||
version: draft.operation.version,
|
||||
});
|
||||
}
|
||||
|
||||
let mut operation = draft.operation.clone();
|
||||
let original_name = operation.name.clone();
|
||||
if operation_name_exists(tx, request.workspace_id, &operation.name).await? {
|
||||
match request.conflict_mode {
|
||||
ImportConflictMode::Skip => {
|
||||
result.skipped.push(SkippedImportOperation {
|
||||
operation_key: draft.operation_key.clone(),
|
||||
name: operation.name,
|
||||
reason: "operation_name_conflict".to_owned(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
ImportConflictMode::Rename => {
|
||||
operation.name =
|
||||
next_available_operation_name(tx, request.workspace_id, &operation.name)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insert_operation_rows(tx, request.workspace_id, &operation, None).await?;
|
||||
result.created.push(AppliedImportOperation {
|
||||
operation_key: draft.operation_key.clone(),
|
||||
operation_id: operation.id,
|
||||
name: operation.name.clone(),
|
||||
version: operation.version,
|
||||
renamed_from: (operation.name != original_name).then_some(original_name),
|
||||
});
|
||||
}
|
||||
|
||||
let stored_result = serde_json::to_value(&result)?;
|
||||
if let Some(object) = preview_payload.as_object_mut() {
|
||||
object.insert(APPLICATION_RESULT_KEY.to_owned(), stored_result);
|
||||
} else {
|
||||
preview_payload = serde_json::json!({
|
||||
"preview": preview_payload,
|
||||
"_crank_application_result": stored_result,
|
||||
});
|
||||
}
|
||||
let created_operation_ids = serde_json::to_value(
|
||||
result
|
||||
.created
|
||||
.iter()
|
||||
.map(|operation| operation.operation_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
sqlx::query(
|
||||
"update import_jobs
|
||||
set status = $3,
|
||||
preview_payload = $4,
|
||||
created_operation_ids = $5,
|
||||
error_text = null,
|
||||
finished_at = $6::timestamptz
|
||||
where id = $1 and workspace_id = $2",
|
||||
)
|
||||
.bind(request.id.as_str())
|
||||
.bind(request.workspace_id.as_str())
|
||||
.bind(serialize_enum_text(&ImportJobStatus::Completed, "status")?)
|
||||
.bind(preview_payload)
|
||||
.bind(created_operation_ids)
|
||||
.bind(request.finished_at)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn stored_application_result(
|
||||
preview_payload: &Value,
|
||||
) -> Result<Option<ImportJobApplyResult>, RegistryError> {
|
||||
preview_payload
|
||||
.get(APPLICATION_RESULT_KEY)
|
||||
.cloned()
|
||||
.map(serde_json::from_value)
|
||||
.transpose()
|
||||
.map_err(RegistryError::from)
|
||||
}
|
||||
|
||||
async fn operation_name_exists(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
workspace_id: &WorkspaceId,
|
||||
name: &str,
|
||||
) -> Result<bool, RegistryError> {
|
||||
Ok(sqlx::query_scalar::<_, bool>(
|
||||
"select exists(
|
||||
select 1 from operations where workspace_id = $1 and name = $2
|
||||
)",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(name)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn next_available_operation_name(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
workspace_id: &WorkspaceId,
|
||||
base_name: &str,
|
||||
) -> Result<String, RegistryError> {
|
||||
for index in 2.. {
|
||||
let candidate = format!("{base_name}_{index}");
|
||||
if !operation_name_exists(tx, workspace_id, &candidate).await? {
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user