File "TransactionalMutex.php"
Full path: /home/webcknlt/admissiontell.com/wp-content/plugins/vibes/includes/libraries/lock/mutex/TransactionalMutex.php
File
size: 5.07 B (5.07 KB bytes)
MIME-type: text/x-php
Charset: utf-8
Download Open Edit Advanced Editor &nnbsp; Back
<?php
declare(strict_types=1);
namespace malkusch\lock\mutex;
use Exception;
use InvalidArgumentException;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\util\Loop;
use PDO;
use PDOException;
/**
* Serialization is delegated to the DBS.
*
* The critical code is executed within a transaction. The DBS will decide
* which parts of that code need to be locked (if at all).
*
* A failing transaction will be replayed.
*
* @author Markus Malkusch <markus@malkusch.de>
* @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations
* @license WTFPL
*/
class TransactionalMutex extends Mutex
{
/**
* @var \PDO $pdo The PDO.
*/
private $pdo;
/**
* @var Loop The loop.
*/
private $loop;
/**
* Sets the PDO.
*
* The PDO object MUST be configured with PDO::ATTR_ERRMODE
* to throw exceptions on errors.
*
* As this implementation spans a transaction over a unit of work,
* PDO::ATTR_AUTOCOMMIT SHOULD not be enabled.
*
* @param \PDO $pdo The PDO.
* @param int $timeout The timeout in seconds, default is 3.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(\PDO $pdo, int $timeout = 3)
{
if ($pdo->getAttribute(\PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) {
throw new InvalidArgumentException('The pdo must have PDO::ERRMODE_EXCEPTION set.');
}
self::checkAutocommit($pdo);
$this->pdo = $pdo;
$this->loop = new Loop($timeout);
}
/**
* Checks that the AUTOCOMMIT mode is turned off.
*
* @param \PDO $pdo PDO
*/
private static function checkAutocommit(\PDO $pdo): void
{
$vendor = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
// MySQL turns autocommit off during a transaction.
if ($vendor == 'mysql') {
return;
}
try {
if ($pdo->getAttribute(\PDO::ATTR_AUTOCOMMIT)) {
throw new InvalidArgumentException('PDO::ATTR_AUTOCOMMIT should be disabled.');
}
} catch (PDOException $e) {
/*
* Ignore this, as some drivers would throw an exception for an
* unsupported attribute (e.g. Postgres).
*/
}
}
/**
* Executes the critical code within a transaction.
*
* It's up to the user to set the correct transaction isolation level.
* However if the transaction fails, the code will be executed again in a
* new transaction. Therefore the code must not have any side effects
* besides SQL statements. Also the isolation level should be conserved for
* the repeated transaction.
*
* A transaction is considered as failed if a PDOException or an exception
* which has a PDOException as any previous exception was raised.
*
* If the code throws any other exception, the transaction is rolled back
* and won't be replayed.
*
* @param callable $code The synchronized execution block.
* @throws \Exception The execution block threw an exception.
* @throws LockAcquireException The transaction was not commited.
* @return mixed The return value of the execution block.
* @SuppressWarnings(PHPMD)
*/
public function synchronized(callable $code)
{
return $this->loop->execute(function () use ($code) {
try {
// BEGIN
$this->pdo->beginTransaction();
} catch (PDOException $e) {
throw new LockAcquireException('Could not begin transaction.', 0, $e);
}
try {
// Unit of work
$result = $code();
$this->pdo->commit();
$this->loop->end();
return $result;
} catch (Exception $e) {
$this->rollBack($e);
if (self::hasPDOException($e)) {
return null; // Replay
} else {
throw $e;
}
}
});
}
/**
* Checks if an exception or any of its previous exceptions is a PDOException.
*
* @param \Throwable $exception The exception.
* @return bool True if there's a PDOException.
*/
private static function hasPDOException(\Throwable $exception)
{
if ($exception instanceof PDOException) {
return true;
}
if ($exception->getPrevious() === null) {
return false;
}
return self::hasPDOException($exception->getPrevious());
}
/**
* Rolls back a transaction.
*
* @param \Exception $exception The causing exception.
*
* @throws LockAcquireException The roll back failed.
*/
private function rollBack(\Exception $exception)
{
try {
$this->pdo->rollBack();
} catch (\PDOException $e2) {
throw new LockAcquireException(
"Could not roll back transaction: {$e2->getMessage()})",
0,
$exception
);
}
}
}