mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.6.x' into update-messaging
This commit is contained in:
+7
-2
@@ -26,6 +26,9 @@ use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
|
||||
use function Swoole\Coroutine\run;
|
||||
|
||||
// Overwriting runtimes to be architecture agnostic for CLI
|
||||
Config::setParam('runtimes', (new Runtimes('v4'))->getAll(supported: false));
|
||||
@@ -200,7 +203,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
};
|
||||
}, ['pools', 'cache']);
|
||||
|
||||
CLI::setResource('queueForStatsUsage', function (Connection $publisher) {
|
||||
CLI::setResource('queueForStatsUsage', function (Publisher $publisher) {
|
||||
return new StatsUsage($publisher);
|
||||
}, ['publisher']);
|
||||
CLI::setResource('queueForStatsResources', function (Publisher $publisher) {
|
||||
@@ -264,6 +267,8 @@ CLI::setResource('logError', function (Registry $register) {
|
||||
|
||||
CLI::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
|
||||
|
||||
CLI::setResource('telemetry', fn () => new NoTelemetry());
|
||||
|
||||
$platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
|
||||
@@ -293,4 +298,4 @@ $cli
|
||||
|
||||
$cli->shutdown()->action(fn () => Timer::clearAll());
|
||||
|
||||
$cli->run();
|
||||
run($cli->run(...));
|
||||
|
||||
@@ -2813,11 +2813,12 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
|
||||
->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.')
|
||||
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
|
||||
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional:true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) {
|
||||
->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) {
|
||||
|
||||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
@@ -2877,9 +2878,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
'size' => 0
|
||||
];
|
||||
|
||||
// lengths hidden by default
|
||||
$lengths = [];
|
||||
|
||||
foreach ($attributes as $i => $attribute) {
|
||||
// find attribute metadata in collection document
|
||||
$attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
|
||||
@@ -2901,8 +2899,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE, 'Attribute not available: ' . $oldAttributes[$attributeIndex]['key']);
|
||||
}
|
||||
|
||||
$lengths[$i] = null;
|
||||
|
||||
$lengths[$i] ??= null;
|
||||
if ($attributeArray === true) {
|
||||
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
|
||||
$orders[$i] = null;
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@
|
||||
"utopia-php/cache": "0.13.*",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "0.2.*",
|
||||
"utopia-php/database": "0.68.*",
|
||||
"utopia-php/database": "0.69.*",
|
||||
"utopia-php/domains": "0.5.*",
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
|
||||
Generated
+14
-16
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "194987f84ab514a271856a347ba0e636",
|
||||
"content-hash": "1bedbbb171d688660adb388298261f4c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -3499,16 +3499,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "0.68.1",
|
||||
"version": "0.69.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "72b2e2c0b875028f7d9dd755f6d4524b693c6507"
|
||||
"reference": "cc6538e05e25d930244ab938c966d32db0922e83"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/72b2e2c0b875028f7d9dd755f6d4524b693c6507",
|
||||
"reference": "72b2e2c0b875028f7d9dd755f6d4524b693c6507",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cc6538e05e25d930244ab938c966d32db0922e83",
|
||||
"reference": "cc6538e05e25d930244ab938c966d32db0922e83",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3549,9 +3549,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/0.68.1"
|
||||
"source": "https://github.com/utopia-php/database/tree/0.69.1"
|
||||
},
|
||||
"time": "2025-05-09T10:08:53+00:00"
|
||||
"time": "2025-05-13T12:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
@@ -3902,16 +3902,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/messaging",
|
||||
"version": "dev-feat-inforu-adapter",
|
||||
"version": "0.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/messaging.git",
|
||||
"reference": "3c5a61c5a91a991efc4b4e4bda85ec06a567221e"
|
||||
"reference": "c51915d0e030db3a3add37f1561751d18b2d9a85"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/3c5a61c5a91a991efc4b4e4bda85ec06a567221e",
|
||||
"reference": "3c5a61c5a91a991efc4b4e4bda85ec06a567221e",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/c51915d0e030db3a3add37f1561751d18b2d9a85",
|
||||
"reference": "c51915d0e030db3a3add37f1561751d18b2d9a85",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3947,9 +3947,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/messaging/issues",
|
||||
"source": "https://github.com/utopia-php/messaging/tree/feat-inforu-adapter"
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.17.0"
|
||||
},
|
||||
"time": "2025-05-12T16:03:53+00:00"
|
||||
"time": "2025-05-12T16:14:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
@@ -8136,9 +8136,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"utopia-php/messaging": 20
|
||||
},
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
||||
@@ -33,11 +33,7 @@ class Migrate extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('register')
|
||||
->callback(function ($version, $dbForPlatform, $getProjectDB, Registry $register) {
|
||||
\Co\run(function () use ($version, $dbForPlatform, $getProjectDB, $register) {
|
||||
$this->action($version, $dbForPlatform, $getProjectDB, $register);
|
||||
});
|
||||
});
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
private function clearProjectsCache(Document $project)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Swoole\Runtime;
|
||||
use Swoole\Timer;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -13,8 +14,9 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\System\System;
|
||||
|
||||
use function Swoole\Coroutine\run;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Gauge;
|
||||
use Utopia\Telemetry\Histogram;
|
||||
|
||||
abstract class ScheduleBase extends Action
|
||||
{
|
||||
@@ -23,6 +25,11 @@ abstract class ScheduleBase extends Action
|
||||
|
||||
protected array $schedules = [];
|
||||
|
||||
private ?Histogram $collectSchedulesTelemetryDuration = null;
|
||||
private ?Gauge $collectSchedulesTelemetryCount = null;
|
||||
private ?Gauge $scheduleTelemetryCount = null;
|
||||
private ?Histogram $enqueueDelayTelemetry = null;
|
||||
|
||||
abstract public static function getName(): string;
|
||||
abstract public static function getSupportedResource(): string;
|
||||
abstract public static function getCollectionId(): string;
|
||||
@@ -37,7 +44,8 @@ abstract class ScheduleBase extends Action
|
||||
->inject('pools')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->callback(fn (Group $pools, Database $dbForPlatform, callable $getProjectDB) => $this->action($pools, $dbForPlatform, $getProjectDB));
|
||||
->inject('telemetry')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
protected function updateProjectAccess(Document $project, Database $dbForPlatform): void
|
||||
@@ -56,11 +64,49 @@ abstract class ScheduleBase extends Action
|
||||
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
|
||||
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
|
||||
*/
|
||||
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
|
||||
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB, Telemetry $telemetry): void
|
||||
{
|
||||
Runtime::enableCoroutine();
|
||||
|
||||
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
|
||||
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
|
||||
|
||||
$this->scheduleTelemetryCount = $telemetry->createGauge('task.schedule.count');
|
||||
$this->collectSchedulesTelemetryDuration = $telemetry->createHistogram('task.schedule.collect_schedules.duration', 's');
|
||||
$this->collectSchedulesTelemetryCount = $telemetry->createGauge('task.schedule.collect_schedules.count');
|
||||
$this->enqueueDelayTelemetry = $telemetry->createHistogram('task.schedule.enqueue_delay', 's');
|
||||
|
||||
// start with "0" to load all active documents.
|
||||
$lastSyncUpdate = "0";
|
||||
$this->collectSchedules($pools, $dbForPlatform, $getProjectDB, $lastSyncUpdate);
|
||||
|
||||
Console::success("Starting timers at " . DateTime::now());
|
||||
/**
|
||||
* The timer synchronize $schedules copy with database collection.
|
||||
*/
|
||||
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($pools, $dbForPlatform, $getProjectDB, &$lastSyncUpdate) {
|
||||
$time = DateTime::now();
|
||||
Console::log("Sync tick: Running at $time");
|
||||
$this->collectSchedules($pools, $dbForPlatform, $getProjectDB, $lastSyncUpdate);
|
||||
});
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
go(fn () => $this->enqueueResources($pools, $dbForPlatform, $getProjectDB));
|
||||
$this->scheduleTelemetryCount->record(count($this->schedules), ['resourceType' => static::getSupportedResource()]);
|
||||
sleep(static::ENQUEUE_TIMER);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Failed to enqueue resources: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function collectSchedules(Group $pools, Database $dbForPlatform, callable $getProjectDB, string &$lastSyncUpdate): void
|
||||
{
|
||||
// If we haven't synced yet, load all active schedules
|
||||
$initialLoad = $lastSyncUpdate === "0";
|
||||
|
||||
/**
|
||||
* Extract only necessary attributes to lower memory used.
|
||||
*
|
||||
@@ -68,7 +114,7 @@ abstract class ScheduleBase extends Action
|
||||
* @throws Exception
|
||||
* @var Document $schedule
|
||||
*/
|
||||
$getSchedule = function (Document $schedule) use ($dbForPlatform, $getProjectDB): array {
|
||||
$getSchedule = function (Document $schedule) use ($pools, $dbForPlatform, $getProjectDB): array {
|
||||
$project = $dbForPlatform->getDocument('projects', $schedule->getAttribute('projectId'));
|
||||
|
||||
$resource = $getProjectDB($project)->getDocument(
|
||||
@@ -76,6 +122,8 @@ abstract class ScheduleBase extends Action
|
||||
$schedule->getAttribute('resourceId')
|
||||
);
|
||||
|
||||
$pools->reclaim();
|
||||
|
||||
return [
|
||||
'$internalId' => $schedule->getInternalId(),
|
||||
'$id' => $schedule->getId(),
|
||||
@@ -88,12 +136,12 @@ abstract class ScheduleBase extends Action
|
||||
];
|
||||
};
|
||||
|
||||
$lastSyncUpdate = DateTime::now();
|
||||
$loadStart = microtime(true);
|
||||
$time = DateTime::now();
|
||||
|
||||
$limit = 10_000;
|
||||
$sum = $limit;
|
||||
$total = 0;
|
||||
$loadStart = \microtime(true);
|
||||
$latestDocument = null;
|
||||
|
||||
while ($sum === $limit) {
|
||||
@@ -110,105 +158,57 @@ abstract class ScheduleBase extends Action
|
||||
$regions[] = 'default';
|
||||
}
|
||||
|
||||
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
|
||||
$paginationQueries = [
|
||||
...$paginationQueries,
|
||||
Query::equal('region', $regions),
|
||||
Query::equal('resourceType', [static::getSupportedResource()]),
|
||||
Query::equal('active', [true]),
|
||||
]));
|
||||
];
|
||||
|
||||
$sum = \count($results);
|
||||
if ($initialLoad) {
|
||||
$paginationQueries[] = Query::equal('active', [true]);
|
||||
} else {
|
||||
$paginationQueries[] = Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate);
|
||||
}
|
||||
|
||||
$results = $dbForPlatform->find('schedules', $paginationQueries);
|
||||
|
||||
$sum = count($results);
|
||||
$total = $total + $sum;
|
||||
|
||||
foreach ($results as $document) {
|
||||
try {
|
||||
$this->schedules[$document->getInternalId()] = $getSchedule($document);
|
||||
} catch (\Throwable $th) {
|
||||
$collectionId = static::getCollectionId();
|
||||
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
|
||||
Console::error($th->getMessage());
|
||||
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
|
||||
|
||||
if ($localDocument !== null) {
|
||||
if (!$document['active']) {
|
||||
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
|
||||
unset($this->schedules[$document->getInternalId()]);
|
||||
} elseif (strtotime($localDocument['resourceUpdatedAt']) !== strtotime($document['resourceUpdatedAt'])) {
|
||||
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
|
||||
$this->schedules[$document->getInternalId()] = $getSchedule($document);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->schedules[$document->getInternalId()] = $getSchedule($document);
|
||||
} catch (\Throwable $th) {
|
||||
$collectionId = static::getCollectionId();
|
||||
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$latestDocument = \end($results);
|
||||
}
|
||||
|
||||
$pools->reclaim();
|
||||
$lastSyncUpdate = $time;
|
||||
$duration = microtime(true) - $loadStart;
|
||||
$this->collectSchedulesTelemetryDuration->record($duration, ['initial' => $initialLoad, 'resourceType' => static::getSupportedResource()]);
|
||||
$this->collectSchedulesTelemetryCount->record($total, ['initial' => $initialLoad, 'resourceType' => static::getSupportedResource()]);
|
||||
Console::success("{$total} resources were loaded in " . $duration . " seconds");
|
||||
}
|
||||
|
||||
Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds");
|
||||
|
||||
Console::success("Starting timers at " . DateTime::now());
|
||||
|
||||
run(function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
|
||||
/**
|
||||
* The timer synchronize $schedules copy with database collection.
|
||||
*/
|
||||
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools) {
|
||||
$time = DateTime::now();
|
||||
$timerStart = \microtime(true);
|
||||
|
||||
$limit = 1000;
|
||||
$sum = $limit;
|
||||
$total = 0;
|
||||
$latestDocument = null;
|
||||
|
||||
Console::log("Sync tick: Running at $time");
|
||||
|
||||
while ($sum === $limit) {
|
||||
$paginationQueries = [Query::limit($limit)];
|
||||
|
||||
if ($latestDocument) {
|
||||
$paginationQueries[] = Query::cursorAfter($latestDocument);
|
||||
}
|
||||
|
||||
// Temporarly accepting both 'fra' and 'default'
|
||||
// When all migrated, only use _APP_REGION with 'default' as default value
|
||||
$regions = [System::getEnv('_APP_REGION', 'default')];
|
||||
if (!in_array('default', $regions)) {
|
||||
$regions[] = 'default';
|
||||
}
|
||||
|
||||
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
|
||||
Query::equal('region', $regions),
|
||||
Query::equal('resourceType', [static::getSupportedResource()]),
|
||||
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
|
||||
]));
|
||||
|
||||
$sum = count($results);
|
||||
$total = $total + $sum;
|
||||
|
||||
foreach ($results as $document) {
|
||||
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
|
||||
|
||||
// Check if resource has been updated since last sync
|
||||
$org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null;
|
||||
$new = \strtotime($document['resourceUpdatedAt']);
|
||||
|
||||
if (!$document['active']) {
|
||||
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
|
||||
unset($this->schedules[$document->getInternalId()]);
|
||||
} elseif ($new !== $org) {
|
||||
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
|
||||
$this->schedules[$document->getInternalId()] = $getSchedule($document);
|
||||
}
|
||||
}
|
||||
|
||||
$latestDocument = \end($results);
|
||||
}
|
||||
|
||||
$lastSyncUpdate = $time;
|
||||
$timerEnd = \microtime(true);
|
||||
|
||||
$pools->reclaim();
|
||||
|
||||
Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds");
|
||||
});
|
||||
|
||||
Timer::tick(
|
||||
static::ENQUEUE_TIMER * 1000,
|
||||
fn () => $this->enqueueResources($pools, $dbForPlatform, $getProjectDB)
|
||||
);
|
||||
|
||||
$this->enqueueResources($pools, $dbForPlatform, $getProjectDB);
|
||||
});
|
||||
protected function recordEnqueueDelay(\DateTime $expectedExecutionSchedule): void
|
||||
{
|
||||
$this->enqueueDelayTelemetry->record(time() - $expectedExecutionSchedule->getTimestamp(), ['resourceType' => static::getSupportedResource()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class ScheduleExecutions extends ScheduleBase
|
||||
|
||||
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
|
||||
|
||||
\go(function () use ($queueForFunctions, $schedule, $delay, $data) {
|
||||
\go(function () use ($queueForFunctions, $schedule, $scheduledAt, $delay, $data) {
|
||||
Co::sleep($delay);
|
||||
|
||||
$queueForFunctions->setType('schedule')
|
||||
@@ -74,6 +74,8 @@ class ScheduleExecutions extends ScheduleBase
|
||||
->setProject($schedule['project'])
|
||||
->setUserId($data['userId'] ?? '')
|
||||
->trigger();
|
||||
|
||||
$this->recordEnqueueDelay($scheduledAt);
|
||||
});
|
||||
|
||||
$dbForPlatform->deleteDocument(
|
||||
|
||||
@@ -46,7 +46,13 @@ class ScheduleFunctions extends ScheduleBase
|
||||
$delayedExecutions = []; // Group executions with same delay to share one coroutine
|
||||
|
||||
foreach ($this->schedules as $key => $schedule) {
|
||||
$cron = new CronExpression($schedule['schedule']);
|
||||
try {
|
||||
$cron = new CronExpression($schedule['schedule']);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// ignore invalid cron expressions
|
||||
continue;
|
||||
}
|
||||
|
||||
$nextDate = $cron->getNextRunDate();
|
||||
$next = DateTime::format($nextDate);
|
||||
|
||||
@@ -66,17 +72,18 @@ class ScheduleFunctions extends ScheduleBase
|
||||
$delayedExecutions[$delay] = [];
|
||||
}
|
||||
|
||||
$delayedExecutions[$delay][] = $key;
|
||||
$delayedExecutions[$delay][] = ['key' => $key, 'nextDate' => $nextDate];
|
||||
}
|
||||
|
||||
foreach ($delayedExecutions as $delay => $scheduleKeys) {
|
||||
\go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) {
|
||||
foreach ($delayedExecutions as $delay => $schedules) {
|
||||
\go(function () use ($delay, $schedules, $pools, $dbForPlatform) {
|
||||
\sleep($delay); // in seconds
|
||||
|
||||
$queue = $pools->get('publisher')->pop();
|
||||
$connection = $queue->getResource();
|
||||
|
||||
foreach ($scheduleKeys as $scheduleKey) {
|
||||
foreach ($schedules as $delayConfig) {
|
||||
$scheduleKey = $delayConfig['key'];
|
||||
// Ensure schedule was not deleted
|
||||
if (!\array_key_exists($scheduleKey, $this->schedules)) {
|
||||
return;
|
||||
@@ -95,6 +102,8 @@ class ScheduleFunctions extends ScheduleBase
|
||||
->setPath('/')
|
||||
->setProject($schedule['project'])
|
||||
->trigger();
|
||||
|
||||
$this->recordEnqueueDelay($delayConfig['nextDate']);
|
||||
}
|
||||
|
||||
$queue->reclaim();
|
||||
|
||||
@@ -40,7 +40,7 @@ class ScheduleMessages extends ScheduleBase
|
||||
continue;
|
||||
}
|
||||
|
||||
\go(function () use ($schedule, $pools, $dbForPlatform) {
|
||||
\go(function () use ($schedule, $scheduledAt, $pools, $dbForPlatform) {
|
||||
$queue = $pools->get('publisher')->pop();
|
||||
$connection = $queue->getResource();
|
||||
$queueForMessaging = new Messaging($connection);
|
||||
@@ -59,7 +59,7 @@ class ScheduleMessages extends ScheduleBase
|
||||
);
|
||||
|
||||
$queue->reclaim();
|
||||
|
||||
$this->recordEnqueueDelay($scheduledAt);
|
||||
unset($this->schedules[$schedule['$internalId']]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Utopia;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -29,35 +30,49 @@ class Request extends UtopiaRequest
|
||||
{
|
||||
$parameters = parent::getParams();
|
||||
|
||||
if ($this->hasFilters() && self::hasRoute()) {
|
||||
$methods = self::getRoute()->getLabel('sdk', null);
|
||||
if (!$this->hasFilters() || !self::hasRoute()) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
if (!\is_array($methods)) {
|
||||
$methods = [$methods];
|
||||
}
|
||||
$methods = self::getRoute()->getLabel('sdk', null);
|
||||
|
||||
$params = [];
|
||||
|
||||
foreach ($methods as $method) {
|
||||
/** @var \Appwrite\SDK\Method $method */
|
||||
if (empty($method)) {
|
||||
$endpointIdentifier = 'unknown.unknown';
|
||||
} else {
|
||||
$endpointIdentifier = $method->getNamespace() . '.' . $method->getMethodName();
|
||||
}
|
||||
|
||||
$params += $method->getParameters();
|
||||
}
|
||||
|
||||
if (!empty($params)) {
|
||||
$parameters = array_filter($parameters, function ($key) use ($params) {
|
||||
return array_key_exists($key, $params);
|
||||
}, \ARRAY_FILTER_USE_KEY);
|
||||
}
|
||||
if (empty($methods)) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
if (!\is_array($methods)) {
|
||||
$id = $methods->getNamespace() . '.' . $methods->getMethodName();
|
||||
foreach ($this->getFilters() as $filter) {
|
||||
$parameters = $filter->parse($parameters, $endpointIdentifier);
|
||||
$parameters = $filter->parse($parameters, $id);
|
||||
}
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
$matched = null;
|
||||
foreach ($methods as $method) {
|
||||
/** @var Method|null $method */
|
||||
if ($method === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the method that matches the parameters passed
|
||||
$methodParamNames = \array_map(fn ($param) => $param->getName(), $method->getParameters());
|
||||
$invalidParams = \array_diff(\array_keys($parameters), $methodParamNames);
|
||||
|
||||
// No params defined, or all params are valid
|
||||
if (empty($methodParamNames) || empty($invalidParams)) {
|
||||
$matched = $method;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$id = $matched !== null
|
||||
? $matched->getNamespace() . '.' . $matched->getMethodName()
|
||||
: 'unknown.unknown';
|
||||
|
||||
// Apply filters
|
||||
foreach ($this->getFilters() as $filter) {
|
||||
$parameters = $filter->parse($parameters, $id);
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
|
||||
@@ -41,6 +41,13 @@ class Index extends Model
|
||||
'example' => [],
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('lengths', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Index attributes length.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('orders', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Index orders.',
|
||||
|
||||
@@ -1422,9 +1422,102 @@ trait DatabasesBase
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @depends testCreateIndexes
|
||||
*/
|
||||
* @depends testCreateAttributes
|
||||
*/
|
||||
public function testGetIndexByKeyWithLengths(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$collectionId = $data['moviesId'];
|
||||
|
||||
// Test case for valid lengths
|
||||
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'lengthTestIndex',
|
||||
'type' => 'key',
|
||||
'attributes' => ['title','description'],
|
||||
'lengths' => [128,200]
|
||||
]);
|
||||
$this->assertEquals(202, $create['headers']['status-code']);
|
||||
|
||||
// Fetch index and check correct lengths
|
||||
$index = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/indexes/lengthTestIndex", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $index['headers']['status-code']);
|
||||
$this->assertEquals('lengthTestIndex', $index['body']['key']);
|
||||
$this->assertEquals([128, 200], $index['body']['lengths']);
|
||||
|
||||
// Test case for lengths array overriding
|
||||
// set a length for an array attribute, it should get overriden with Database::ARRAY_INDEX_LENGTH
|
||||
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'lengthOverrideTestIndex',
|
||||
'type' => 'key',
|
||||
'attributes' => ['actors'],
|
||||
'lengths' => [120]
|
||||
]);
|
||||
$this->assertEquals(202, $create['headers']['status-code']);
|
||||
|
||||
$index = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/indexes/lengthOverrideTestIndex", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals([Database::ARRAY_INDEX_LENGTH], $index['body']['lengths']);
|
||||
|
||||
// Test case for count of lengths greater than attributes (should throw 400)
|
||||
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'lengthCountExceededIndex',
|
||||
'type' => 'key',
|
||||
'attributes' => ['title'],
|
||||
'lengths' => [128, 128]
|
||||
]);
|
||||
$this->assertEquals(400, $create['headers']['status-code']);
|
||||
|
||||
// Test case for lengths exceeding total of 768
|
||||
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'lengthTooLargeIndex',
|
||||
'type' => 'key',
|
||||
'attributes' => ['title','description','tagline','actors'],
|
||||
'lengths' => [256,256,256,20]
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $create['headers']['status-code']);
|
||||
|
||||
// Test case for negative length values
|
||||
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'negativeLengthIndex',
|
||||
'type' => 'key',
|
||||
'attributes' => ['title'],
|
||||
'lengths' => [-1]
|
||||
]);
|
||||
$this->assertEquals(400, $create['headers']['status-code']);
|
||||
}
|
||||
/**
|
||||
* @depends testCreateIndexes
|
||||
*/
|
||||
public function testListIndexes(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Tests\Unit\Utopia;
|
||||
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Parameter;
|
||||
use Appwrite\Utopia\Request;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
@@ -57,4 +58,130 @@ class RequestTest extends TestCase
|
||||
$this->assertTrue($output['second']);
|
||||
$this->assertArrayNotHasKey('deleted', $output);
|
||||
}
|
||||
|
||||
public function testGetParamsWithMultipleMethods(): void
|
||||
{
|
||||
$this->setupMultiMethodRoute();
|
||||
|
||||
// Pass only "foo", should match Method A
|
||||
$this->request->setQueryString([
|
||||
'foo' => 'valueFoo',
|
||||
]);
|
||||
|
||||
$params = $this->request->getParams();
|
||||
|
||||
$this->assertArrayHasKey('foo', $params);
|
||||
$this->assertSame('valueFoo', $params['foo']);
|
||||
$this->assertArrayNotHasKey('baz', $params);
|
||||
}
|
||||
|
||||
public function testGetParamsWithAllRequired(): void
|
||||
{
|
||||
$this->setupMultiMethodRoute();
|
||||
|
||||
// Pass "foo" and "bar", should match Method A
|
||||
$this->request->setQueryString([
|
||||
'foo' => 'valueFoo',
|
||||
'bar' => 'valueBar',
|
||||
]);
|
||||
|
||||
$params = $this->request->getParams();
|
||||
$this->assertArrayHasKey('foo', $params);
|
||||
$this->assertSame('valueFoo', $params['foo']);
|
||||
$this->assertArrayHasKey('bar', $params);
|
||||
$this->assertSame('valueBar', $params['bar']);
|
||||
$this->assertArrayNotHasKey('baz', $params);
|
||||
}
|
||||
|
||||
public function testGetParamsWithAllOptional(): void
|
||||
{
|
||||
$this->setupMultiMethodRoute();
|
||||
|
||||
// Pass only "bar", should match Method A
|
||||
$this->request->setQueryString([
|
||||
'bar' => 'valueBar',
|
||||
]);
|
||||
|
||||
$params = $this->request->getParams();
|
||||
|
||||
$this->assertArrayHasKey('bar', $params);
|
||||
$this->assertSame('valueBar', $params['bar']);
|
||||
$this->assertArrayNotHasKey('foo', $params);
|
||||
$this->assertArrayNotHasKey('baz', $params);
|
||||
}
|
||||
|
||||
public function testGetParamsMatchesMethodB(): void
|
||||
{
|
||||
$this->setupMultiMethodRoute();
|
||||
|
||||
// Pass only "baz", should match Method B
|
||||
$this->request->setQueryString([
|
||||
'baz' => 'valueBaz',
|
||||
]);
|
||||
|
||||
$params = $this->request->getParams();
|
||||
|
||||
$this->assertArrayHasKey('baz', $params);
|
||||
$this->assertSame('valueBaz', $params['baz']);
|
||||
$this->assertArrayNotHasKey('foo', $params);
|
||||
}
|
||||
|
||||
public function testGetParamsFallbackForMixedAndUnknown(): void
|
||||
{
|
||||
$this->setupMultiMethodRoute();
|
||||
|
||||
// Mixed and unknown should fallback to raw params
|
||||
$this->request->setQueryString([
|
||||
'foo' => 'valueFoo',
|
||||
'baz' => 'valueBaz',
|
||||
'extra' => 'unexpected',
|
||||
]);
|
||||
|
||||
$params = $this->request->getParams();
|
||||
|
||||
$this->assertArrayHasKey('foo', $params);
|
||||
$this->assertSame('valueFoo', $params['foo']);
|
||||
$this->assertArrayHasKey('baz', $params);
|
||||
$this->assertSame('valueBaz', $params['baz']);
|
||||
$this->assertArrayHasKey('extra', $params);
|
||||
$this->assertSame('unexpected', $params['extra']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to attach a route with multiple SDK methods to the request.
|
||||
*/
|
||||
private function setupMultiMethodRoute(): void
|
||||
{
|
||||
$route = new Route(Request::METHOD_GET, '/multi');
|
||||
|
||||
$methodA = new Method(
|
||||
namespace: 'namespace',
|
||||
group: 'group',
|
||||
name: 'methodA',
|
||||
description: 'desc',
|
||||
auth: [],
|
||||
responses: [],
|
||||
parameters: [
|
||||
new Parameter('foo'),
|
||||
new Parameter('bar', optional: true),
|
||||
],
|
||||
);
|
||||
|
||||
$methodB = new Method(
|
||||
namespace: 'namespace',
|
||||
group: 'group',
|
||||
name: 'methodB',
|
||||
description: 'desc',
|
||||
auth: [],
|
||||
responses: [],
|
||||
parameters: [
|
||||
new Parameter('baz'),
|
||||
],
|
||||
);
|
||||
|
||||
$route->label('sdk', [$methodA, $methodB]);
|
||||
$this->request->addFilter(new First());
|
||||
$this->request->addFilter(new Second());
|
||||
$this->request->setRoute($route);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user