Skip to content

Commit

Permalink
Merge pull request #296 from utopia-php/static-set-timeout
Browse files Browse the repository at this point in the history
Add method for creating a timeout for all find, sum, count queries in timeout supported dbs
  • Loading branch information
abnegate authored Jul 31, 2023
2 parents 540ec7e + 48300b1 commit 5ae3035
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 33 deletions.
40 changes: 38 additions & 2 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ abstract class Adapter
*/
protected array $debug = [];

protected static ?int $timeout = null;

/**
* @param string $key
* @param mixed $value
Expand Down Expand Up @@ -385,7 +387,7 @@ abstract public function find(string $collection, array $queries = [], ?int $lim
*
* @return int|float
*/
abstract public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int;
abstract public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): float|int;

/**
* Count Documents
Expand All @@ -396,7 +398,7 @@ abstract public function sum(string $collection, string $attribute, array $queri
*
* @return int
*/
abstract public function count(string $collection, array $queries = [], ?int $max = null): int;
abstract public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int;

/**
* Get max STRING limit
Expand Down Expand Up @@ -641,4 +643,38 @@ abstract public function increaseDocumentAttribute(string $collection, string $i
* @return int
*/
abstract public function getMaxIndexLength(): int;


/**
* Set a global timeout for database queries in milliseconds.
*
* This function allows you to set a maximum execution time for all database
* queries executed using the library. Once this timeout is set, any database
* query that takes longer than the specified time will be automatically
* terminated by the library, and an appropriate error or exception will be
* raised to handle the timeout condition.
*
* @param int $milliseconds The timeout value in milliseconds for database queries.
* @return void
*
* @throws \Exception The provided timeout value must be greater than or equal to 0.
*/
public static function setTimeoutForQueries(int $milliseconds): void
{
if ($milliseconds <= 0) {
throw new Exception('Timeout must be greater than 0');
}
self::$timeout = $milliseconds;
}

/**
* Clears a global timeout for database queries.
*
* @return void
*
*/
public static function clearTimeoutForQueries(): void
{
self::$timeout = null;
}
}
38 changes: 23 additions & 15 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1005,8 +1005,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
{$sqlLimit};
";

if ($timeout) {
$sql = $this->setTimeout($sql, $timeout);
if ($timeout || static::$timeout) {
$sql = $this->setTimeout($sql, $timeout ? $timeout : static::$timeout);
}

$stmt = $this->getPDO()->prepare($sql);
Expand Down Expand Up @@ -1078,7 +1078,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
* @throws Exception
* @throws PDOException
*/
public function count(string $collection, array $queries = [], ?int $max = null): int
public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int
{
$name = $this->filter($collection);
$roles = Authorization::getRoles();
Expand All @@ -1103,6 +1103,10 @@ public function count(string $collection, array $queries = [], ?int $max = null)
{$limit}
) table_count
";
if ($timeout || self::$timeout) {
$sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout);
}

$stmt = $this->getPDO()->prepare($sql);
foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
Expand Down Expand Up @@ -1130,7 +1134,7 @@ public function count(string $collection, array $queries = [], ?int $max = null)
* @throws Exception
* @throws PDOException
*/
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): int|float
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): int|float
{
$name = $this->filter($collection);
$roles = Authorization::getRoles();
Expand All @@ -1146,16 +1150,20 @@ public function sum(string $collection, string $attribute, array $queries = [],
}

$sqlWhere = !empty($where) ? 'where ' . implode(' AND ', $where) : '';
$sql = "SELECT SUM({$attribute}) as sum
FROM
(
SELECT {$attribute}
FROM {$this->getSQLTable($name)} table_main
" . $sqlWhere . "
{$limit}
) table_count
";
if ($timeout || self::$timeout) {
$sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout);
}

$stmt = $this->getPDO()->prepare("
SELECT SUM({$attribute}) as sum
FROM (
SELECT {$attribute}
FROM {$this->getSQLTable($name)} table_main
" . $sqlWhere . "
{$limit}
) table_count
");
$stmt = $this->getPDO()->prepare($sql);

foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
Expand Down Expand Up @@ -1224,7 +1232,7 @@ protected function getSQLCondition(Query $query): string
default => $query->getAttribute()
});

$attribute = "`{$query->getAttribute()}`" ;
$attribute = "`{$query->getAttribute()}`";
$placeholder = $this->getSQLPlaceholder($query);

switch ($query->getMethod()) {
Expand All @@ -1241,7 +1249,7 @@ protected function getSQLCondition(Query $query): string
default:
$conditions = [];
foreach ($query->getValues() as $key => $value) {
$conditions[] = $attribute.' '.$this->getSQLOperator($query->getMethod()).' :'.$placeholder.'_'.$key;
$conditions[] = $attribute . ' ' . $this->getSQLOperator($query->getMethod()) . ' :' . $placeholder . '_' . $key;
}
$condition = implode(' OR ', $conditions);
return empty($condition) ? '' : '(' . $condition . ')';
Expand Down
15 changes: 11 additions & 4 deletions src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -803,8 +803,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$options['skip'] = $offset;
}

