258 lines
8.1 KiB
PHP
258 lines
8.1 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) 2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace oat\taoTaskQueue\model\QueueBroker;
|
||
|
|
||
|
use common_persistence_Manager;
|
||
|
use common_persistence_SqlPersistence;
|
||
|
use Doctrine\DBAL\Connection;
|
||
|
use Doctrine\DBAL\ParameterType;
|
||
|
use Doctrine\DBAL\Query\QueryBuilder;
|
||
|
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||
|
use Exception;
|
||
|
use InvalidArgumentException;
|
||
|
use oat\generis\Helper\UuidPrimaryKeyTrait;
|
||
|
use oat\tao\model\taskQueue\Queue\Broker\AbstractQueueBroker;
|
||
|
use oat\tao\model\taskQueue\Task\TaskInterface;
|
||
|
use oat\taoTaskQueue\model\QueueBroker\storage\NewSqlSchema;
|
||
|
use PDO;
|
||
|
use Throwable;
|
||
|
|
||
|
/**
|
||
|
* Storing messages/tasks in newSQl DB.
|
||
|
*/
|
||
|
class NewSqlQueueBroker extends AbstractQueueBroker
|
||
|
{
|
||
|
public const ID ='newsql';
|
||
|
|
||
|
use UuidPrimaryKeyTrait;
|
||
|
|
||
|
/** @var string */
|
||
|
private $persistenceId;
|
||
|
|
||
|
/** @var common_persistence_SqlPersistence */
|
||
|
protected $persistence;
|
||
|
|
||
|
public function __construct(string $persistenceId, int $receiveTasks = 1)
|
||
|
{
|
||
|
parent::__construct($receiveTasks);
|
||
|
|
||
|
if (empty($persistenceId)) {
|
||
|
throw new InvalidArgumentException("Persistence id needs to be set for " . __CLASS__);
|
||
|
}
|
||
|
|
||
|
$this->persistenceId = $persistenceId;
|
||
|
}
|
||
|
|
||
|
public function __toPhpCode()
|
||
|
{
|
||
|
return 'new ' . get_called_class() . '('
|
||
|
. \common_Utils::toHumanReadablePhpString($this->persistenceId)
|
||
|
. ', '
|
||
|
. \common_Utils::toHumanReadablePhpString($this->getNumberOfTasksToReceive())
|
||
|
. ')';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Note: this method can be run multiple times because only the migrate queries
|
||
|
* (result of getMigrateSchemaSql) will be run.
|
||
|
*
|
||
|
* @inheritdoc
|
||
|
*/
|
||
|
public function createQueue(): void
|
||
|
{
|
||
|
$persistence = $this->getPersistence();
|
||
|
/** @var AbstractSchemaManager $schemaManager */
|
||
|
$schemaManager = $persistence->getDriver()->getSchemaManager();
|
||
|
$schema = $schemaManager->createSchema();
|
||
|
$fromSchema = clone $schema;
|
||
|
try {
|
||
|
$schema->dropTable($this->getTableName());
|
||
|
} catch (Throwable $exception) {
|
||
|
$this->logDebug('Schema of ' . $this->getTableName() . ' table already up to date.');
|
||
|
}
|
||
|
// Create the table
|
||
|
$schema = $this->getSchemaProvider()
|
||
|
->setQueueName($this->getQueueName())
|
||
|
->getSchema($schema, $this->getTableName());
|
||
|
$queries = $persistence->getPlatForm()->getMigrateSchemaSql($fromSchema, $schema);
|
||
|
foreach ($queries as $query) {
|
||
|
$persistence->exec($query);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Insert a new task into the queue table.
|
||
|
*/
|
||
|
public function push(TaskInterface $task): bool
|
||
|
{
|
||
|
return (bool)$this->getPersistence()->insert($this->getTableName(), [
|
||
|
'id' => $this->getUniquePrimaryKey(),
|
||
|
'message' => $this->serializeTask($task),
|
||
|
'created_at' => $this->getPersistence()->getPlatForm()->getNowExpression(),
|
||
|
'visible' => true,
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
public function delete(TaskInterface $task): void
|
||
|
{
|
||
|
$this->doDelete($task->getMetadata('NewSqlMessageId'), [
|
||
|
'InternalMessageId' => $task->getId(),
|
||
|
'NewSqlMessageId' => $task->getMetadata('NewSqlMessageId')
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
public function count(): int
|
||
|
{
|
||
|
try {
|
||
|
return (int)$this->getQueryBuilder()
|
||
|
->select('COUNT(id)')
|
||
|
->from($this->getTableName())
|
||
|
->andWhere('visible = :visible')
|
||
|
->setParameter('visible', true, ParameterType::BOOLEAN)
|
||
|
->execute()
|
||
|
->fetchColumn();
|
||
|
} catch (Exception $e) {
|
||
|
$this->logError('Counting tasks failed with MSG: ' . $e->getMessage());
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* @inheritDoc
|
||
|
*/
|
||
|
protected function doPop(): void
|
||
|
{
|
||
|
$this->getPersistence()->getPlatform()->beginTransaction();
|
||
|
|
||
|
$logContext = $this->getLogContext();
|
||
|
|
||
|
try {
|
||
|
$dbResult = $this->fetchVisibleMessages();
|
||
|
|
||
|
if ($dbResult) {
|
||
|
// set the received messages to invisible for other workers
|
||
|
$this->changeMessagesVisibility($dbResult);
|
||
|
|
||
|
$this->processMessages($dbResult, $logContext);
|
||
|
} else {
|
||
|
$this->logDebug('No task in the queue.', $logContext);
|
||
|
}
|
||
|
|
||
|
$this->getPersistence()->getPlatform()->commit();
|
||
|
} catch (Exception $e) {
|
||
|
$this->getPersistence()->getPlatform()->rollBack();
|
||
|
$this->logError('Popping tasks failed with MSG: ' . $e->getMessage(), $logContext);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected function doDelete($id, array $logContext = []): void
|
||
|
{
|
||
|
try {
|
||
|
$this->getQueryBuilder()
|
||
|
->delete($this->getTableName())
|
||
|
->where('id = :id')
|
||
|
->andWhere('visible = :visible')
|
||
|
->setParameter('id', $id)
|
||
|
->setParameter('visible', false, ParameterType::BOOLEAN)
|
||
|
->execute();
|
||
|
} catch (Exception $e) {
|
||
|
$this->logError('Deleting task failed with MSG: ' . $e->getMessage(), $logContext);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function getSchemaProvider(): NewSqlSchema
|
||
|
{
|
||
|
return $this->getServiceLocator()->get(NewSqlSchema::class);
|
||
|
}
|
||
|
|
||
|
private function getQueryBuilder(): QueryBuilder
|
||
|
{
|
||
|
return $this->getPersistence()->getPlatform()->getQueryBuilder();
|
||
|
}
|
||
|
|
||
|
private function getPersistence(): ?common_persistence_SqlPersistence
|
||
|
{
|
||
|
if (is_null($this->persistence)) {
|
||
|
$this->persistence = $this->getServiceLocator()
|
||
|
->get(common_persistence_Manager::SERVICE_ID)
|
||
|
->getPersistenceById($this->persistenceId);
|
||
|
}
|
||
|
|
||
|
return $this->persistence;
|
||
|
}
|
||
|
|
||
|
private function getTableName(): string
|
||
|
{
|
||
|
return strtolower($this->getQueueNameWithPrefix());
|
||
|
}
|
||
|
|
||
|
private function changeMessagesVisibility(array $dbResult): void
|
||
|
{
|
||
|
$qb = $this->getQueryBuilder()
|
||
|
->update($this->getTableName())
|
||
|
->set('visible', ':visible')
|
||
|
->where('id IN (:ids)')
|
||
|
->setParameter('visible', false, ParameterType::BOOLEAN)
|
||
|
->setParameter('ids', array_column($dbResult, 'id'), Connection::PARAM_STR_ARRAY);
|
||
|
|
||
|
$qb->execute();
|
||
|
}
|
||
|
|
||
|
private function processMessages(array $dbResult, array $logContext): void
|
||
|
{
|
||
|
foreach ($dbResult as $row) {
|
||
|
if ($task = $this->unserializeTask($row['message'], $row['id'], $logContext)) {
|
||
|
$task->setMetadata('NewSqlMessageId', $row['id']);
|
||
|
$this->pushPreFetchedMessage($task);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function fetchVisibleMessages(): array
|
||
|
{
|
||
|
$qb = $this->getQueryBuilder()
|
||
|
->select('id, message')
|
||
|
->from($this->getTableName())
|
||
|
->where('visible = :visible')
|
||
|
->orderBy('created_at')
|
||
|
->setMaxResults($this->getNumberOfTasksToReceive());
|
||
|
|
||
|
/**
|
||
|
* SELECT ... FOR UPDATE is used for locking
|
||
|
*/
|
||
|
$sql = $qb->getSQL() . ' ' . $this->getPersistence()->getPlatForm()->getWriteLockSQL();
|
||
|
|
||
|
return $this->getPersistence()->query($sql, ['visible' => true])->fetchAll(PDO::FETCH_ASSOC);
|
||
|
}
|
||
|
|
||
|
private function getLogContext(): array
|
||
|
{
|
||
|
return [
|
||
|
'Queue' => $this->getQueueNameWithPrefix()
|
||
|
];
|
||
|
}
|
||
|
}
|