@@ -428,32 +428,36 @@ impl TaskStore {
428428 . await
429429 . context ( "failed to open worker task update transaction" ) ?;
430430
431- let row = sqlx:: query ( & format ! (
432- "{SELECT_COLUMNS} FROM tasks WHERE worker_id = ? ORDER BY updated_at DESC LIMIT 1 "
431+ let exact_row = sqlx:: query ( & format ! (
432+ "{SELECT_COLUMNS} FROM tasks WHERE worker_id = ? AND task_number = ? "
433433 ) )
434434 . bind ( worker_id)
435+ . bind ( task_number)
435436 . fetch_optional ( & mut * tx)
436437 . await
437- . context ( "failed to fetch task by worker id for update" ) ?;
438+ . context ( "failed to fetch worker task by id and number for update" ) ?;
439+
440+ let Some ( row) = exact_row else {
441+ let assigned_task_number = sqlx:: query_scalar :: < _ , i64 > (
442+ "SELECT task_number FROM tasks WHERE worker_id = ? ORDER BY task_number DESC LIMIT 1" ,
443+ )
444+ . bind ( worker_id)
445+ . fetch_optional ( & mut * tx)
446+ . await
447+ . context ( "failed to fetch any task by worker id for update" ) ?;
438448
439- let Some ( row) = row else {
440449 tx. commit ( )
441450 . await
442- . context ( "failed to commit empty worker task update transaction" ) ?;
451+ . context ( "failed to commit unmatched worker task update transaction" ) ?;
452+ if let Some ( assigned_task_number) = assigned_task_number {
453+ return Ok ( WorkerTaskUpdateResult :: WrongTask {
454+ assigned_task_number,
455+ } ) ;
456+ }
443457 return Ok ( WorkerTaskUpdateResult :: NotAssigned ) ;
444458 } ;
445459
446460 let current = task_from_row ( row) ?;
447- if current. task_number != task_number {
448- let assigned_task_number = current. task_number ;
449- tx. commit ( )
450- . await
451- . context ( "failed to commit rejected worker task update transaction" ) ?;
452- return Ok ( WorkerTaskUpdateResult :: WrongTask {
453- assigned_task_number,
454- } ) ;
455- }
456-
457461 let previous_status = current. status ;
458462 let task = Self :: update_current_in_tx ( & mut tx, task_number, current, input) . await ?;
459463
@@ -928,6 +932,59 @@ mod tests {
928932 assert_eq ! ( result. task. status, TaskStatus :: Done ) ;
929933 }
930934
935+ #[ tokio:: test]
936+ async fn update_worker_task_prefers_exact_match_with_duplicate_worker_bindings ( ) {
937+ let store = setup_store ( ) . await ;
938+ let first = store
939+ . create ( self_assigned_input ( "first task" , TaskStatus :: InProgress ) )
940+ . await
941+ . expect ( "first task should be created" ) ;
942+ let second = store
943+ . create ( self_assigned_input ( "second task" , TaskStatus :: InProgress ) )
944+ . await
945+ . expect ( "second task should be created" ) ;
946+
947+ let shared_worker_id = "worker-shared" ;
948+ store
949+ . update (
950+ first. task_number ,
951+ UpdateTaskInput {
952+ worker_id : Some ( shared_worker_id. to_string ( ) ) ,
953+ ..Default :: default ( )
954+ } ,
955+ )
956+ . await
957+ . expect ( "first worker binding should update" ) ;
958+ store
959+ . update (
960+ second. task_number ,
961+ UpdateTaskInput {
962+ worker_id : Some ( shared_worker_id. to_string ( ) ) ,
963+ ..Default :: default ( )
964+ } ,
965+ )
966+ . await
967+ . expect ( "second worker binding should update" ) ;
968+
969+ let result = store
970+ . update_worker_task (
971+ shared_worker_id,
972+ first. task_number ,
973+ UpdateTaskInput {
974+ metadata : Some ( serde_json:: json!( { "target" : "first" } ) ) ,
975+ ..Default :: default ( )
976+ } ,
977+ )
978+ . await
979+ . expect ( "worker-scoped update should succeed" ) ;
980+
981+ let WorkerTaskUpdateResult :: Updated ( result) = result else {
982+ panic ! ( "expected exact task update despite duplicate worker bindings" ) ;
983+ } ;
984+ assert_eq ! ( result. task. task_number, first. task_number) ;
985+ assert_eq ! ( result. task. metadata[ "target" ] , "first" ) ;
986+ }
987+
931988 #[ tokio:: test]
932989 async fn can_requeue_in_progress_and_clear_worker_binding ( ) {
933990 let store = setup_store ( ) . await ;
0 commit comments