@@ -721,26 +721,20 @@ public function getNextScheduledTask(array $providerIds, array $taskTypeIds): Da
721721 throw new NotFoundException ();
722722 }
723723
724- $ taskIdsToIgnore = [];
725- while (true ) {
726- // Until we find a task whose task type is set to be provided by the providers requested with this request
727- // Or no scheduled task is found anymore (given the taskIds to ignore)
728- $ task = $ this ->taskProcessingManager ->getNextScheduledTask ($ possibleTaskTypeIds , $ taskIdsToIgnore );
729- try {
730- $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
731- if (in_array ($ provider ->getId (), $ possibleProviderIds , true )) {
732- if ($ this ->taskProcessingManager ->lockTask ($ task )) {
733- break ;
734- }
735- }
736- } catch (Exception ) {
737- // There is no provider set for the task type of this task
738- // proceed to ignore this task
739- }
740-
741- $ taskIdsToIgnore [] = (int )$ task ->getId ();
724+ // Atomically claim the oldest scheduled task across the eligible task types in a
725+ // single query (FOR UPDATE SKIP LOCKED, with a SQLite/Oracle fallback). This both
726+ // selects the task and marks it RUNNING, so multiple ex-app instances (e.g. several
727+ // replicas under Kubernetes) competing for the same queue never claim the same task
728+ // and no per-request ignore-list / retry loop is needed. $possibleTaskTypeIds is
729+ // already restricted to task types whose preferred provider is among the requested
730+ // providers, so any claimed task can be served by one of them.
731+ $ task = $ this ->taskProcessingManager ->claimNextScheduledTask ($ possibleTaskTypeIds );
732+ if ($ task === null ) {
733+ return new DataResponse (null , Http::STATUS_NO_CONTENT );
742734 }
743735
736+ $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
737+
744738 /** @var CoreTaskProcessingTask $json */
745739 $ json = $ task ->jsonSerialize ();
746740
@@ -781,27 +775,36 @@ public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds
781775 ]);
782776 }
783777
784- $ tasks = $ this ->taskProcessingManager ->getNextScheduledTasks ($ possibleTaskTypeIds , numberOfTasks: $ numberOfTasks + 1 );
785778 $ tasksJson = [];
786- // Stop when $numberOfTasks is reached or the json payload is larger than 50MiB
787- while (count ($ tasks ) > 0 && count ($ tasksJson ) < $ numberOfTasks && strlen (json_encode ($ tasks )) < 50 * 1024 * 1024 ) {
788- // Until we find a task whose task type is set to be provided by the providers requested with this request
789- // Or no scheduled task is found anymore (given the taskIds to ignore)
790- $ task = array_shift ($ tasks );
791- try {
792- $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
793- if (in_array ($ provider ->getId (), $ possibleProviderIds , true )) {
794- if ($ this ->taskProcessingManager ->lockTask ($ task )) {
795- $ tasksJson [] = ['task ' => $ task ->jsonSerialize (), 'provider ' => $ provider ->getId ()];
796- continue ;
797- }
798- }
799- } catch (Exception ) {
800- // There is no provider set for the task type of this task
801- // proceed to ignore this task
779+ // Atomically claim up to $numberOfTasks scheduled tasks, one by one. Each claim uses
780+ // FOR UPDATE SKIP LOCKED (with a SQLite/Oracle fallback) to mark the task RUNNING in
781+ // the same step, so concurrent ex-app instances (e.g. several replicas under
782+ // Kubernetes) never hand out the same task twice and no per-request ignore-list is
783+ // needed. $possibleTaskTypeIds is already restricted to task types whose preferred
784+ // provider is among the requested providers, so any claimed task can be served.
785+ while (count ($ tasksJson ) < $ numberOfTasks ) {
786+ $ task = $ this ->taskProcessingManager ->claimNextScheduledTask ($ possibleTaskTypeIds );
787+ if ($ task === null ) {
788+ // No more schedulable tasks.
789+ break ;
790+ }
791+ $ provider = $ this ->taskProcessingManager ->getPreferredProvider ($ task ->getTaskTypeId ());
792+ $ tasksJson [] = ['task ' => $ task ->jsonSerialize (), 'provider ' => $ provider ->getId ()];
793+ // Cap the response payload at ~50MiB. The task is already claimed, so it is always
794+ // included; we simply stop claiming further tasks once the limit is reached.
795+ if (strlen (json_encode ($ tasksJson )) >= 50 * 1024 * 1024 ) {
796+ break ;
802797 }
803798 }
804- $ hasMore = count ($ tasks ) > 0 ;
799+
800+ // Report whether at least one more schedulable task remains, without claiming it.
801+ $ hasMore = false ;
802+ try {
803+ $ this ->taskProcessingManager ->getNextScheduledTask ($ possibleTaskTypeIds );
804+ $ hasMore = true ;
805+ } catch (NotFoundException ) {
806+ // No further scheduled task remains.
807+ }
805808
806809 return new DataResponse ([
807810 'tasks ' => $ tasksJson ,
0 commit comments