From 82c4d54e73bc8fd31208b4e2dfc2223068ae4326 Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 17 Sep 2026 22:28:04 +0200 Subject: [PATCH] fix(db): convert driver exceptions raised by COMMIT Doctrine converts driver-level exceptions into typed DBAL exceptions for queries, but Connection::doCommit() leaves them untouched. A deadlock, or a Galera certification failure, raised at commit time therefore surfaced as a raw Doctrine\DBAL\Driver\PDO\PDOException. That class implements the driver exception interface, not Doctrine\DBAL\Exception, so ConnectionAdapter::commit() never wrapped it into a DbalException and the retry loop in Propagator::propagateChange() never saw it. The error reached the user as an HTTP 500, shown as "unknown error" in the web interface, when several uploads updated the same parent folder rows at once. Group folders hit this often because every member shares one storage. Run the driver exception through the platform exception converter so error 1213 becomes a DeadlockException and callers can detect it as retryable. Also guard both rollback calls in the propagator. A failed commit has already ended the transaction, so rolling back throws "there is no active transaction" and masks the original error. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Hamza --- lib/private/DB/Connection.php | 10 +++++++++- lib/private/Files/Cache/Propagator.php | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 0ad9886e7c05d..c04c75df2d7fb 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -14,6 +14,7 @@ use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\ConnectionLost; @@ -854,7 +855,14 @@ public function beginTransaction() { #[\Override] public function commit() { - $result = parent::commit(); + try { + $result = parent::commit(); + } catch (DriverException $e) { + // DBAL converts driver exceptions for queries, but not for COMMIT. Without + // this, a deadlock or a Galera certification failure raised at commit time + // escapes as a raw PDOException that no caller recognises as retryable. + throw $this->getDriver()->getExceptionConverter()->convert($e, null); + } if ($this->getTransactionNestingLevel() === 0) { $timeTook = microtime(true) - $this->transactionActiveSince; $this->transactionBacktrace = null; diff --git a/lib/private/Files/Cache/Propagator.php b/lib/private/Files/Cache/Propagator.php index 2f8cf909ed03e..26aca124e1109 100644 --- a/lib/private/Files/Cache/Propagator.php +++ b/lib/private/Files/Cache/Propagator.php @@ -138,7 +138,8 @@ public function propagateChange(string $internalPath, int $time, int $sizeDiffer } break; } catch (DbalException $e) { - if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_SQLITE) { + // a failed commit already ended the transaction, so only roll back an open one + if ($this->connection->inTransaction()) { $this->connection->rollBack(); } if (!$e->isRetryable()) { @@ -292,7 +293,9 @@ public function commitBatch(): void { $this->connection->commit(); } catch (\Exception $e) { - $this->connection->rollback(); + if ($this->connection->inTransaction()) { + $this->connection->rollback(); + } throw $e; } }