-
-
Notifications
You must be signed in to change notification settings - Fork 48
Pass CommandInterface to retry handler (fixes #1155, #1130) #1171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
39ee632
2c6522b
ee253e3
a8c1cc9
83d7d99
76e66cc
57f83d1
a4653a5
91d570b
9f4ca1e
0f64f53
08fd065
67c2e8d
8f6f17a
79f449d
ffb39dd
c0811d7
836a751
1d38847
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -53,12 +53,20 @@ abstract class AbstractPdoCommand extends AbstractCommand implements PdoCommandI | |||||
| */ | ||||||
| protected ?PDOStatement $pdoStatement = null; | ||||||
|
|
||||||
| /** | ||||||
| * @var array<int|string, array{value: mixed, type: int, length: int|null, driverOptions: mixed}> | ||||||
| * Parameters bound via {@see bindParam()} stored by reference for re-binding after statement re-preparation | ||||||
| * (e.g., on reconnect). | ||||||
| */ | ||||||
| protected array $pendingBoundParams = []; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We have Perhaps good idea to rename |
||||||
|
|
||||||
| /** | ||||||
| * @param PdoConnectionInterface $db The PDO database connection to use. | ||||||
| */ | ||||||
| public function __construct(PdoConnectionInterface $db) | ||||||
| { | ||||||
| parent::__construct($db); | ||||||
| $this->retryHandler = (new ConnectionRecoveryHandler($db))->asClosure(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is there way to get db connection from the command? |
||||||
| } | ||||||
|
|
||||||
| /** | ||||||
|
|
@@ -81,19 +89,16 @@ public function bindParam( | |||||
| ?int $length = null, | ||||||
| mixed $driverOptions = null, | ||||||
| ): static { | ||||||
| $this->prepare(); | ||||||
|
|
||||||
| if ($dataType === null) { | ||||||
| $dataType = $this->db->getSchema()->getDataType($value); | ||||||
| } | ||||||
|
|
||||||
| if ($length === null) { | ||||||
| $this->pdoStatement?->bindParam($name, $value, $dataType); | ||||||
| } elseif ($driverOptions === null) { | ||||||
| $this->pdoStatement?->bindParam($name, $value, $dataType, $length); | ||||||
| } else { | ||||||
| $this->pdoStatement?->bindParam($name, $value, $dataType, $length, $driverOptions); | ||||||
| } | ||||||
| $this->pendingBoundParams[$name] = [ | ||||||
| 'type' => $dataType, | ||||||
| 'length' => $length, | ||||||
| 'driverOptions' => $driverOptions, | ||||||
| 'value' => &$value, | ||||||
| ]; | ||||||
|
|
||||||
| return $this; | ||||||
| } | ||||||
|
|
@@ -163,7 +168,7 @@ public function prepare(?bool $forRead = null): void | |||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Binds pending parameters registered via {@see bindValue()} and {@see bindValues()}. | ||||||
| * Binds pending parameters registered via {@see bindValue()}, {@see bindValues()} and {@see bindParam()}. | ||||||
| * | ||||||
| * Note that this method requires an active {@see PDOStatement}. | ||||||
| */ | ||||||
|
|
@@ -172,6 +177,27 @@ protected function bindPendingParams(): void | |||||
| foreach ($this->params as $name => $value) { | ||||||
| $this->pdoStatement?->bindValue($name, $value->value, $value->type); | ||||||
| } | ||||||
|
|
||||||
| foreach ($this->pendingBoundParams as $name => &$entry) { | ||||||
| $value = &$entry['value']; | ||||||
|
|
||||||
| if ($entry['length'] === null) { | ||||||
| $this->pdoStatement?->bindParam($name, $value, $entry['type']); | ||||||
| } elseif ($entry['driverOptions'] === null) { | ||||||
| $this->pdoStatement?->bindParam($name, $value, $entry['type'], $entry['length']); | ||||||
| } else { | ||||||
| $this->pdoStatement?->bindParam($name, $value, $entry['type'], $entry['length'], $entry['driverOptions']); | ||||||
| } | ||||||
|
|
||||||
| unset($value); | ||||||
| } | ||||||
| unset($entry); | ||||||
| } | ||||||
|
|
||||||
| protected function reset(): void | ||||||
| { | ||||||
| parent::reset(); | ||||||
| $this->pendingBoundParams = []; | ||||||
| } | ||||||
|
|
||||||
| protected function getQueryBuilder(): QueryBuilderInterface | ||||||
|
|
@@ -195,6 +221,9 @@ protected function getQueryMode(int $queryMode): string | |||||
| /** | ||||||
| * A wrapper around {@see pdoStatementExecute()} to support transactions and retry handlers. | ||||||
| * | ||||||
| * By default uses {@see ConnectionRecoveryHandler} to automatically recover from connection errors on the first | ||||||
| * attempt. Override via {@see setRetryHandler()} to customize retry behavior. | ||||||
| * | ||||||
| * @throws Exception | ||||||
| */ | ||||||
| protected function internalExecute(): void | ||||||
|
|
@@ -207,9 +236,12 @@ protected function internalExecute(): void | |||||
| $rawSql ??= $this->getRawSql(); | ||||||
| $e = (new ConvertException($e, $rawSql))->run(); | ||||||
|
|
||||||
| if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) { | ||||||
| throw $e; | ||||||
| if ($this->retryHandler !== null && ($this->retryHandler)($e, $attempt, $this)) { | ||||||
| continue; | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| throw $e; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||
| <?php | ||||||||||||||||||
|
|
||||||||||||||||||
| declare(strict_types=1); | ||||||||||||||||||
|
|
||||||||||||||||||
| namespace Yiisoft\Db\Driver\Pdo; | ||||||||||||||||||
|
|
||||||||||||||||||
| use Closure; | ||||||||||||||||||
| use Throwable; | ||||||||||||||||||
| use Yiisoft\Db\Command\CommandInterface; | ||||||||||||||||||
| use Yiisoft\Db\Exception\ConnectionException; | ||||||||||||||||||
| use Yiisoft\Db\Exception\Exception; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Default retry handler that implements automatic connection recovery on the first execution failure. | ||||||||||||||||||
| * | ||||||||||||||||||
| * Detects {@see ConnectionException} errors and attempts to reconnect and re-prepare the statement before retrying. | ||||||||||||||||||
| * No retry is performed when: | ||||||||||||||||||
| * - it is not the first attempt, | ||||||||||||||||||
| * - the error is not a {@see ConnectionException}, | ||||||||||||||||||
| * - a transaction is active (reconnecting would silently roll it back), | ||||||||||||||||||
| * - the reconnection itself fails. | ||||||||||||||||||
| * | ||||||||||||||||||
| * Set as the default {@see AbstractPdoCommand::$retryHandler}. | ||||||||||||||||||
| * Replace it via {@see CommandInterface::setRetryHandler()} to customize retry behavior. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @psalm-type RetryHandlerClosure = Closure(Exception, int, CommandInterface): bool | ||||||||||||||||||
| */ | ||||||||||||||||||
| final class ConnectionRecoveryHandler | ||||||||||||||||||
| { | ||||||||||||||||||
| public function __construct(private readonly PdoConnectionInterface $db) {} | ||||||||||||||||||
|
|
||||||||||||||||||
| public function __invoke(Exception $e, int $attempt, CommandInterface $command): bool | ||||||||||||||||||
| { | ||||||||||||||||||
| // Only attempt recovery on the first failure. | ||||||||||||||||||
| if ($attempt !== 0 || !$e instanceof ConnectionException) { | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Reconnecting during an active transaction would silently roll it back. | ||||||||||||||||||
| if ($this->db->getTransaction() !== null) { | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Try to renew the connection. | ||||||||||||||||||
| try { | ||||||||||||||||||
| $this->db->close(); | ||||||||||||||||||
| $this->db->open(); | ||||||||||||||||||
| $command->cancel(); // resets the PDOStatement so prepare() creates a fresh one | ||||||||||||||||||
| } catch (Throwable) { | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Re-prepare the statement against the new connection, restoring all parameter bindings. | ||||||||||||||||||
| $command->prepare(); | ||||||||||||||||||
|
|
||||||||||||||||||
| return true; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * @psalm-return RetryHandlerClosure | ||||||||||||||||||
| */ | ||||||||||||||||||
| public function asClosure(): Closure | ||||||||||||||||||
| { | ||||||||||||||||||
| return $this->__invoke(...); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+58
to
+65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Use |
||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\Db\Exception; | ||
|
|
||
| /** | ||
| * Represents an exception caused by a lost or refused database connection. | ||
| */ | ||
| final class ConnectionException extends Exception {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.