Commit c0ad1c08 authored by Tobias Schultze's avatar Tobias Schultze

implement method for retrying database queries/transactionse

it is best practice to implement retry logic for transactions that are aborted because of deadlocks or timeouts. this makes such method available inside the DBAL and also add detection for errors where retrying makes sense in the different database drivers
parent bfe166af
......@@ -32,6 +32,7 @@ use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Cache\ArrayStatement;
use Doctrine\DBAL\Cache\CacheException;
use Doctrine\DBAL\Driver\PingableConnection;
use Doctrine\DBAL\Exception\RetryableException;
/**
* A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
......@@ -1110,6 +1111,43 @@ class Connection implements DriverConnection
}
}
/**
* Executes a function and retries it in case of a temporary database error.
*
* The function gets passed this Connection instance as an (optional) parameter.
*
* The function is only re-executed for temporary database errors where retrying
* the failed transaction after a short delay usually resolves the problem. Such
* errors are for example deadlocks and lock wait timeouts. Internally the raised
* exception must extend RetryableException. Other exceptions like syntax errors
* or constraint violations will not cause the closure to be re-executed.
*
* @param Closure $func The function to execute that can be retried on failure.
* @param integer $maxRetries Maximum number of retries.
* @param integer $retryDelay Delay between retries in milliseconds to give the blocking
* transaction time to finish.
*
* @return mixed The return value of the closure
*
* @throws \Exception If an exception has been raised where retrying makes no sense
* or a RetryableException after max retries has been reached.
*/
public function retryable(Closure $func, $maxRetries = 3, $retryDelay = 100)
{
do {
try {
return $func($this);
} catch (RetryableException $e) {
if ($maxRetries > 0) {
$maxRetries--;
usleep($retryDelay * 1000);
} else {
throw $e;
}
}
} while (true);
}
/**
* Sets if nested transactions should use savepoints.
*
......
......@@ -45,6 +45,10 @@ abstract class AbstractMySQLDriver implements Driver, ExceptionConverterDriver,
public function convertException($message, DriverException $exception)
{
switch ($exception->getErrorCode()) {
case '1213':
return new Exception\DeadlockException($message, $exception);
case '1205':
return new Exception\LockWaitTimeoutException($message, $exception);
case '1050':
return new Exception\TableExistsException($message, $exception);
......
......@@ -46,6 +46,9 @@ abstract class AbstractPostgreSQLDriver implements Driver, ExceptionConverterDri
public function convertException($message, DriverException $exception)
{
switch ($exception->getSQLState()) {
case '40001':
case '40P01':
return new Exception\DeadlockException($message, $exception);
case '0A000':
// Foreign key constraint violations during a TRUNCATE operation
// are considered "feature not supported" in PostgreSQL.
......
......@@ -46,6 +46,8 @@ abstract class AbstractSQLAnywhereDriver implements Driver, ExceptionConverterDr
public function convertException($message, DriverException $exception)
{
switch ($exception->getErrorCode()) {
case '-306':
return new Exception\DeadlockException($message, $exception);
case '-100':
case '-103':
case '-832':
......
......@@ -40,6 +40,10 @@ abstract class AbstractSQLiteDriver implements Driver, ExceptionConverterDriver
*/
public function convertException($message, DriverException $exception)
{
if (strpos($exception->getMessage(), 'database is locked') !== false) {
return new Exception\LockWaitTimeoutException($message, $exception);
}
if (strpos($exception->getMessage(), 'must be unique') !== false ||
strpos($exception->getMessage(), 'is not unique') !== false ||
strpos($exception->getMessage(), 'are not unique') !== false ||
......
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\DBAL\Exception;
/**
* Exception for a deadlock error of a transaction detected in the driver.
*
* @author Tobias Schultze <http://tobion.de>
* @link www.doctrine-project.org
* @since 2.5
*/
class DeadlockException extends RetryableException
{
}
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\DBAL\Exception;
/**
* Exception for a lock wait timeout error of a transaction detected in the driver.
*
* @author Tobias Schultze <http://tobion.de>
* @link www.doctrine-project.org
* @since 2.5
*/
class LockWaitTimeoutException extends RetryableException
{
}
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\DBAL\Exception;
/**
* Base class for all exceptions where retrying the transaction makes sense.
*
* @author Tobias Schultze <http://tobion.de>
* @link www.doctrine-project.org
* @since 2.5
*/
abstract class RetryableException extends ServerException
{
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment