fix(jobs): make completed->completed transition idempotent to prevent race errors (#1068)

* fix(jobs): make completed->completed transition idempotent to prevent race errors

Both execution_loop and the worker wrapper in execute() can race to call
mark_completed(). Previously the second call hit "Cannot transition from
completed to completed" and errored the job despite successful completion.

This narrowly allows only the Completed->Completed self-transition as
idempotent (early return with debug log, no duplicate history entry).
All other self-transitions remain rejected to preserve state machine
strictness.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix assert! formatting in idempotent completion test

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Zaki Manian
2026-03-16 07:53:06 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9e41b8acea
commit 596d17f04b
2 changed files with 73 additions and 3 deletions
+14 -3
View File
@@ -1591,7 +1591,7 @@ mod tests {
}
#[tokio::test]
async fn test_mark_completed_twice_returns_error() {
async fn test_mark_completed_twice_is_idempotent() {
let worker = make_worker(vec![]).await;
worker
@@ -1612,11 +1612,22 @@ mod tests {
.unwrap();
assert_eq!(ctx.state, JobState::Completed);
// Second mark_completed should succeed (idempotent) rather than
// erroring, matching the fix for the execution_loop / worker wrapper
// race condition.
let result = worker.mark_completed().await;
assert!(
result.is_err(),
"Completed Completed transition should be rejected by state machine"
result.is_ok(),
"Completed -> Completed transition should be idempotent"
);
// State should still be Completed
let ctx = worker
.context_manager()
.get_context(worker.job_id)
.await
.unwrap();
assert_eq!(ctx.state, JobState::Completed);
}
/// Build a Worker with the given approval context.