Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle deadlock exception in laravel

I got this error in my Laravel application when I execute some insert/update query by using Laravel Eloquent

SQLSTATE[40001]: Serialization failure: 1213 Deadlock found

How can I re-execute the query until it done?

like image 237
Saeed Vaziry Avatar asked Jul 01 '17 10:07

Saeed Vaziry


People also ask

How can we avoid deadlock in laravel?

runQueryCallback runs all the db executions(selects, inserts, updates). So,if it catches QueryException,as it does,and check if it is error4001 it repeat the command that would theoretically prevent deadlock.

What is deadlock in mysql?

A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both transactions are waiting for a resource to become available, neither ever release the locks it holds.


1 Answers

This solution works for Laravel 5.1, but I believe that it can be used for new versions of the framework with minor changes.

Following code assumes that the default database connection name is "mysql". Check it in config/database.php field default.

Create new class extending Illuminate\Database\MySqlConnection:

namespace App\Helpers\MySQL;

use Closure;
use Exception;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\QueryException;
use Log;
use PDOException;

/**
 * Class DeadlockReadyMySqlConnection
 *
 * @package App\Helpers
 */
class DeadlockReadyMySqlConnection extends MySqlConnection
{
    /**
     * Error code of deadlock exception
     */
    const DEADLOCK_ERROR_CODE = 40001;

    /**
     * Number of attempts to retry
     */
    const ATTEMPTS_COUNT = 3;

    /**
     * Run a SQL statement.
     *
     * @param  string    $query
     * @param  array     $bindings
     * @param  \Closure  $callback
     * @return mixed
     *
     * @throws \Illuminate\Database\QueryException
     */
    protected function runQueryCallback($query, $bindings, Closure $callback)
    {
        $attempts_count = self::ATTEMPTS_COUNT;

        for ($attempt = 1; $attempt <= $attempts_count; $attempt++) {
            try {
                return $callback($this, $query, $bindings);
            } catch (Exception $e) {
                if (((int)$e->getCode() !== self::DEADLOCK_ERROR_CODE) || ($attempt >= $attempts_count)) {
                    throw new QueryException(
                        $query, $this->prepareBindings($bindings), $e
                    );
                } else {
                    $sql = str_replace_array('\?', $this->prepareBindings($bindings), $query);
                    Log::warning("Transaction has been restarted. Attempt {$attempt}/{$attempts_count}. SQL: {$sql}");
                }
            }
        }

    }
}

Extend basic connection factory Illuminate\Database\Connectors\ConnectionFactory:

namespace App\Helpers\MySQL;

use Config;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use InvalidArgumentException;
use PDO;

/**
 * Class YourAppConnectionFactory
 *
 * @package App\Helpers\MySQL
 */
class YourAppConnectionFactory extends ConnectionFactory
{
    /**
     * Create a new connection instance.
     *
     * @param  string   $driver
     * @param  PDO     $connection
     * @param  string   $database
     * @param  string   $prefix
     * @param  array    $config
     * @return \Illuminate\Database\Connection
     *
     * @throws InvalidArgumentException
     */
    protected function createConnection($driver, PDO $connection, $database, $prefix = '', array $config = [])
    {
        if ($this->container->bound($key = "db.connection.{$driver}")) {
            return $this->container->make($key, [$connection, $database, $prefix, $config]);
        }

        switch ($driver) {
            case 'mysql':
                if ($config['database'] === Config::get('database.connections.mysql.database')) {
                    return new DeadlockReadyMySqlConnection($connection, $database, $prefix, $config);
                } else {
                    return new MySqlConnection($connection, $database, $prefix, $config);
                }
            case 'pgsql':
                return new PostgresConnection($connection, $database, $prefix, $config);
            case 'sqlite':
                return new SQLiteConnection($connection, $database, $prefix, $config);
            case 'sqlsrv':
                return new SqlServerConnection($connection, $database, $prefix, $config);
        }

        throw new InvalidArgumentException("Unsupported driver [$driver]");
    }
}

And now we should replace standard framework's DB connection factory in Providers/AppServiceProvider.php (or create new service provider)

public function register()
{
    $this->app->singleton('db.factory', function ($app) {
        return new YourAppConnectionFactory($app);
    });
}

That's it! Now all queries failed on deadlocks should be restarted.

like image 66
Bushikot Avatar answered Sep 17 '22 18:09

Bushikot