appwrite/src/Appwrite/Platform/Tasks/ScheduleBase.php

197 lines
7.5 KiB
PHP
Raw Normal View History

2024-01-11 03:06:59 +00:00
<?php
namespace Appwrite\Platform\Tasks;
2024-10-01 14:30:47 +00:00
use Appwrite\Utopia\Queue\Connections;
2024-01-11 03:06:59 +00:00
use Swoole\Timer;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Exception;
2024-01-11 03:06:59 +00:00
use Utopia\Database\Query;
2024-03-06 17:34:21 +00:00
use Utopia\Platform\Action;
2024-04-01 11:02:47 +00:00
use Utopia\System\System;
2024-01-11 03:06:59 +00:00
abstract class ScheduleBase extends Action
{
protected const UPDATE_TIMER = 10; //seconds
protected const ENQUEUE_TIMER = 60; //seconds
protected array $schedules = [];
2024-10-01 14:30:47 +00:00
protected Connections $connections;
2024-01-11 03:06:59 +00:00
abstract public static function getName(): string;
2024-10-01 14:30:47 +00:00
2024-01-11 03:06:59 +00:00
abstract public static function getSupportedResource(): string;
2024-05-29 15:59:10 +00:00
abstract public static function getCollectionId(): string;
2024-01-11 03:06:59 +00:00
abstract protected function enqueueResources(
2024-10-01 14:30:47 +00:00
array $pools,
callable $getConsoleDB
2024-01-11 03:06:59 +00:00
);
public function __construct()
{
2024-10-01 14:30:47 +00:00
$this->connections = new Connections();
2024-01-11 03:06:59 +00:00
$type = static::getSupportedResource();
$this
->desc("Execute {$type}s scheduled in Appwrite")
->inject('pools')
2024-10-01 14:30:47 +00:00
->inject('getConsoleDB')
2024-01-11 03:06:59 +00:00
->inject('getProjectDB')
2024-10-01 14:30:47 +00:00
->callback(fn (array $pools, callable $getConsoleDB, callable $getProjectDB) => $this->action($pools, $getConsoleDB, $getProjectDB));
2024-01-11 03:06:59 +00:00
}
/**
* 1. Load all documents from 'schedules' collection to create local copy
* 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.
*/
2024-10-01 14:30:47 +00:00
public function action(array $pools, callable $getConsoleDB, callable $getProjectDB): void
2024-01-11 03:06:59 +00:00
{
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
2024-10-01 14:30:47 +00:00
[$_, $_, $dbForConsole] = $getConsoleDB();
2024-01-11 03:06:59 +00:00
/**
* Extract only necessary attributes to lower memory used.
*
* @return array
* @throws Exception
* @var Document $schedule
*/
$getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array {
$project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId'));
2024-02-20 14:25:01 +00:00
$collectionId = match ($schedule->getAttribute('resourceType')) {
'function' => 'functions',
2024-06-17 12:44:12 +00:00
'message' => 'messages',
'execution' => 'executions'
2024-02-20 14:25:01 +00:00
};
2024-01-11 03:06:59 +00:00
$resource = $getProjectDB($project)->getDocument(
2024-05-29 16:09:14 +00:00
static::getCollectionId(),
2024-01-11 03:06:59 +00:00
$schedule->getAttribute('resourceId')
);
return [
'$internalId' => $schedule->getInternalId(),
'$id' => $schedule->getId(),
2024-01-11 03:06:59 +00:00
'resourceId' => $schedule->getAttribute('resourceId'),
'schedule' => $schedule->getAttribute('schedule'),
'active' => $schedule->getAttribute('active'),
2024-01-11 03:06:59 +00:00
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'project' => $project, // TODO: @Meldiron Send only ID to worker to reduce memory usage here
'resource' => $resource, // TODO: @Meldiron Send only ID to worker to reduce memory usage here
];
};
$lastSyncUpdate = DateTime::now();
$limit = 10_000;
$sum = $limit;
$total = 0;
$loadStart = \microtime(true);
$latestDocument = null;
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
if ($latestDocument) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
2024-04-01 11:02:47 +00:00
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
2024-01-11 03:06:59 +00:00
Query::equal('resourceType', [static::getSupportedResource()]),
Query::equal('active', [true]),
]));
$sum = \count($results);
$total = $total + $sum;
foreach ($results as $document) {
try {
$this->schedules[$document->getInternalId()] = $getSchedule($document);
2024-01-11 03:06:59 +00:00
} catch (\Throwable $th) {
2024-05-29 16:09:14 +00:00
$collectionId = static::getCollectionId();
2024-02-20 14:25:01 +00:00
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
2024-01-11 03:06:59 +00:00
Console::error($th->getMessage());
}
}
$latestDocument = \end($results);
}
Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds");
Console::success("Starting timers at " . DateTime::now());
2024-10-01 14:30:47 +00:00
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($getConsoleDB, &$lastSyncUpdate, $getSchedule, $pools) {
[$connection,$pool, $dbForConsole] = $getConsoleDB();
$connections = new Connections();
$connections->add($connection, $pool);
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
$time = DateTime::now();
$timerStart = \microtime(true);
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
$limit = 1000;
$sum = $limit;
$total = 0;
$latestDocument = null;
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
Console::log("Sync tick: Running at $time");
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
if ($latestDocument) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
]));
2024-09-20 16:30:05 +00:00
2024-10-01 14:30:47 +00:00
$sum = count($results);
$total = $total + $sum;
2024-09-20 16:30:05 +00:00
2024-10-01 14:30:47 +00:00
foreach ($results as $document) {
2024-10-01 14:39:43 +00:00
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
2024-09-20 16:30:05 +00:00
2024-10-01 14:30:47 +00:00
// Check if resource has been updated since last sync
$org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null;
$new = \strtotime($document['resourceUpdatedAt']);
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
if (!$document['active']) {
2024-10-07 15:02:18 +00:00
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
2024-10-01 14:30:47 +00:00
unset($this->schedules[$document->getInternalId()]);
} elseif ($new !== $org) {
2024-10-07 15:02:18 +00:00
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
2024-10-01 14:30:47 +00:00
$this->schedules[$document->getInternalId()] = $getSchedule($document);
}
2024-01-11 03:06:59 +00:00
}
2024-10-01 14:30:47 +00:00
$latestDocument = \end($results);
}
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
$lastSyncUpdate = $time;
$timerEnd = \microtime(true);
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
$connections->reclaim();
Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds");
});
2024-01-11 03:06:59 +00:00
2024-10-01 14:30:47 +00:00
Timer::tick(
static::ENQUEUE_TIMER * 1000,
fn () => $this->enqueueResources($pools, $getConsoleDB)
);
2024-06-03 18:09:58 +00:00
2024-10-01 14:30:47 +00:00
$this->enqueueResources($pools, $getConsoleDB);
2024-01-11 03:06:59 +00:00
}
}