diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -34,6 +34,7 @@ 'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php', 'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php', 'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php', + 'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php', 'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php', 'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php', 'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php', @@ -564,6 +565,7 @@ 'AphrontObjectMissingQueryException' => 'AphrontQueryException', 'AphrontParameterQueryException' => 'AphrontQueryException', 'AphrontQueryException' => 'Exception', + 'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontRecoverableQueryException' => 'AphrontQueryException', 'AphrontRequestStream' => 'Phobject', 'AphrontSchemaQueryException' => 'AphrontQueryException', diff --git a/src/aphront/storage/connection/AphrontDatabaseConnection.php b/src/aphront/storage/connection/AphrontDatabaseConnection.php --- a/src/aphront/storage/connection/AphrontDatabaseConnection.php +++ b/src/aphront/storage/connection/AphrontDatabaseConnection.php @@ -9,6 +9,7 @@ private $transactionState; private $readOnly; + private $queryTimeout; abstract public function getInsertID(); abstract public function getAffectedRows(); @@ -48,6 +49,15 @@ return $this->readOnly; } + public function setQueryTimeout($query_timeout) { + $this->queryTimeout = $query_timeout; + return $this; + } + + public function getQueryTimeout() { + return $this->queryTimeout; + } + public function asyncQuery($raw_query) { throw new Exception(pht('Async queries are not supported.')); } diff --git a/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php --- a/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php +++ b/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php @@ -81,7 +81,37 @@ } protected function rawQuery($raw_query) { - return @$this->requireConnection()->query($raw_query); + $conn = $this->requireConnection(); + $time_limit = $this->getQueryTimeout(); + + // If we have a query time limit, run this query synchronously but use + // the async API. This allows us to kill queries which take too long + // without requiring any configuration on the server side. + if ($time_limit && $this->supportsAsyncQueries()) { + $conn->query($raw_query, MYSQLI_ASYNC); + + $read = array($conn); + $error = array($conn); + $reject = array($conn); + + $result = mysqli::poll($read, $error, $reject, $time_limit); + + if ($result === false) { + $this->closeConnection(); + throw new Exception( + pht('Failed to poll mysqli connection!')); + } else if ($result === 0) { + $this->closeConnection(); + throw new AphrontQueryTimeoutQueryException( + pht( + 'Query timed out after %s second(s)!', + new PhutilNumber($time_limit))); + } + + return @$conn->reap_async_query(); + } + + return @$conn->query($raw_query); } protected function rawQueries(array $raw_queries) { diff --git a/src/aphront/storage/exception/AphrontQueryTimeoutQueryException.php b/src/aphront/storage/exception/AphrontQueryTimeoutQueryException.php new file mode 100644 --- /dev/null +++ b/src/aphront/storage/exception/AphrontQueryTimeoutQueryException.php @@ -0,0 +1,4 @@ +<?php + +final class AphrontQueryTimeoutQueryException + extends AphrontRecoverableQueryException {}