tao-test/app/tao/models/classes/taskQueue/QueueDispatcher.php

460 lines
12 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
namespace oat\tao\model\taskQueue;
use oat\generis\model\OntologyAwareTrait;
use oat\oatbox\service\ConfigurableService;
use oat\oatbox\log\LoggerAwareTrait;
use oat\tao\model\taskQueue\Queue\TaskSelector\SelectorStrategyInterface;
use oat\tao\model\taskQueue\Queue\TaskSelector\WeightStrategy;
use oat\tao\model\taskQueue\Task\CallbackTask;
use oat\tao\model\taskQueue\Task\CallbackTaskInterface;
use oat\tao\model\taskQueue\Task\QueueAssociableInterface;
use oat\tao\model\taskQueue\Task\TaskInterface;
use oat\tao\model\taskQueue\TaskLog\TaskLogAwareInterface;
use oat\tao\model\taskQueue\Worker\OneTimeWorker;
/**
* @author Gyula Szucs <gyula@taotesting.com>
*/
class QueueDispatcher extends ConfigurableService implements QueueDispatcherInterface
{
use LoggerAwareTrait;
use OntologyAwareTrait;
/**
* @var TaskLogInterface
*/
private $taskLog;
/** @var string */
private $owner;
/** @var SelectorStrategyInterface */
private $selectorStrategy;
private $propagated = false;
/**
* QueueDispatcher constructor.
*
* @param array $options
* @throws \common_exception_Error
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->assertQueues();
$this->assertTasks();
if (!$this->hasOption(self::OPTION_TASK_SELECTOR_STRATEGY) || empty($this->getOption(self::OPTION_TASK_SELECTOR_STRATEGY))) {
// setting default strategy
$this->selectorStrategy = new WeightStrategy();
} else {
// using the strategy set in the options
if (!is_a($this->getOption(self::OPTION_TASK_SELECTOR_STRATEGY), SelectorStrategyInterface::class)) {
throw new \common_exception_Error('Task selector must implement ' . SelectorStrategyInterface::class);
}
$this->selectorStrategy = $this->getOption(self::OPTION_TASK_SELECTOR_STRATEGY);
}
if (!$this->hasOption(self::OPTION_TASK_LOG) || empty($this->getOption(self::OPTION_TASK_LOG))) {
throw new \common_exception_Error('Task Log service needs to be set.');
}
}
/**
* @param TaskInterface $task
* @return QueueInterface
*/
protected function getQueueForTask(TaskInterface $task)
{
$action = $task instanceof CallbackTaskInterface && is_object($task->getCallable()) ? $task->getCallable() : $task;
// getting queue name using the implemented getter function
if ($action instanceof QueueAssociableInterface && ($queueName = $action->getQueueName($task->getParameters()))) {
return $this->getQueue($queueName);
}
// getting the queue name based on the linked tasks configuration
$className = get_class($action);
if (array_key_exists($className, $this->getLinkedTasks())) {
$queueName = $this->getLinkedTasks()[$className];
return $this->getQueue($queueName);
}
// if we still don't have a queue, let's use the default one
return $this->getDefaultQueue();
}
/**
* @inheritdoc
*/
public function getQueueNames()
{
return array_map(function (QueueInterface $queue) {
return $queue->getName();
}, $this->getOption(self::OPTION_QUEUES));
}
/**
* @inheritdoc
*/
public function setQueues(array $queues)
{
$this->propagated = false;
$this->setOption(self::OPTION_QUEUES, $queues);
return $this;
}
/**
* @inheritdoc
* @throws \LogicException
*/
public function addQueue(QueueInterface $queue)
{
if ($this->hasQueue($queue->getName())) {
throw new \LogicException('Queue "' . $queue . '" is already registered.');
}
$this->propagated = false;
$queues = $this->getQueues();
$queues[] = $queue;
$this->setOption(self::OPTION_QUEUES, $queues);
return $this;
}
/**
* @inheritdoc
*/
public function hasQueue($queueName)
{
return in_array($queueName, $this->getQueueNames());
}
/**
* @inheritdoc
*/
public function getQueue($queueName)
{
$foundQueue = array_filter($this->getQueues(), function (QueueInterface $queue) use ($queueName) {
return $queue->getName() === $queueName;
});
if (count($foundQueue) === 1) {
return reset($foundQueue);
}
throw new \InvalidArgumentException('Queue "' . $queueName . '" does not exist.');
}
/**
* @return QueueInterface[]
*/
public function getQueues()
{
if (!$this->propagated) {
$queues = (array) $this->getOption(self::OPTION_QUEUES);
// propagate the services for the queues first
array_walk($queues, function (QueueInterface $queue) {
$this->propagateServices($queue);
});
$this->propagated = true;
}
return $this->getOption(self::OPTION_QUEUES);
}
/**
* @inheritdoc
*/
public function linkTaskToQueue($taskName, $queueName)
{
if (is_object($taskName)) {
$taskName = get_class($taskName);
}
if (!$this->hasQueue($queueName)) {
throw new \LogicException('Task "' . $taskName . '" cannot be added to "' . $queueName . '". Queue is not registered.');
}
$tasks = $this->getLinkedTasks();
$tasks[ (string) $taskName ] = $queueName;
$this->setOption(self::OPTION_TASK_TO_QUEUE_ASSOCIATIONS, $tasks);
return $this;
}
/**
* @inheritdoc
*/
public function getLinkedTasks()
{
return (array) $this->getOption(self::OPTION_TASK_TO_QUEUE_ASSOCIATIONS);
}
/**
* Return the first queue as a default one.
* Maybe, later we need other logic the determine the default queue.
*
* @return QueueInterface
*/
public function getDefaultQueue()
{
return $this->hasOption(self::OPTION_DEFAULT_QUEUE) && $this->getOption(self::OPTION_DEFAULT_QUEUE)
? $this->getQueue($this->getOption(self::OPTION_DEFAULT_QUEUE))
: $this->getFirstQueue();
}
/**
* Return the first queue from the array.
*
* @return QueueInterface
*/
protected function getFirstQueue()
{
$queues = $this->getQueues();
return reset($queues);
}
/**
* @inheritdoc
*/
public function setTaskSelector(SelectorStrategyInterface $selectorStrategy)
{
$this->setOption(self::OPTION_TASK_SELECTOR_STRATEGY, $selectorStrategy);
return $this;
}
/**
* Initialize queue.
*
* @return void
*/
public function initialize()
{
foreach ($this->getQueues() as $queue) {
$queue->initialize();
}
}
/**
* @inheritdoc
*/
public function createTask(callable $callable, array $parameters = [], $label = null, TaskInterface $parent = null, $masterStatus = false)
{
$id = \common_Utils::getNewUri();
$owner = $parent ? $parent->getOwner() : $this->getOwner();
$callbackTask = new CallbackTask($id, $owner);
$callbackTask->setCallable($callable)
->setParameter($parameters);
if ($parent) {
$callbackTask->setParentId($parent->getId());
}
$callbackTask->setMasterStatus($masterStatus);
if ($this->enqueue($callbackTask, $label)) {
$callbackTask->markAsEnqueued();
}
return $callbackTask;
}
/**
* @param string $owner
*/
public function setOwner($owner)
{
$this->owner = $owner;
}
/**
* @return string
* @throws \common_exception_Error
*/
public function getOwner()
{
if (is_null($this->owner)) {
return \common_session_SessionManager::getSession()->getUser()->getIdentifier();
}
return $this->owner;
}
/**
* @param TaskInterface $task
* @param null|string $label
* @return bool
*/
public function enqueue(TaskInterface $task, $label = null)
{
$queue = $this->getQueueForTask($task);
$isEnqueued = $queue->enqueue($task, $label);
// if we need to run the task straightaway, then run a worker on-the-fly for one round.
if ($isEnqueued && $queue->isSync()) {
$oneTimeWorker = new OneTimeWorker($queue, $this->getTaskLog());
$this->propagate($oneTimeWorker);
$oneTimeWorker->run();
}
return $isEnqueued;
}
/**
* Receive a task from a specified queue or from a queue selected by a predefined strategy
*
* @inheritdoc
*/
public function dequeue()
{
// if there is only one queue defined, let's use that
if (count($this->getQueues()) === 1) {
return $this->getFirstQueue()->dequeue();
}
// default: getting a task using the current task selector strategy
return $this->selectorStrategy->pickNextTask($this->getQueues());
}
/**
* @inheritdoc
*/
public function acknowledge(TaskInterface $task)
{
$this->getQueueForTask($task)->acknowledge($task);
}
/**
* Count of messages in all queues.
*
* @return int
*/
public function count()
{
$counts = array_map(function (QueueInterface $queue) {
return $queue->count();
}, $this->getQueues());
return array_sum($counts);
}
/**
* @inheritdoc
*/
public function isSync()
{
foreach ($this->getQueues() as $queue) {
if (!$queue->isSync()) {
return false;
}
}
return true;
}
public function getWaitTime()
{
return $this->selectorStrategy->getWaitTime();
}
/**
* @return TaskLogInterface
*/
protected function getTaskLog()
{
if (is_null($this->taskLog)) {
$this->taskLog = $this->getServiceLocator()->get($this->getOption(self::OPTION_TASK_LOG));
}
return $this->taskLog;
}
/**
* @param QueueInterface $queue
* @return QueueInterface
*/
protected function propagateServices(QueueInterface $queue)
{
$this->propagate($queue);
if ($queue instanceof TaskLogAwareInterface) {
$queue->setTaskLog($this->getTaskLog());
}
return $queue;
}
/**
* @throws \InvalidArgumentException
*/
private function assertQueues()
{
if (!$this->hasOption(self::OPTION_QUEUES) || empty($this->getOption(self::OPTION_QUEUES))) {
throw new \InvalidArgumentException("Queues needs to be set.");
}
if (count($this->getOption(self::OPTION_QUEUES)) === 1) {
return;
}
if (count($this->getOption(self::OPTION_QUEUES)) != count(array_unique($this->getOption(self::OPTION_QUEUES)))) {
throw new \InvalidArgumentException('There are duplicated Queue names. Please check the values of "' . self::OPTION_QUEUES . '" in your queue dispatcher settings.');
}
}
/**
* @throws \InvalidArgumentException
*/
private function assertTasks()
{
if (empty($this->getLinkedTasks())) {
return;
}
// check if every task is linked to a registered queue
$notRegisteredQueues = array_diff(array_values($this->getLinkedTasks()), $this->getQueueNames());
if (count($notRegisteredQueues)) {
throw new \LogicException('Found not registered queue(s) linked to task(s): "' . implode('", "', $notRegisteredQueues) . '". Please check the values of "' . self::OPTION_TASK_TO_QUEUE_ASSOCIATIONS . '" in your queue dispatcher settings.');
}
}
}