@@ -693,26 +693,20 @@ public function getNextScheduledTask(array $providerIds, array $taskTypeIds): Da
693693 throw new NotFoundException ();
694694 }
695695
696- $ taskIdsToIgnore = [];
697- while (true ) {
698- // Until we find a task whose task type is set to be provided by the providers requested with this request
699- // Or no scheduled task is found anymore (given the taskIds to ignore)
700- $ task = $ this ->taskProcessingManager ->getNextScheduledTask ($ possibleTaskTypeIds , $ taskIdsToIgnore );
701- try {
702- $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
703- if (in_array ($ provider ->getId (), $ possibleProviderIds , true )) {
704- if ($ this ->taskProcessingManager ->lockTask ($ task )) {
705- break ;
706- }
707- }
708- } catch (Exception ) {
709- // There is no provider set for the task type of this task
710- // proceed to ignore this task
711- }
712-
713- $ taskIdsToIgnore [] = (int )$ task ->getId ();
696+ // Atomically claim the oldest scheduled task across the eligible task types in a
697+ // single query (FOR UPDATE SKIP LOCKED, with a SQLite/Oracle fallback). This both
698+ // selects the task and marks it RUNNING, so multiple ex-app instances (e.g. several
699+ // replicas under Kubernetes) competing for the same queue never claim the same task
700+ // and no per-request ignore-list / retry loop is needed. $possibleTaskTypeIds is
701+ // already restricted to task types whose preferred provider is among the requested
702+ // providers, so any claimed task can be served by one of them.
703+ $ task = $ this ->taskProcessingManager ->claimNextScheduledTask ($ possibleTaskTypeIds );
704+ if ($ task === null ) {
705+ return new DataResponse (null , Http::STATUS_NO_CONTENT );
714706 }
715707
708+ $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
709+
716710 /** @var CoreTaskProcessingTask $json */
717711 $ json = $ task ->jsonSerialize ();
718712
@@ -753,27 +747,36 @@ public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds
753747 ]);
754748 }
755749
756- $ tasks = $ this ->taskProcessingManager ->getNextScheduledTasks ($ possibleTaskTypeIds , numberOfTasks: $ numberOfTasks + 1 );
757750 $ tasksJson = [];
758- // Stop when $numberOfTasks is reached or the json payload is larger than 50MiB
759- while (count ($ tasks ) > 0 && count ($ tasksJson ) < $ numberOfTasks && strlen (json_encode ($ tasks )) < 50 * 1024 * 1024 ) {
760- // Until we find a task whose task type is set to be provided by the providers requested with this request
761- // Or no scheduled task is found anymore (given the taskIds to ignore)
762- $ task = array_shift ($ tasks );
763- try {
764- $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
765- if (in_array ($ provider ->getId (), $ possibleProviderIds , true )) {
766- if ($ this ->taskProcessingManager ->lockTask ($ task )) {
767- $ tasksJson [] = ['task ' => $ task ->jsonSerialize (), 'provider ' => $ provider ->getId ()];
768- continue ;
769- }
770- }
771- } catch (Exception ) {
772- // There is no provider set for the task type of this task
773- // proceed to ignore this task
751+ // Atomically claim up to $numberOfTasks scheduled tasks, one by one. Each claim uses
752+ // FOR UPDATE SKIP LOCKED (with a SQLite/Oracle fallback) to mark the task RUNNING in
753+ // the same step, so concurrent ex-app instances (e.g. several replicas under
754+ // Kubernetes) never hand out the same task twice and no per-request ignore-list is
755+ // needed. $possibleTaskTypeIds is already restricted to task types whose preferred
756+ // provider is among the requested providers, so any claimed task can be served.
757+ while (count ($ tasksJson ) < $ numberOfTasks ) {
758+ $ task = $ this ->taskProcessingManager ->claimNextScheduledTask ($ possibleTaskTypeIds );
759+ if ($ task === null ) {
760+ // No more schedulable tasks.
761+ break ;
762+ }
763+ $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
764+ $ tasksJson [] = ['task ' => $ task ->jsonSerialize (), 'provider ' => $ provider ->getId ()];
765+ // Cap the response payload at ~50MiB. The task is already claimed, so it is always
766+ // included; we simply stop claiming further tasks once the limit is reached.
767+ if (strlen (json_encode ($ tasksJson )) >= 50 * 1024 * 1024 ) {
768+ break ;
774769 }
775770 }
776- $ hasMore = count ($ tasks ) > 0 ;
771+
772+ // Report whether at least one more schedulable task remains, without claiming it.
773+ $ hasMore = false ;
774+ try {
775+ $ this ->taskProcessingManager ->getNextScheduledTask ($ possibleTaskTypeIds );
776+ $ hasMore = true ;
777+ } catch (NotFoundException ) {
778+ // No further scheduled task remains.
779+ }
777780
778781 return new DataResponse ([
779782 'tasks ' => $ tasksJson ,
0 commit comments