Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/api-core/src/handlers/machine_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,15 @@ pub(crate) async fn persist_validation_result(
);
}
};
let machine_validation =
db::machine_validation::find_by_id(&mut txn, &validation_result.validation_id).await?;
// Acquire the parent-run lock before record_result() touches run-item rows.
// Heartbeats and stale-attempt reconciliation use the same parent-run ->
// run-item order. Successful results also serialize with the trigger that
// increments the parent run's completed count.
let machine_validation = db::machine_validation::lock_by_id_no_key_update(
&mut txn,
&validation_result.validation_id,
)
.await?;
if !db::machine_validation::is_active(&machine_validation) {
tracing::info!(
validation_id = %validation_result.validation_id,
Expand Down
4 changes: 4 additions & 0 deletions crates/api-core/src/machine_validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ async fn reconcile_stale_attempt(
stale_attempt.attempt_id, stale_attempt.test_id, stale_attempt.validation_id
);

// Keep the same parent-run -> run-item lock order used by result
// persistence and heartbeats.
db::machine_validation::lock_by_id_no_key_update(txn, &stale_attempt.validation_id).await?;

let Some(validation_id) = db::machine_validation_execution::mark_attempt_stale_if_active(
txn,
&stale_attempt.attempt_id,
Expand Down
30 changes: 30 additions & 0 deletions crates/api-db/src/machine_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,36 @@ pub async fn find_by_id(
)))
}

pub async fn lock_by_id_no_key_update(
txn: &mut PgConnection,
id: &MachineValidationId,
) -> DatabaseResult<MachineValidation> {
// Raw SQL is required because FilterableQueryBuilder does not support row-lock clauses.
let query = "
SELECT
id,
machine_id,
name,
start_time,
end_time,
filter,
context,
state,
total,
completed,
duration_to_complete,
last_heartbeat_at
FROM machine_validation
WHERE id=$1
FOR NO KEY UPDATE";
sqlx::query_as::<_, MachineValidation>(query)
.bind(id)
.fetch_optional(txn)
.await
.map_err(|e| DatabaseError::query(query, e))?
.ok_or_else(|| DatabaseError::InvalidArgument(format!("Validation Id not found {id:?}")))
}

pub async fn find_all(txn: impl DbReader<'_>) -> DatabaseResult<Vec<MachineValidation>> {
find_by(txn, ObjectColumnFilter::<IdColumn>::All).await
}
Expand Down