2026-03-17 14:21:23 -04:00
< ? php
declare ( strict_types = 1 );
/**
2026-03-19 06:14:26 -04:00
* SPDX - FileCopyrightText : 2026 Nextcloud GmbH and Nextcloud contributors
2026-03-17 14:21:23 -04:00
* SPDX - License - Identifier : AGPL - 3.0 - or - later
*/
namespace OC\Core\Command\TaskProcessing ;
use OC\Core\Command\Base ;
use OC\Core\Command\InterruptedException ;
use OCP\TaskProcessing\Exception\Exception ;
use OCP\TaskProcessing\Exception\NotFoundException ;
use OCP\TaskProcessing\IManager ;
use OCP\TaskProcessing\ISynchronousProvider ;
use Psr\Log\LoggerInterface ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
class WorkerCommand extends Base {
public function __construct (
private readonly IManager $taskProcessingManager ,
private readonly LoggerInterface $logger ,
) {
parent :: __construct ();
}
protected function configure () : void {
$this
-> setName ( 'taskprocessing:worker' )
-> setDescription ( 'Run a dedicated worker for synchronous TaskProcessing providers' )
-> addOption (
'timeout' ,
't' ,
InputOption :: VALUE_OPTIONAL ,
2026-03-19 04:39:02 -04:00
'Duration in seconds after which the worker exits (0 = run indefinitely). You should regularly (e.g. every 5 minutes) restart this worker by using this option to make sure it picks up configuration changes.' ,
2026-03-17 14:21:23 -04:00
0
)
-> addOption (
'interval' ,
'i' ,
InputOption :: VALUE_OPTIONAL ,
'Sleep duration in seconds between polling iterations when no task was processed' ,
1
)
-> addOption (
'once' ,
null ,
InputOption :: VALUE_NONE ,
'Process at most one task then exit'
2026-03-17 15:32:06 -04:00
)
-> addOption (
'taskTypes' ,
null ,
InputOption :: VALUE_REQUIRED | InputOption :: VALUE_IS_ARRAY ,
'Only process tasks of the given task type IDs (can be specified multiple times)'
2026-03-17 14:21:23 -04:00
);
parent :: configure ();
}
protected function execute ( InputInterface $input , OutputInterface $output ) : int {
$startTime = time ();
$timeout = ( int ) $input -> getOption ( 'timeout' );
$interval = ( int ) $input -> getOption ( 'interval' );
$once = $input -> getOption ( 'once' ) === true ;
2026-03-17 15:32:06 -04:00
/** @var list<string> $taskTypes */
$taskTypes = $input -> getOption ( 'taskTypes' );
2026-03-17 14:21:23 -04:00
if ( $timeout > 0 ) {
$output -> writeln ( '<info>Task processing worker will stop after ' . $timeout . ' seconds</info>' );
}
while ( true ) {
// Stop if timeout exceeded
if ( $timeout > 0 && ( $startTime + $timeout ) < time ()) {
$output -> writeln ( 'Timeout reached, exiting...' , OutputInterface :: VERBOSITY_VERBOSE );
break ;
}
// Handle SIGTERM/SIGINT gracefully
try {
$this -> abortIfInterrupted ();
} catch ( InterruptedException $e ) {
$output -> writeln ( '<info>Task processing worker stopped</info>' );
break ;
}
2026-03-17 15:32:06 -04:00
$processedTask = $this -> processNextTask ( $output , $taskTypes );
2026-03-17 14:21:23 -04:00
if ( $once ) {
break ;
}
if ( ! $processedTask ) {
$output -> writeln ( 'No task processed, waiting ' . $interval . ' second(s)...' , OutputInterface :: VERBOSITY_VERBOSE );
sleep ( $interval );
}
}
return 0 ;
}
/**
* Attempt to process one task across all preferred synchronous providers .
*
2026-03-18 09:35:10 -04:00
* To avoid starvation , all eligible task types are first collected and then
* the oldest scheduled task across all of them is fetched in a single query .
* This ensures that tasks are processed in the order they were scheduled ,
* regardless of which provider handles them .
*
2026-03-17 15:32:06 -04:00
* @ param list < string > $taskTypes When non - empty , only providers for these task type IDs are considered .
2026-03-17 14:21:23 -04:00
* @ return bool True if a task was processed , false if no task was found
*/
2026-03-17 15:32:06 -04:00
private function processNextTask ( OutputInterface $output , array $taskTypes = []) : bool {
2026-03-17 14:21:23 -04:00
$providers = $this -> taskProcessingManager -> getProviders ();
2026-03-18 09:35:10 -04:00
// Build a map of eligible taskTypeId => provider for all preferred synchronous providers
/** @var array<string, ISynchronousProvider> $eligibleProviders */
$eligibleProviders = [];
2026-03-17 14:21:23 -04:00
foreach ( $providers as $provider ) {
if ( ! $provider instanceof ISynchronousProvider ) {
continue ;
}
$taskTypeId = $provider -> getTaskTypeId ();
2026-03-17 15:32:06 -04:00
// If a task type whitelist was provided, skip providers not in the list
if ( ! empty ( $taskTypes ) && ! in_array ( $taskTypeId , $taskTypes , true )) {
continue ;
}
2026-03-17 14:21:23 -04:00
// Only use this provider if it is the preferred one for the task type
try {
$preferredProvider = $this -> taskProcessingManager -> getPreferredProvider ( $taskTypeId );
} catch ( Exception $e ) {
$this -> logger -> error ( 'Failed to get preferred provider for task type ' . $taskTypeId , [ 'exception' => $e ]);
continue ;
}
if ( $provider -> getId () !== $preferredProvider -> getId ()) {
continue ;
}
2026-03-18 09:35:10 -04:00
$eligibleProviders [ $taskTypeId ] = $provider ;
}
2026-03-17 14:21:23 -04:00
2026-03-18 09:35:10 -04:00
if ( empty ( $eligibleProviders )) {
return false ;
}
2026-03-17 14:21:23 -04:00
2026-03-18 09:35:10 -04:00
// Fetch the oldest scheduled task across all eligible task types in one query.
// This naturally prevents starvation: regardless of how many tasks one provider
// has queued, another provider's older tasks will be picked up first.
try {
$task = $this -> taskProcessingManager -> getNextScheduledTask ( array_keys ( $eligibleProviders ));
} catch ( NotFoundException ) {
return false ;
} catch ( Exception $e ) {
$this -> logger -> error ( 'Unknown error while retrieving scheduled TaskProcessing tasks' , [ 'exception' => $e ]);
return false ;
}
2026-03-17 14:21:23 -04:00
2026-03-18 09:35:10 -04:00
$taskTypeId = $task -> getTaskTypeId ();
$provider = $eligibleProviders [ $taskTypeId ];
2026-03-17 14:21:23 -04:00
2026-03-18 09:35:10 -04:00
$output -> writeln (
'Processing task ' . $task -> getId () . ' of type ' . $taskTypeId . ' with provider ' . $provider -> getId (),
OutputInterface :: VERBOSITY_VERBOSE
);
$this -> taskProcessingManager -> processTask ( $task , $provider );
$output -> writeln (
'Finished processing task ' . $task -> getId (),
OutputInterface :: VERBOSITY_VERBOSE
);
2026-03-17 14:21:23 -04:00
2026-03-18 09:35:10 -04:00
return true ;
2026-03-17 14:21:23 -04:00
}
}