Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion system/Database/BaseConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,20 @@ abstract class BaseConnection implements ConnectionInterface
*/
protected $transFailure = false;

/**
* The server has already rolled this transaction back (MySQLi errno 1213).
* mysqli::commit() still returns true, so transCommit() must not report success.
*/
protected bool $transServerRolledBack = false;

/**
* The next statement must open a server transaction before it runs.
* InnoDB ended the previous one and the connection is back in autocommit.
* Errno 1205 does not set this: innodb_rollback_on_timeout decides whether
* that error rolled the transaction back.
*/
protected bool $transNeedsServerRestart = false;

/**
* Whether to throw exceptions during transaction
*/
Expand Down Expand Up @@ -1022,6 +1036,7 @@ public function transBegin(bool $testMode = false): bool

if ($this->_transBegin()) {
$this->transDepth++;
$this->clearServerRolledBackTransaction();

return true;
}
Expand All @@ -1039,7 +1054,24 @@ public function transCommit(): bool
}

// When transactions are nested we only begin/commit/rollback the outermost ones
if ($this->transDepth > 1 || $this->_transCommit()) {
if ($this->transDepth > 1) {
$this->transDepth--;

return true;
}

// InnoDB already removed the victim transaction. mysqli::commit()
// returns true anyway, which would publish later writes and report success.
// Roll back the restarted server transaction so a false return ends the
// transaction, as transComplete() does. Otherwise a caller that retries
// without transRollback() nests inside it and nothing commits.
if ($this->transServerRolledBack) {
$this->transRollback();

return false;
}

if ($this->_transCommit()) {
$this->transDepth--;

return true;
Expand All @@ -1061,6 +1093,10 @@ public function transRollback(): bool
if ($this->transDepth > 1 || $this->_transRollback()) {
$this->transDepth--;

if ($this->transDepth === 0) {
$this->clearServerRolledBackTransaction();
}

return true;
}

Expand Down Expand Up @@ -1089,6 +1125,37 @@ public function handleTransStatus(): void
}
}

/**
* Remember that the server has rolled the open transaction back.
*/
protected function markServerRolledBackTransaction(): void
{
$this->transServerRolledBack = true;
$this->transNeedsServerRestart = true;
}

/**
* True while the next statement must BEGIN before it runs.
*/
protected function serverTransactionRestartPending(): bool
{
return $this->transNeedsServerRestart && $this->transDepth > 0;
}

/**
* BEGIN succeeded. Clear the restart only now, so a failed BEGIN keeps it pending.
*/
protected function serverTransactionRestarted(): void
{
$this->transNeedsServerRestart = false;
}

private function clearServerRolledBackTransaction(): void
{
$this->transServerRolledBack = false;
$this->transNeedsServerRestart = false;
}

/**
* Begin Transaction
*/
Expand Down
14 changes: 14 additions & 0 deletions system/Database/MySQLi/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,16 @@ protected function execute(string $sql)
}

try {
// Errno 1213 ends the server transaction. Begin again before this
// statement so it does not autocommit. A failed BEGIN goes through
// the normal error path below, and the restart stays pending.
// Errno 1205 is not handled: innodb_rollback_on_timeout decides
// whether InnoDB rolled that transaction back.
if ($this->connID instanceof mysqli && $this->serverTransactionRestartPending()) {
$this->connID->begin_transaction();
$this->serverTransactionRestarted();
}

return $this->connID->query($this->prepQuery($sql), $this->resultMode);
} catch (mysqli_sql_exception $e) {
log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
Expand All @@ -300,6 +310,10 @@ protected function execute(string $sql)
'trace' => render_backtrace($e->getTrace()),
]);

if ($e->getCode() === 1213 && $this->transDepth > 0) {
$this->markServerRolledBackTransaction();
}

if ($this->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
Expand Down
Loading
Loading