*/ final class Worker implements WorkerInterface { use LoggerAwareTrait; const WAIT_INTERVAL = 1; // sec const MAX_SLEEPING_TIME_FOR_DEDICATED_QUEUE = 30; //max sleeping time if working on only one queue /** * @var QueueDispatcherInterface */ private $queueService; /** * @var QueueInterface */ private $dedicatedQueue; private $maxIterations = 0; //0 means infinite iteration private $iterations; private $shutdown; private $paused; private $iterationsWithOutTask = 0; private $processId; private $logContext; private $taskLog; /** * @var bool */ private $handleSignals; /** * @param QueueDispatcherInterface $queueService * @param TaskLogInterface $taskLog * @param bool $handleSignals */ public function __construct(QueueDispatcherInterface $queueService, TaskLogInterface $taskLog, $handleSignals = true) { $this->queueService = $queueService; $this->taskLog = $taskLog; $this->handleSignals = $handleSignals; $this->processId = getmypid(); $this->logContext = [ 'PID' => $this->processId ]; if ($handleSignals) { $this->registerSigHandlers(); } } /** * @inheritdoc */ public function run() { $this->logDebug('Starting worker.', $this->logContext); while ($this->isRunning()) { if ($this->paused) { $this->logDebug('Paused... ', array_merge($this->logContext, [ 'Iteration' => $this->iterations ])); usleep(self::WAIT_INTERVAL * 1000000); continue; } ++$this->iterations; $this->logContext = array_merge($this->logContext, [ 'Iteration' => $this->iterations ]); try { $this->logDebug('Fetching tasks from queue ', $this->logContext); // if there is a dedicated queue set, let's do dequeue on that one // otherwise using the built-in strategy to get a new task from any registered queue $task = $this->dedicatedQueue instanceof QueueInterface ? $this->dedicatedQueue->dequeue() : $this->queueService->dequeue(); // if no task to process, sleep for the specified time and continue. if (!$task) { ++$this->iterationsWithOutTask; $waitInterval = $this->getWaitInterval(); $this->logDebug('Sleeping for ' . $waitInterval . ' sec', $this->logContext); usleep($waitInterval * 1000000); continue; } // we have task, so set this back to 0 $this->iterationsWithOutTask = 0; if (!$task instanceof TaskInterface) { $this->logWarning('The received queue item (' . $task . ') not processable.', $this->logContext); continue; } $this->processTask($task); unset($task); } catch (\Exception $e) { $this->logError('Fetching data from queue failed with MSG: ' . $e->getMessage(), $this->logContext); continue; } } $this->logDebug('Worker finished.', $this->logContext); } /** * @inheritdoc */ public function processTask(TaskInterface $task) { $report = Report::createInfo(__('Running task %s', $task->getId())); try { $this->logDebug('Processing task ' . $task->getId(), $this->logContext); $rowsTouched = $this->taskLog->setStatus($task->getId(), TaskLogInterface::STATUS_RUNNING, TaskLogInterface::STATUS_DEQUEUED); // if the task is being executed by another worker, just return, no report needs to be saved if (!$rowsTouched) { $this->logDebug('Task ' . $task->getId() . ' seems to be processed by another worker.', $this->logContext); return TaskLogInterface::STATUS_UNKNOWN; } // let the task know that it is called from a worker $task->applyWorkerContext(); // execute the task $taskReport = $task(); if (!$taskReport instanceof Report) { $this->logWarning('Task ' . $task->getId() . ' should return a report object.', $this->logContext); $taskReport = Report::createInfo(__('Task not returned any report.')); } $report->add($taskReport); unset($taskReport, $rowsTouched); } catch (\Exception $e) { $this->logError('Executing task ' . $task->getId() . ' failed with MSG: ' . $e->getMessage(), $this->logContext); $report = Report::createFailure(__('Executing task %s failed', $task->getId())); } // Initial status $status = $report->getType() == Report::TYPE_ERROR || $report->containsError() ? TaskLogInterface::STATUS_FAILED : TaskLogInterface::STATUS_COMPLETED; // Change the status if the task has children if ($task->hasChildren() && $status == TaskLogInterface::STATUS_COMPLETED) { $status = TaskLogInterface::STATUS_CHILD_RUNNING; } $cloneCreated = false; // if the task is a special sync task: the status of the parent task depends on the status of the remote task. if ($this->isRemoteTaskSynchroniser($task) && $status == TaskLogInterface::STATUS_COMPLETED) { // if the remote task is still in progress, we have to reschedule this task // the RESTApi returns TaskLogCategorizedStatus values if (in_array($this->getRemoteStatus($task), [TaskLogCategorizedStatus::STATUS_CREATED, TaskLogCategorizedStatus::STATUS_IN_PROGRESS])) { if ($this->queueService->count() <= 1) { //if there is less than or exactly one task in the queue, let's sleep a bit, in order not to regenerate the same task too much sleep(3); } $cloneCreated = $this->queueService->enqueue(clone $task, $task->getLabel()); } elseif ($this->getRemoteStatus($task) == TaskLogCategorizedStatus::STATUS_FAILED) { // if the remote task status is failed $status = TaskLogInterface::STATUS_FAILED; } } if (!$cloneCreated) { $this->taskLog->setReport($task->getId(), $report, $status); } else { // if there is a clone, delete the old task log //TODO: once we have the centralized way of cleaning up the log table, this should be refactored $this->taskLog->getBroker()->deleteById($task->getId()); } // Update parent if ($task->hasParent()) { /** @var TaskLogEntity $parentLogTask */ $parentLogTask = $this->taskLog->getById($task->getParentId()); if (!$parentLogTask->isMasterStatus()) { $this->taskLog->updateParent($task->getParentId()); } } unset($report); // delete message from queue $this->queueService->acknowledge($task); return $status; } /** * @param TaskInterface $task * @return bool */ private function isRemoteTaskSynchroniser(TaskInterface $task) { return $task instanceof RemoteTaskSynchroniserInterface || ($task instanceof CallbackTaskInterface && $task->getCallable() instanceof RemoteTaskSynchroniserInterface); } /** * @param TaskInterface $task * @return mixed */ private function getRemoteStatus(TaskInterface $task) { return $task instanceof CallbackTaskInterface ? $task->getCallable()->getRemoteStatus() : $task->getRemoteStatus(); } /** * Only set-able if there is a dedicated queue set. * @deprecated * * @inheritdoc */ public function setMaxIterations($maxIterations) { $this->maxIterations = $maxIterations; return $this; } /** * @inheritdoc */ public function setDedicatedQueue(QueueInterface $queue, $maxIterations = 0) { $this->dedicatedQueue = $queue; $this->maxIterations = (int) $maxIterations * $this->dedicatedQueue->getNumberOfTasksToReceive(); $this->logContext['QueueName'] = $queue->getName(); return $this; } /** * @return bool */ private function isRunning() { if ($this->handleSignals) { pcntl_signal_dispatch(); } if ($this->shutdown) { return false; } if ($this->maxIterations > 0) { return $this->iterations < $this->maxIterations; } return true; } /** * Register signal handlers that a worker should respond to. * * TERM/INT/QUIT: Shutdown after the current job is finished then exit. * USR2: Pause worker, no new jobs will be processed but the current one will be finished. * CONT: Resume worker. */ private function registerSigHandlers() { if (!function_exists('pcntl_signal')) { $this->logError('Please make sure that "pcntl" is enabled.', $this->logContext); throw new \RuntimeException('Please make sure that "pcntl" is enabled.'); } declare(ticks=1); pcntl_signal(SIGTERM, [$this, 'shutdown']); pcntl_signal(SIGINT, [$this, 'shutdown']); pcntl_signal(SIGQUIT, [$this, 'shutdown']); pcntl_signal(SIGUSR2, [$this, 'pauseProcessing']); pcntl_signal(SIGCONT, [$this, 'unPauseProcessing']); $this->logDebug('Finished setting up signal handlers', $this->logContext); } public function shutdown() { $this->logDebug('TERM/INT/QUIT received; shutting down gracefully...', $this->logContext); $this->shutdown = true; } public function pauseProcessing() { $this->logDebug('USR2 received; pausing task processing...', $this->logContext); $this->paused = true; } public function unPauseProcessing() { $this->logDebug('CONT received; resuming task processing...', $this->logContext); $this->paused = false; } /** * Calculate the sleeping time dynamically in case of no task to work on. * * @return int (sec) */ private function getWaitInterval() { if ($this->dedicatedQueue instanceof QueueInterface) { $waitTime = $this->iterationsWithOutTask * self::WAIT_INTERVAL; return min($waitTime, self::MAX_SLEEPING_TIME_FOR_DEDICATED_QUEUE); } else { return (int) $this->queueService->getWaitTime(); } } }