Added an error count to the database connection to ensure that the connection can be reset when occur too many exceptions. (#6085)

This commit is contained in:
李铭昕 2023-08-25 21:34:08 +08:00 committed by GitHub
parent 5d2d69a80b
commit d4f14fd945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 49 deletions

View File

@ -37,6 +37,7 @@
- [#5994](https://github.com/hyperf/hyperf/pull/5994) Added `events` of `crontab` lifecycle.
- [#6039](https://github.com/hyperf/hyperf/pull/6039) Support semantic crontab rules.
- [#6082](https://github.com/hyperf/hyperf/pull/6082) Added `hyperf/stdlib` component.
- [#6085](https://github.com/hyperf/hyperf/pull/6085) Added an error count to the database connection to ensure that the connection can be reset when occur too many exceptions.
## Optimized

View File

@ -146,6 +146,11 @@ class Connection implements ConnectionInterface
*/
protected static array $beforeExecutingCallbacks = [];
/**
* Error count for executing SQL.
*/
protected int $errorCount = 0;
/**
* Create a new database connection instance.
*
@ -967,6 +972,11 @@ class Connection implements ConnectionInterface
return static::$resolvers[$driver] ?? null;
}
public function getErrorCount(): int
{
return $this->errorCount;
}
/**
* Escape a string value for safe SQL embedding.
*/
@ -1115,11 +1125,9 @@ class Connection implements ConnectionInterface
/**
* Run a SQL statement.
*
* @param string $query
* @param array $bindings
* @throws QueryException
*/
protected function runQueryCallback($query, $bindings, Closure $callback)
protected function runQueryCallback(string $query, array $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
@ -1132,11 +1140,15 @@ class Connection implements ConnectionInterface
// message to include the bindings with SQL, which will make this exception a
// lot more helpful to the developer instead of just the database's errors.
catch (Exception $e) {
++$this->errorCount;
throw new QueryException(
$query,
$this->prepareBindings($bindings),
$e
);
} catch (Throwable $throwable) {
++$this->errorCount;
throw $throwable;
}
return $result;
@ -1153,13 +1165,9 @@ class Connection implements ConnectionInterface
/**
* Handle a query exception.
*
* @param Exception $e
* @param string $query
* @param array $bindings
*
* @throws Exception
*/
protected function handleQueryException($e, $query, $bindings, Closure $callback)
protected function handleQueryException(QueryException $e, string $query, array $bindings, Closure $callback)
{
if ($this->transactions >= 1) {
throw $e;
@ -1176,11 +1184,9 @@ class Connection implements ConnectionInterface
/**
* Handle a query exception that occurred during query execution.
*
* @param string $query
* @param array $bindings
* @throws QueryException
*/
protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback)
protected function tryAgainIfCausedByLostConnection(QueryException $e, string $query, array $bindings, Closure $callback)
{
if ($this->causedByLostConnection($e->getPrevious())) {
$this->reconnect();

View File

@ -109,6 +109,11 @@ class Connection extends BaseConnection implements ConnectionInterface, DbConnec
if ($this->connection instanceof \Hyperf\Database\Connection) {
// Reset $recordsModified property of connection to false before the connection release into the pool.
$this->connection->resetRecordsModified();
if ($this->connection->getErrorCount() > 100) {
// If the error count of connection is more than 100, we think it is a bad connection,
// So we'll reset it at the next time
$this->lastUseTime = 0.0;
}
}
if ($this->transactionLevel() > 0) {

View File

@ -11,11 +11,14 @@ declare(strict_types=1);
*/
namespace HyperfTest\DbConnection;
use Exception;
use Hyperf\Context\Context;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Exception\QueryException;
use Hyperf\DbConnection\Connection;
use Hyperf\DbConnection\Pool\PoolFactory;
use Hyperf\Support\Reflection\ClassInvoker;
use HyperfTest\DbConnection\Stubs\ConnectionStub;
use HyperfTest\DbConnection\Stubs\ContainerStub;
use HyperfTest\DbConnection\Stubs\PDOStub;
@ -117,17 +120,21 @@ class ConnectionTest extends TestCase
$pool = $container->get(PoolFactory::class)->getPool('default');
$config = $container->get(ConfigInterface::class)->get('databases.default');
$callables = [function ($connection) {
$connection->selectOne('SELECT 1;');
}, function ($connection) {
$connection->table('user')->leftJoin('user_ext', 'user.id', '=', 'user_ext.id')->get();
}];
$callables = [
function ($connection) {
$connection->selectOne('SELECT 1;');
}, function ($connection) {
$connection->table('user')->leftJoin('user_ext', 'user.id', '=', 'user_ext.id')->get();
},
];
$closes = [function ($connection) {
$connection->close();
}, function ($connection) {
$connection->reconnect();
}];
$closes = [
function ($connection) {
$connection->close();
}, function ($connection) {
$connection->reconnect();
},
];
foreach ($callables as $callable) {
foreach ($closes as $closure) {
@ -147,45 +154,83 @@ class ConnectionTest extends TestCase
{
$container = ContainerStub::mockReadWriteContainer();
parallel([function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
parallel([
function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
/** @var \Hyperf\Database\Connection $connection */
$connection = $resolver->connection();
$connection->statement('UPDATE hyperf.test SET name = 1 WHERE id = 1;');
/** @var \Hyperf\Database\Connection $connection */
$connection = $resolver->connection();
$connection->statement('UPDATE hyperf.test SET name = 1 WHERE id = 1;');
/** @var PDOStub $pdo */
$pdo = $connection->getPdo();
$this->assertSame('mysql:host=192.168.1.2;dbname=hyperf', $pdo->dsn);
$pdo = $connection->getReadPdo();
$this->assertSame('mysql:host=192.168.1.2;dbname=hyperf', $pdo->dsn);
}]);
/** @var PDOStub $pdo */
$pdo = $connection->getPdo();
$this->assertSame('mysql:host=192.168.1.2;dbname=hyperf', $pdo->dsn);
$pdo = $connection->getReadPdo();
$this->assertSame('mysql:host=192.168.1.2;dbname=hyperf', $pdo->dsn);
},
]);
parallel([function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
parallel([
function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
/** @var \Hyperf\Database\Connection $connection */
$connection = $resolver->connection();
/** @var \Hyperf\Database\Connection $connection */
$connection = $resolver->connection();
/** @var PDOStub $pdo */
$pdo = $connection->getPdo();
$this->assertSame('mysql:host=192.168.1.2;dbname=hyperf', $pdo->dsn);
$pdo = $connection->getReadPdo();
$this->assertSame('mysql:host=192.168.1.1;dbname=hyperf', $pdo->dsn);
}]);
/** @var PDOStub $pdo */
$pdo = $connection->getPdo();
$this->assertSame('mysql:host=192.168.1.2;dbname=hyperf', $pdo->dsn);
$pdo = $connection->getReadPdo();
$this->assertSame('mysql:host=192.168.1.1;dbname=hyperf', $pdo->dsn);
},
]);
}
public function testDbConnectionUseInDefer()
{
$container = ContainerStub::mockReadWriteContainer();
parallel([function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
parallel([
function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
defer(function () {
$this->assertFalse(Context::has('database.connection.default'));
});
$resolver->connection();
}]);
defer(function () {
$this->assertFalse(Context::has('database.connection.default'));
});
$resolver->connection();
},
]);
}
public function testDbConnectionResetWhenThrowTooManyExceptions()
{
$container = ContainerStub::mockContainer();
$pool = $container->get(PoolFactory::class)->getPool('default');
$dbConnection = $pool->get();
$connection = $dbConnection->getConnection();
$this->assertSame(0, $connection->getErrorCount());
$id = spl_object_hash((new ClassInvoker($connection))->connection);
$dbConnection->release();
$dbConnection = $pool->get();
$connection = $dbConnection->getConnection();
$id2 = spl_object_hash((new ClassInvoker($connection))->connection);
$this->assertSame($id, $id2);
$invoker = new ClassInvoker($connection);
for ($i = 0; $i < 101; ++$i) {
try {
(new ClassInvoker($invoker->connection))->runQueryCallback('', [], fn () => throw new Exception('xxx'));
} catch (QueryException) {
}
}
$this->assertSame(101, $connection->getErrorCount());
$dbConnection->release();
$dbConnection = $pool->get();
$connection = $dbConnection->getConnection();
$id3 = spl_object_hash((new ClassInvoker($connection))->connection);
$this->assertNotSame($id, $id3);
}
}