if ($timeout) {
$options['maxTimeMS'] = $timeout;
if ($timeout || self::$timeout) {
$options['maxTimeMS'] = $timeout ? $timeout : self::$timeout;
}

$selections = $this->getAttributeSelections($queries);
Expand Down Expand Up @@ -1040,7 +1040,7 @@ private function recursiveReplace(array $array, string $from, string $to, array
* @return int
* @throws Exception
*/
public function count(string $collection, array $queries = [], ?int $max = null): int
public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int
{
$name = $this->getNamespace() . '_' . $this->filter($collection);

Expand All @@ -1052,6 +1052,10 @@ public function count(string $collection, array $queries = [], ?int $max = null)
$options['limit'] = $max;
}

if ($timeout || self::$timeout) {
$options['maxTimeMS'] = $timeout ? $timeout : self::$timeout;
}

// queries
$filters = $this->buildFilters($queries);

Expand All @@ -1075,11 +1079,14 @@ public function count(string $collection, array $queries = [], ?int $max = null)
* @return int|float
* @throws Exception
*/
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): float|int
{
$name = $this->getNamespace() . '_' . $this->filter($collection);
$collection = $this->getDatabase()->selectCollection($name);
// todo $collection is not used?

// todo add $timeout for aggregate in Mongo utopia client

$filters = [];

// queries
Expand Down
35 changes: 24 additions & 11 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -1015,8 +1015,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
{$sqlLimit};
";

if ($timeout) {
$sql = $this->setTimeout($sql, $timeout);
if ($timeout || self::$timeout) {
$sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout);
}

$stmt = $this->getPDO()->prepare($sql);
Expand Down Expand Up @@ -1089,7 +1089,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
*
* @return int
*/
public function count(string $collection, array $queries = [], ?int $max = null): int
public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int
{
$name = $this->filter($collection);
$roles = Authorization::getRoles();
Expand All @@ -1114,6 +1114,11 @@ public function count(string $collection, array $queries = [], ?int $max = null)
{$limit}
) table_count
";

if ($timeout || self::$timeout) {
$sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout);
}

$stmt = $this->getPDO()->prepare($sql);
foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
Expand Down Expand Up @@ -1142,7 +1147,7 @@ public function count(string $collection, array $queries = [], ?int $max = null)
*
* @return int|float
*/
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): int|float
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): int|float
{
$name = $this->filter($collection);
$roles = Authorization::getRoles();
Expand All @@ -1159,13 +1164,21 @@ public function sum(string $collection, string $attribute, array $queries = [],
$where[] = $this->getSQLPermissionsCondition($name, $roles);
}

$stmt = $this->getPDO()->prepare("SELECT SUM({$attribute}) as sum
FROM (
SELECT {$attribute}
FROM {$this->getSQLTable($name)} table_main
WHERE {$permissions} AND " . implode(' AND ', $where) . "
{$limit}
) table_count");
$sql = "SELECT SUM({$attribute}) as sum
FROM
(
SELECT {$attribute}
FROM {$this->getSQLTable($name)} table_main
WHERE {$permissions} AND " . implode(' AND ', $where) . "
{$limit}
) table_count
";

if ($timeout || self::$timeout) {
$sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout);
}

$stmt = $this->getPDO()->prepare($sql);

foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
Expand Down
9 changes: 9 additions & 0 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -4207,6 +4207,15 @@ public function sum(string $collection, string $attribute, array $queries = [],
return $sum;
}

public function setTimeoutForQueries(int $milliseconds): void
{
$this->adapter->setTimeoutForQueries($milliseconds);
}

public function clearTimeoutForQueries(): void
{
$this->adapter->clearTimeoutForQueries();
}
/**
* Add Attribute Filter
*
Expand Down
35 changes: 34 additions & 1 deletion tests/Database/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,46 @@ public function testCreatedAtUpdatedAt(): void
$this->assertNotNull($document->getInternalId());
}

public function testQueryTimeoutUsingStaticTimeout(): void
{
if ($this->getDatabase()->getAdapter()->getSupportForTimeouts()) {
static::getDatabase()->createCollection('global-timeouts');
$this->assertEquals(true, static::getDatabase()->createAttribute('global-timeouts', 'longtext', Database::VAR_STRING, 100000000, true));

for ($i = 0 ; $i <= 5 ; $i++) {
static::getDatabase()->createDocument('global-timeouts', new Document([
'longtext' => file_get_contents(__DIR__ . '/../resources/longtext.txt'),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any())
]
]));
}

$this->expectException(Timeout::class);
static::getDatabase()->setTimeoutForQueries(1);

try {
static::getDatabase()->find('global-timeouts', [
Query::notEqual('longtext', 'appwrite'),
]);
} catch(Timeout $ex) {
static::getDatabase()->clearTimeoutForQueries();
static::getDatabase()->deleteCollection('global-timeouts');
throw $ex;
}
}
$this->expectNotToPerformAssertions();
}


/**
* @depends testCreateExistsDelete
*/
public function testCreateListExistsDeleteCollection(): void
{
$this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('actors'));

$this->assertCount(2, static::getDatabase()->listCollections());
$this->assertEquals(true, static::getDatabase()->exists($this->testDatabase, 'actors'));

Expand Down

0 comments on commit 5ae3035

Please sign in to comment.