diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 5aaa28107..30223aec7 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -32,6 +32,7 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL $this->timeout = $milliseconds; + // Bound reads: the max_execution_time optimizer hint only applies to SELECTs. $this->before($event, 'timeout', function ($sql) use ($milliseconds) { return \preg_replace( pattern: '/SELECT/', @@ -40,6 +41,13 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL limit: 1 ); }); + + // Bound writes: hints are ignored by INSERT/UPDATE/DDL, so cap how long a + // statement waits on row (InnoDB) and metadata locks before failing fast. + // This is a connection-scoped floor: Pool re-applies it on each checkout, + // so it tracks the latest timeout without a paired reset to leak through. + $seconds = \max(1, (int) \ceil($milliseconds / 1000)); + $this->getPDO()->exec("SET SESSION innodb_lock_wait_timeout = {$seconds}, SESSION lock_wait_timeout = {$seconds}"); } /** @@ -162,6 +170,11 @@ protected function processException(PDOException $e): \Exception return new TimeoutException('Query timed out', $e->getCode(), $e); } + // Lock wait timeout (blocked write released by innodb_lock_wait_timeout/lock_wait_timeout) + if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1205) { + return new TimeoutException('Query timed out', $e->getCode(), $e); + } + // Functional index dependency if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3837) { return new DependencyException('Attribute cannot be deleted because it is used in an index', $e->getCode(), $e);