Skip to content

Commit

Permalink
Automatically retry queries on database connections that have 'gone a…
Browse files Browse the repository at this point in the history
…way'.
  • Loading branch information
taylorotwell committed Aug 4, 2014
1 parent dcfdb61 commit 143f7a9
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 16 deletions.
135 changes: 124 additions & 11 deletions src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ class Connection implements ConnectionInterface {
*/
protected $readPdo;

/**
* The reconnector instance for the connection.
*
* @var callable
*/
protected $reconnector;

/**
* The query grammar implementation.
*
Expand Down Expand Up @@ -537,8 +544,46 @@ public function pretend(Closure $callback)
*/
protected function run($query, $bindings, Closure $callback)
{
$this->reconnectIfMissingConnection();

$start = microtime(true);

// Here we will run this query. If an exception occurs we'll determine if it was
// caused by a connection that has been lost. If that is the cause, we'll try
// to re-establish connection and re-run the query with a fresh connection.
try
{
$result = $this->runQueryCallback($query, $bindings, $callback);
}
catch (QueryException $e)
{
$result = $this->tryAgainIfCausedByLostConnection(
$e, $query, $bindings, $callback
);
}

// Once we have run the query we will calculate the time that it took to run and
// then log the query, bindings, and execution time so we will report them on
// the event that the developer needs them. We'll log time in milliseconds.
$time = $this->getElapsedTime($start);

$this->logQuery($query, $bindings, $time);

return $result;
}

/**
* Run a SQL statement.
*
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws QueryException
*/
protected function runQueryCallback($query, $bindings, Closure $callback)
{
// To execute the statement, we'll simply call the callback, which will actually
// run the SQL against the PDO connection. Then we can calculate the time it
// took to execute and log the query SQL, bindings and time in our memory.
Expand All @@ -552,17 +597,72 @@ protected function run($query, $bindings, Closure $callback)
// lot more helpful to the developer instead of just the database's errors.
catch (\Exception $e)
{
throw new QueryException($query, $this->prepareBindings($bindings), $e);
throw new QueryException(
$query, $this->prepareBindings($bindings), $e
);
}

// Once we have run the query we will calculate the time that it took to run and
// then log the query, bindings, and execution time so we will report them on
// the event that the developer needs them. We'll log time in milliseconds.
$time = $this->getElapsedTime($start);
return $result;
}

$this->logQuery($query, $bindings, $time);
/**
* Handle a query exception that occurred during query execution.
*
* @param \Illuminate\Database\QueryException $e
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*/
protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, $callback)
{
if ($this->causedByLostConnection($e))
{
$this->reconnect();

return $result;
return $this->runQueryCallback($query, $bindings, $callback);
}

throw $e;
}

/**
* Determine if the given exception was caused by a lost connection.
*
* @param \Illuminate\Database\QueryException
* @return bool
*/
protected function causedByLostConnection(QueryException $e)
{
return str_contains($e->getPrevious()->getMessage(), 'server has gone away');
}

/**
* Reconnect to the database.
*
* @return void
*/
public function reconnect()
{
if (is_callable($this->reconnector))
{
return call_user_func($this->reconnector, $this);
}

throw new \LogicException("Lost connection and no reconnector available.");
}

/**
* Reconnect to the database if a PDO connection is missing.
*
* @return void
*/
protected function reconnectIfMissingConnection()
{
if (is_null($this->getPdo()) || is_null($this->getReadPdo()))
{
$this->reconnect();
}
}

/**
Expand Down Expand Up @@ -687,10 +787,10 @@ public function getReadPdo()
/**
* Set the PDO connection.
*
* @param \PDO $pdo
* @param \PDO|null $pdo
* @return $this
*/
public function setPdo(PDO $pdo)
public function setPdo($pdo)
{
$this->pdo = $pdo;

Expand All @@ -700,16 +800,29 @@ public function setPdo(PDO $pdo)
/**
* Set the PDO connection used for reading.
*
* @param \PDO $pdo
* @param \PDO|null $pdo
* @return $this
*/
public function setReadPdo(PDO $pdo)
public function setReadPdo($pdo)
{
$this->readPdo = $pdo;

return $this;
}

/**
* Set the reconnect instance on the connection.
*
* @param callable $reconnector
* @return $this
*/
public function setReconnector(callable $reconnector)
{
$this->reconnector = $reconnector;

return $this;
}

/**
* Get the database connection name.
*
Expand Down
21 changes: 17 additions & 4 deletions src/Illuminate/Database/DatabaseManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ public function connection($name = null)
*/
public function reconnect($name = null)
{
$name = $name ?: $this->getDefaultConnection();
$this->disconnect($name = $name ?: $this->getDefaultConnection());

$this->disconnect($name);
$fresh = $this->makeConnection($name);

return $this->connection($name);
return $this->connections[$name]
->setPdo($fresh->getPdo())
->setReadPdo($fresh->getReadPdo());
}

/**
Expand All @@ -93,7 +95,10 @@ public function disconnect($name = null)
{
$name = $name ?: $this->getDefaultConnection();

unset($this->connections[$name]);
if (isset($this->connections[$name]))
{
$this->connections[$name]->setPdo(null)->setReadPdo(null);
}
}

/**
Expand Down Expand Up @@ -160,6 +165,14 @@ protected function prepare(Connection $connection)
return $app['paginator'];
});

// Here we'll set a reconnector callback. This reconnector can be any callable
// so we will set a Closure to reconnect from this manager with the name of
// the connection, which will allow us to reconnect from the connections.
$connection->setReconnector(function($connection)
{
$this->reconnect($connection->getName());
});

return $connection;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Illuminate/Database/QueryException.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class QueryException extends PDOException {
*/
public function __construct($sql, array $bindings, $previous)
{
parent::__construct('', 0, $previous);

$this->sql = $sql;
$this->bindings = $bindings;
$this->previous = $previous;
Expand Down
3 changes: 2 additions & 1 deletion src/Illuminate/Foundation/changes.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
{"message": "Added 'contains' method to base Collection.", "backport": null},
{"message": "Allow 'where' route constraints to be passed in array definition of Route.", "backport": null},
{"message": "Properly support Route 'where' constraints on a Route group.", "backport": null},
{"message": "When 'increment' or 'decrement' is called on a single Model instance, the local attribute value is updated as well.", "backport": null}
{"message": "When 'increment' or 'decrement' is called on a single Model instance, the local attribute value is updated as well.", "backport": null},
{"message": "Automatically retry queries on database connections that have 'gone away'.", "backport": null}
],
"4.1.*": [
{"message": "Added new SSH task runner tools.", "backport": null},
Expand Down

0 comments on commit 143f7a9

Please sign in to comment.